How to create an observable array with undo? - javascript

I am trying to add knockout JS to a search page on our website. Currently you open up a jQuery dialog box, which has a number of checkboxes of criteria that you can select.
There are multiple dialogs with multiple types of criteria. When you open the dialog, the checkboxes do not take effect until you hit an "Update" button, if you click cancel or just close the window, the changes you made get reverted and the dialog is set to its former state.
I read this and a few other posts. However this seems to only work with ko.observable, and I cannot seem to get it to work with ko.observableArray.
Has anyone accomplished this or have any ideas?
An example of what I want to do:
Html:
<form>
<div>
<div>
<label><input type="checkbox" data-bind="checked: genders" value="1" />Male</label>
<label><input type="checkbox" data-bind="checked: genders" value="2" />Female</label>
</div>
</div>
<a id="buttonCancel">Cancel</a>
<a id="buttonUpdate">Update</a>
</form>
<div data-bind="text: ko.toJSON(viewModel)"></div>
Javascript:
var viewModel = {
genders: ko.observableArrayWithUndo([])
};
ko.applyBindings(viewModel);
$('#buttonCancel').click(function(){
viewModel.genders.resetChange();
});
$('#buttonUpdate').click(function(){
viewModel.genders.commit();
return false;
});

Here would be one way to approach it:
//wrapper to an observableArray of primitive types that has commit/reset
ko.observableArrayWithUndo = function(initialArray) {
var _tempValue = ko.observableArray(initialArray.slice(0)),
result = ko.observableArray(initialArray);
//expose temp value for binding
result.temp = _tempValue;
//commit temp value
result.commit = function() {
result(_tempValue.slice(0));
};
//reset temp value
result.reset = function() {
_tempValue(result.slice(0));
};
return result;
};
You would bind your checkboxes to yourName.temp and the other part of your UI to just yourName.
Here is a sample: http://jsfiddle.net/rniemeyer/YrfyW/
The slice(0) is one way to get a shallow copy of an array (or even just slice()). Otherwise, you would be performing operations on a reference to the same array.

Given HTML similar to:
<div>
<button data-bind="click: function() { undo(); }">Undo</button>
<input data-bind="value: firstName" />
<input data-bind="value: lastName" />
<textarea data-bind="value: text"></textarea>
</div>
You could use some Knockout code similar to this, basically saving the undo stack as a JSON string representation of the state after every change. Basically you create a fake dependent observable to subscribe to all the properties in the view, alternatively you could manually iterate and subscribe to each property.
//current state would probably come from the server, hard coded here for example
var currentState = JSON.stringify({
firstName: 'Paul',
lastName: 'Tyng',
text: 'Text'
})
, undoStack = [] //this represents all the previous states of the data in JSON format
, performingUndo = false //flag indicating in the middle of an undo, to skip pushing to undoStack when resetting properties
, viewModel = ko.mapping.fromJSON(currentState); //enriching of state with observables
//this creates a dependent observable subscribed to all observables
//in the view (toJS is just a shorthand to traverse all the properties)
//the dependent observable is then subscribed to for pushing state history
ko.dependentObservable(function() {
ko.toJS(viewModel); //subscribe to all properties
}, viewModel).subscribe(function() {
if(!performingUndo) {
undoStack.push(currentState);
currentState = ko.mapping.toJSON(viewModel);
}
});
//pops state history from undoStack, if its the first entry, just retrieve it
window.undo = function() {
performingUndo = true;
if(undoStack.length > 1)
{
currentState = undoStack.pop();
ko.mapping.fromJSON(currentState, {}, viewModel);
}
else {
currentState = undoStack[0];
ko.mapping.fromJSON(undoStack[0], {}, viewModel);
}
performingUndo = false;
};
ko.applyBindings(viewModel);
I have a sample of N-Level undo with knockout here:
http://jsfiddle.net/paultyng/TmvCs/22/
You may be able to adapt for your uses.

Related

Can't bind to dynamically added input in Bootstrap modal of Angular component

First of all, I don't see how the modal could have anything to do with this issue since its actually in this component's code, not a child. Still, this is in a modal, just in case.
I'm opting to not use FormArray since I need to keep track of my selections that may be added in a separate object, so I'm just creating unique IDs for the controls and adding them to the FormGroup. I can access the controls in the ts code, set values, get values, etc, but the form isn't binding and I can't figure out why not.
I can have an unknown number of items in this modal form, which will each have a selector (dropdown to select a property) and then the input to be able to modify some data. The input could be of different types, so it needs to be added and binded upon the choice from the selector.
<form [formGroup]="multiEditFormGroup" novalidate>
<div *ngFor="let item of multiEditSelections; let i = index">
<div>
<mdb-select [options]="availablePropsSelect"
placeholder="Choose property to edit"
class="colorful-select dropdown-main"
(selected)="multiEditSelectProp(item.selectorId, $event)"></mdb-select>
<label>Property to edit</label>
</div>
<div>
<div>
<input mdbActive
type="text"
class="form-control"
[formControlName]="item.controlId" />
</div>
</div>
</div>
</form>
Excepts of ts code:
public multiEditFormGroup = new FormGroup({});
onModalOpen():void {
const selectorId = this.utils.uuid();
this.multiEditFormGroup.addControl(selectorId, this.propSelector);
this.multiEditSelections.push({
selectorId: selectorId,
prop: '',
label: '',
type: '',
controlId: '' // not yet known since type hasn't been chosen
});
}
onSelect(selectorId: string, selectEvent: any): void {
let selection = this.multiEditSelections.find(selection => {
return selection.selectorId === selectorId;
});
const controlId = this.utils.uuid();
const prop = selectEvent.value;
this.multiEditFormGroup.get(selection.selectorId).setValue(prop);
this.multiEditFormGroup.markAsDirty();
this.multiEditFormGroup.markAsTouched();
const model = this.multiEditModel.find(model => {
return model.prop === prop;
});
this.multiEditFormGroup.addControl(controlId, this.testCtrl);
selection.controlId = controlId;
selection.prop = prop;
selection.label = model.label;
selection.type = model.type;
}
Logging to console shows that items are being added to the FormGroup, but the binding isn't happening to the DOM. For example, I can add a (keyup) event handler to my input and set the value in the form control which has already been created, and the FormGroup is updated. However, any input added in the front-end doesn't update the FG since it obviously isn't binding. Is this a timing issue or because the controlId is being updated later? I'm creating the FormControl before updating my array that is being iterated.
Oh and I get no errors in console on this.
I think you need to make this change:
[formControlName]="item.controlId"
needs to be:
formControlName="{{item.controlId}}"

Allow users to filter data contained in the data collection in MeteorJS

Hello application must allow users filtration data contained in the data collection. The filter has to be supported by the checkboxes. The user selects (checkbox) it interesting data (name, address ...) and submit your selection. The table below displays the selected data from the data collection.
Does anyone have an idea how it should look like the code that retrieves data from the collection of data, taking into account filtration made by the user.
My idea was. Get the value of the checkbox and save them in the array and the later inclusion of data from the array in the code to retrieve data from the collection using the subroutine find ().
'change .cd-checkbox-1': function(event, target) {
var x = event.target.checked;
Session.set("statevalue", x);
var array = [];
if (Session.get("statevalue") === true) {
$( "[name='inputCheckboxPodmiot']:checked" ).each( function( index, element ) {
array.push( element.value );
});
};
var arrayLength = array.length;
for (var i = 0; i < arrayLength; i++) {
var abiRejestr = WkaRejestrAbi.find( {}, { fields: { array[i]: 1 } } );
}
}
One approach is to use Reactive Vars. Sessions are not recommended as they pollute the global namespace.
Example code :
In main.html
<template name="test">
<input type="checkbox" id="checkbox1" name="name" value="data">Check Me
<input type="checkbox" id="checkbox2" name="name" value="data">Check Me2
<input type="checkbox" id="checkbox3" name="name" value="data">Check Me2
{{showData}}
</template>
In Main.js
var check_status='';
//Reactive Var Initialization
Template.main.onCreated(function (){
check_status1=new ReactiveVar({});
check_status2=new ReactiveVar({});
check_status3=new ReactiveVar({});
});
Template.main.helpers({
showData : function(){
return Collection.find({$and : [{check_status1.get(),check_status2.get(),check_status3.get()}]}).fetch();
}
});
Template.main.events({
"change #checkbox1" : function(event) {
if($(event.currentTarget).is(":checked").val())
check_status1.set({field1: 'data1'});
else
check_status1.set({});
},
"change #checkbox2" : function(event) {
if($(event.currentTarget).is(":checked").val())
check_status2.set({field2: 'data2'});
else
check_status2.set({});
},
"change #checkbox3" :function(event) {
if($(event.currentTarget).is(":checked").val())
check_status3.set({field3: 'data2'});
else
check_status3.set({});
},
});
Explanation:
When we initialize the reactive var check_status we set the value equal to {}. In the helper, at the time of rendering, the same data is passed to the query Collection.find(check_status.get()) which is as good as show all data.
As soon as you click on the checkbox, the event described in the Template.main.events is triggered which sets the value of check_status to {field: data}. Since, this is a reactive var, the showData template is re run and this time the query is Collection.find({field: data}), so only fields, where field matched 'data' is returned.
You will need to add the reactive var package(meteor add reactive-var) before using these commands.
Alternatively, you can also use Tracker.autorun is you want to continue using session variables.

Radio buttons bound to knockout computed

I have some problems with knockoutjs writables computes observables I think.
I created a fiddle.
What I need is actually not so hard:
I have the nullable WeightInGramms and VolumeInMilliliters values.
These values should be bound to two input fields (only one of them should be visible).
At the top, the user can choose which of these values he want to use with the radio buttons.
At initialisation, when both of them are null, the "g" radio button should be checked, also when WeightInGramms is not null. When VolumeInMilliliters have some value, the "ml" radio button should be checked.
I used a knockoutjs writable computes observable for this, please correct me if there is a better way to do this!
So, the read function seems to work, when I change the value in the input which are bind to WeightInGramms or VolumeInMilliliters. But when I change the radio buttons nothing happens...
var ViewModel = function (data) {
var self = this;
this.VolumeInMilliliters = ko.observable(data.VolumeInMilliliters);
this.WeightInGramms = ko.observable(data.WeightInGramms);
this.GrammIsSelected = ko.computed({
read: function() {
return (!self.WeightInGramms() && !self.VolumeInMilliliters()) || !self.VolumeInMilliliters();
},
write: function (newValue) {
console.log(newValue);
return newValue;
},
owner: this
});
};
When I change the radio buttons, the corresponding input field should be visible:
<div data-bind="visible: GrammIsSelected">g is active</div>
<div data-bind="visible: !GrammIsSelected()">ml is active</div>
Edit:
When the Form is loaded for the first Time both values will be null -> the "g" button should be checked.
The observables can be initialized with:
null, null
33, null
null, 33
Both can be null, but only one of them can have a value.
If the user types in a value, and then clicks the other radio the value can be applied to the other value.
I hop it is a bit clearer
Some tips:
Make your viewModel (JS) resemble the view (HTML) as much as possible. Additionally, this avoids having to repeat too much markup. In this case, radio buttons are always lists, and so it is most convenient to store the options in an array.
Instead of testing whether GrammIsselected, you should define a selected observable that holds the selected metric. This way if you ever add more options, the code will still work without refactoring.
When to use a computed property? A computed property adds readonly value by calculating a result based on multiple observables/ variables. A writeable computed property does the same, except you can write back changes. This makes it especially useful for 'Select all' style checkboxes (see example 2 in the docs), data validation & transformations.
The absolutely clearest setup for what you want to achieve would be the following:
var ViewModel = function (data) {
this.metrics = [
{ name: 'g', value: ko.observable(data.WeightInGramms) },
{ name: 'ml', value: ko.observable(data.VolumeInMilliliters) }
];
this.selectedMetric = ko.observable(this.metrics[0]);
};
By setting an object as observable (selectedMetric), you can furthermore simplify the markup for the volume/weight input:
<div class="control-group">
<label class="control-label">choose</label>
<div class="controls" data-bind="with: selectedMetric">
<input type="text" data-bind="value: value">
<span class="help-inline" data-bind="text: '(' + name + ')'"></span>
</div>
</div>
Getting the 'final value' of your app would be as easy as retrieving selectedMetric().value().
A computed property isn't super useful here, but for example, if you wanted to provide a way for the user to both set the g/ml with radio buttons and text, you could add the following method to your viewModel:
this.selectedMetricByText = ko.computed({
read: function() {
return this.selectedMetric().name;
},
write: function(value) {
var newMetric = ko.utils.arrayFirst(this.metrics, function(metric) {
return metric.name === value;
}) || false;
this.selectedMetric(newMetric || this.metrics[0]);
}
}, this);
Fiddle
Your write function doesn't write anything, it seems?
Contrary to this other answer, based on my experience I'll give you the advice not to avoid writeable computeds: used wisely they can be very effective!
Note: in my answer I try to remain close to the original design from the question, but if you're able (have resources available) I recommend redesigning things even more based on the answer by #Tyblitz.
Here's the way you could approach this utilizing a computed:
var ViewModel = function (data) {
var self = this;
self.VolumeInMilliliters = ko.observable(data.VolumeInMilliliters);
self.WeightInGramms = ko.observable(data.WeightInGramms);
var _measurementType = ko.observable("volume");
self.MeasurementType = ko.computed({
read: function() {
return _measurementType();
},
write: function (newValue) {
_measurementType(newValue);
self.VolumeInMilliliters(newValue === "volume" ? 0 : null);
self.WeightInGramms(newValue === "mass" ? 0 : null);
}
});
};
ko.applyBindings(new ViewModel({ VolumeInMilliliters: 12 }));
label { cursor: pointer; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<label>
<input type="radio" name="measurementType" value="volume" data-bind="checked: MeasurementType">
Volume
</label>
<input data-bind="value: VolumeInMilliliters, enable: MeasurementType() === 'volume'">
<label>
<input type="radio" name="measurementType" value="mass" data-bind="checked: MeasurementType">
Weight in gramms
</label>
<input data-bind="value: WeightInGramms, enable: MeasurementType() === 'mass'">
For radio buttons, you need to use the "checked" binding.
http://knockoutjs.com/documentation/checked-binding.html
And for my personal experience (as a KO nija) I have to give you the advice: avoid writeable ko computed.
<input type="radio" name="unitSelector" value="g" data-bind="checked: unit" /> Grams</br>
<input type="radio" name="unitSelector" value="ml" data-bind="checked: unit" /> Millis</br>
Now the view model
var ViewModel = function (data) {
var self = this;
self.unit = ko.observable('g');
self.userValue = ko.observable(data.WeightInGramms);
};
Now the binding should only care about the value entered by the user, you don't need computed here and you don't need two fields...
<input type="text" data-bind="textInput: userValue ">
<span data-bind="text: unit"> </span>
It looks really too simple but that's what you need, as #Jotabe mentioned, you should take measurement and the unit as two separate things... what you do with this thing later, could be done with computed observables.
If this thing doesn't solve your problem then you should tell what you really want...

KnockoutJS "checked" binding doesn't work with observableArray and radio button lists

I have an observableArray called SelectedAttributeValueIds which I need to be bound to a radio button list using the "checked" binding. e.g.
<input type="radio" data-bind="checked: SelectedAttributeValueIds" />
However KnockoutJS is replacing the observableArray with a single value that is just observable. so instead of the SelectedAttributeValueIds having a value of [123] it will be "123"
I did this as a workaround but was wondering if there is an easier way:
<input type="radio" data-bind="event: { change: function() { $parent.SelectedAttributeValueIds([$data.ID()]); } }" />
For radio buttons, KO assumes that there will only be one "value" to write. For checkboxes, it would add/remove values from the array.
It sounds like you want to always write a single value, but have it be in an array as the only element.
One option would be to create a ko.computed to represent the item in an array. Another option would be to use a writeable ko.computed to bind against the input and have it broker the value between the formats that you want. Something like:
this.SelectedAttributeValueIds = ko.observableArray();
this.SelectedAttributeValueId = ko.computed({
read: function() {
var values = this.SelectedAttributeValueIds();
return values.length ? values[0] : [];
},
write: function(newValue) {
this.SelectedAttributeValueIds([newValue]);
},
owner: this
});

Working with a list of checkboxes in knockoutjs

I'm trying to get my head around Knockout.js and I'm quite stuck when it comes to checkboxes.
Server side I'm populating a set of checkboxes with their corresponding values. Now, when any of the unchecked checkboxes are checked, I need to store it's value in a comma-seperated string. When they're unchecked, the value needs to be deleted from the string.
Have anyone got a hint on how to achieve this with knockoutjs?
I have the following code so far:
ViewModel:
$().ready(function() {
function classPreValue(preValue)
{
return {
preValue : ko.observable(preValue)
}
}
var editOfferViewModel = {
maxNumOfVisitors : ko.observable(""),
goals : ko.observable(""),
description : ko.observable(""),
contact : ko.observable(""),
comments : ko.observable(""),
classPreValues : ko.observableArray([]),
addPreValue : function(element) {
alert($(element).val());
this.classPreValues.push(new classPreValue(element.val()));
}
};
ko.applyBindings(editOfferViewModel);
});
And my checkboxes are populated with a foreach loop:
<input data-bind="checked: function() { editOfferViewModel.addPreValue(this) }"
type="checkbox" checked="yes" value='#s'>
#s
</input>
I try to pass the checkbox element as the parameter to my addPreValue() function, but nothing seems to happen when I check the checkbox?
Any help/hints on this is greatly appreciated!
The checked binding expects to be passed a structure that it can read/write against. This could be a variable, an observable, or a writable dependentObservable.
When passed an array or observableArray, the checked binding does know how to add and remove simple values from the array.
Here is a sample that also includes a computed observable that contains the array as comma delimited values. http://jsfiddle.net/rniemeyer/Jm2Mh/
var viewModel = {
choices: ["one", "two", "three", "four", "five"],
selectedChoices: ko.observableArray(["two", "four"])
};
viewModel.selectedChoicesDelimited = ko.computed(function() {
return this.selectedChoices().join(",");
}, viewModel);
ko.applyBindings(viewModel);
HTML:
<ul data-bind="template: { name: 'choiceTmpl', foreach: choices, templateOptions: { selections: selectedChoices } }"></ul>
<script id="choiceTmpl" type="text/html">
<li>
<input type="checkbox" data-bind="attr: { value: $data }, checked: $item.selections" />
<span data-bind="text: $data"></span>
</li>
</script>
Why isn't there a Mutually exclusive checkboxes example Online somewhere
Since this link came up first whilst I was searching for mutually exclusive checkboxes I will share my answer here. I was banging my head against the wall with all my attempts. By the way, when you handle the click event in a binding in-line knockoutjs it seems to disconnect the bindings(maybe only because I tried to call my resetIllnesses function as defined below) even if you return true from the function. Maybe there is a better way but until then follow my lead.
Here is the type I needed to bind.
var IllnessType = function (name,title) {
this.Title = ko.observable(title);
this.Name = ko.observable(name);
this.IsSelected = ko.observable(false);
};
The array to bind with.
model.IllnessTypes = ko.observableArray(
[new IllnessType('IsSkinDisorder', 'Skin Disorder'),
new IllnessType('IsRespiratoryProblem', 'Respiratory Problem'),
new IllnessType('IsPoisoning', 'Poisoning'),
new IllnessType('IsHearingLoss', 'Hearing Loss'),
new IllnessType('IsOtherIllness', 'All Other Illness')]
);
The reset illness function to clear them all.
model.resetIllnesses = function () {
ko.utils.arrayForEach(model.IllnessTypes(), function (type) {
type.IsSelected(false);
});
};
The markup
<ul data-bind="foreach:IllnessTypes,visible: model.IsIllness()">
<li><label data-bind="html: Title"></label></li>
<li><input class="checkgroup2" type="checkbox"
data-bind="attr:{name: Name },checked:IsSelected" /></li>
</ul>
This just doesn't work
If you have been struggling with trying to call the resetIllness function as I below, you will feel my pain.
<input type='checkbox' data-bind="checked:IsSelected,
click: function() { model.resetIllnesses(); return true; }" />
you have been sharing my pain. Well, it works! when you call it from following example.
Notice that there is a class that I added above so that I can add the click function.
The script that makes all your problems go away.
<script type="text/javascript">
$(function() {
$(".checkgroup2").on('click', function() {
model.resetIllnesses();
var data = ko.dataFor(this);
data.IsSelected(true);
});
});
</script>
Send info to the server
Also, in my case I had to send the information up to the server differently than the default html format so I changed the inputs a little.
<input class="checkgroup2" type="checkbox" data-bind="checked:IsSelected" />
<input type="hidden" data-bind="attr:{name: Name },value:IsSelected" />

Categories