knockoutjs deep copy observable array - javascript

I have an obeservable array in Knockout, say data that contains objects with observable properties. In one part of my application I want to allow users to add to this array and ultimately mess about with it, but if they cancel to revert the array to the original state.
One solution I though of is to create a copy called dataCopy that replaces the original array in the event of cancellation, but this only creates another pointer to the same underlying data, so it will also reflect the changes that were made which is not what I want.
I've tried converting it to javascript, via ko.toJS(data) and this creates a js array of the data, however all of the objects lose their observable properties and when I re-create the data using ko.observableArray(dataCopy) the app falls over as the properties of the objects are no longer observable.
How can I create a clone of a knockout array rather than another pointer?

This is an example of how you could achieve this using the ko mapping plugin for deeply copying your observable arrays.
I doubt any other method of copying would work for you such as .slice or ko.toJS since as you mentioned you want a deep copy which goes as far as the underlying object's observable propertieš.
The important part here is the function :
function obsArrDeepCopy(from, to) {
ko.mapping.fromJS(ko.toJS(from), {}, to);
}
Which will do the deep copy by first converting the source array to a plan JS array and then populate the target array converting to observables the underlying object properties.
Full working example:
function Person(name) {
this.name = ko.observable(name);
}
var viewModel = {
people: ko.observableArray([
new Person("Annabelle"),
new Person("Bertie"),
new Person("Charles")
]),
peopleCopy: ko.observableArray(),
newName: ko.observable(''),
addPerson: function() { this.peopleCopy.push(new Person(this.newName())) },
commit: function() { obsArrDeepCopy(this.peopleCopy, this.people) },
cancel: function() { obsArrDeepCopy(this.people, this.peopleCopy) },
};
ko.applyBindings(viewModel);
obsArrDeepCopy(viewModel.people, viewModel.peopleCopy);
function obsArrDeepCopy(from, to) {
ko.mapping.fromJS(ko.toJS(from), {}, to);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<h2> Initial </h2>
<ul data-bind="foreach: people">
<li>
<span data-bind="text: name" />
</li>
</ul>
<br/><br/>
<h2> Copy </h2>
<ul data-bind="foreach: peopleCopy">
<li>
<span data-bind="text: name" />
</li>
</ul>
<input type="text" data-bind="textInput: newName" />
<button type="button" data-bind="click: addPerson"> Add </button>
<br/><br/>
<button type="button" data-bind="click: commit"> Commit </button>
<button type="button" data-bind="click: cancel"> Cancel </button>

Related

How to push inside Json Object using angularjs

i'm tryink to push an object inside a json object, but it gives me a duplicated json object, i will expalin:
heres my html:
<input type="text" style="width: 40% !important;" placeholder="Nom" class="input-sm" ng-model="company.link.nom" />
<input type="text" style="width: 40% !important;" placeholder="Lien" class="input-sm" ng-model="company.link.value" />
<a class="btn btn-wide btn-primary" ng-click="company.addExternalLinktoGrid()"><i class="fa fa-plus"></i> Ajouter </a>
here is addExternalLinktoGrid function:
var linkJsonObj = [];
var cp=1;
company.addExternalLinktoGrid = function() {
company.link.id=cp;
currentObj.push(company.link);
console.log(JSON.stringify(company.link));
linkJsonObj.push(company.link);
console.log(JSON.stringify(linkJsonObj));
cp++;
}
For example, lets assume we add new object: company.link.nom="toto" and company.link.value="titi", in this case linkJsonObj print:
[{"nom":"toto","value":"titi","id":1}]
lets add the second object company.link.nom="momo" and company.link.value="mimi", in this case linkJsonObj print:
[{"nom":"momo","value":"mimi","id":2},
{"nom":"momo","value":"mimi","id":2}]
This is what I got, I don't know why? but the expected behaviour is:
[{"nom":"toto","value":"titi","id":1},
{"nom":"momo","value":"mimi","id":2}]
can anyone help please?
company.link in an object and if you push this object to an array it is passed by reference. If you overwrite the values of this object it will be overwritten in your array. What you need is to copy the object (values). If you push it multiple times to an array the array will contain multiple references to the same object.
var newObject = {
nom: company.link.nom,
value: company.link.value,
id: cp
}
linkJsonObj.push(newObject);

cant make an object I added to a knockout model observable

Here is the fiddle demonstrating the problem http://jsfiddle.net/LkqTU/31955/
I made a representation of my actual problem in the fiddle. I am loading an object via web api 2 and ajax and inserting it into my knockout model. however when I do this it appears the attributes are no longer observable. I'm not sure how to make them observable. in the example you will see that the text box and span load with the original value however updating the textbox does not update the value.
here is the javascript.
function model() {
var self = this;
this.emp = ko.observable('');
this.loademp = function() {
self.emp({
name: 'Bryan'
});
}
}
var mymodel = new model();
$(document).ready(function() {
ko.applyBindings(mymodel);
});
here is the html
<button data-bind="click: loademp">
load emp
</button>
<div data-bind="with: emp">
<input data-bind="value: name" />
<span data-bind="text: name"></span>
</div>
You need to make name property observable:
this.loademp = function(){
self.emp({name: ko.observable('Bryan')});
}

ko.mapping.fromJS not mapping nested value after value is set null

I am applying JSON data to my ViewModel using ko.mapping.fromJS. This JSON data has nested objects within it, like so:
var data = {
item : "#1234",
description: "This is item #1",
moreinfo: {
condition: "Good"
}
}
When I try to re-map new data to the object it works fine, updating all the values. However, if I remap the value of moreinfo to nullbecause this item doesn't have moreInfo, like so:
var data2 = {
item : "#4567",
description: "This is item #2",
moreinfo:null
}
it doesn't update the DOM but instead keeps the previous value condition:"Good". If I then update the value one more time to data that has moreinfo, like so:
var data3 = {
item : "#7890",
description: "This is item #3",
moreinfo: {
condition: "Bad"
}
}
it still updates the item and description, but still it doesn't update the DOM but instead keeps the value condition:"Good".
Am I using mapping incorrectly or can you just not allow the value to become null?
Javascript:
var viewModel;
$("#button1").on("click", function(){
viewModel = ko.mapping.fromJS(data);
ko.applyBindings(viewModel, $("itemWrapper")[0]);
});
$("#button2").on("click", function(){
ko.mapping.fromJS(data2, {}, viewModel);
});
$("#button3").on("click", function(){
ko.mapping.fromJS(data3, {}, viewModel);
});
HTML:
<div id="itemWrapper">
<p data-bind="text: item"></p>
<p data-bind="text: description"></p>
<p data-bind="text: moreinfo.condition"></p>
</div>
<button id="button1">
Bind Data #1
</button>
<button id="button2">
Bind Data #2
</button>
<button id="button3">
Bind Data #3
</button>
JSfiddle: https://jsfiddle.net/hrgx5f1y/
If you applyBindings with Button #1, then map the null with #2, then map new values with #3 you will see the issue I describe.
If you applyBindings with Button #1, then map new values with #3, then map new values with #4 (none of these are null) it works perfectly.
The way that Knockout binding handlers work is that they hook up to an observable, and they respond when that observable changes. It does not bind to your binding expression but to your observable object So your binding:
<p data-bind="text: moreinfo.condition"></p>
...takes the condition property of moreinfo, which is an observable object and subscribes to it. When you do this:
var data4 = {
item : "#0000",
description: "This is item #4",
moreinfo: {
condition: "Meh"
}
}
ko.mapping.fromJS(data4, {}, viewModel);
...it works because Knockout can tie the structure of data4 to your view model and update that exact same observable object with the new value 'Meh'.
If you do this instead:
var data2 = {
item : "#4567",
description: "This is item #2",
moreinfo:null
}
$("#button2").on("click", function(){
ko.mapping.fromJS(data2, {}, viewModel);
});
...it is not updating that observable, but rather is updating the moreinfo property so it's null. Since a binding is to an observable object, not an expression, even if you update the view model so moreinfo isn't null anymore, the binding expression is not re-evaluated; your DOM is still bound to that same original observable.
You can work around this by having moreinfo as an observable, binding to it, and subsequently binding to condition; that way, if either updates, your DOM will update as expected. For example:
<!-- ko with:moreinfo -->
<p data-bind="text: condition"></p>
<!-- /ko -->

Create checkbox list from array of objects with knockout

I would like to know what is the proper way to create a list of checkboxes bound to a selected list for an array of objects using knockout. It is very straight forward using basic values like a list of words but can it be done with an array of objects.
this works:
<script type="text/JavaScript">
function ViewModel(){
self.choices = ["one","two","three","four","five"];
self.selectedChoices = ko.observableArray(["two", "four"]);
self.selectedChoicesDelimited = ko.dependentObservable(function () {
return self.selectedChoices().join(",");
});
}
</script>
<ul class="options" data-bind="foreach: choices">
<li><input type="checkbox" data-bind="attr: { value: $data }, checked: selectedChoices" /><span data-bind="text: $data"></span></li>
</ul>
<div data-bind="text: selectedChoicesDelimited"></div>
But I can't figure out how to get it to work if the items are objects like this:
self.choices = [{"Id":1,"Name":"one"},{"Id":2,"Name":"two"}, {"Id":3,"Name":"three"}, {"Id":4,"Name":"four"}, {"Id":5,"Name":"five"}];
self.selectedChoices = ko.observableArray([{"Id":2,"Name":"two"}, {"Id":4,"Name":"four"}]);
In the end I would like to be able to create checkbox lists from MultiSelectList object created in my MVC controller and passed to the view in the ViewBag.
#ebohlman answer about using CheckedValue is what I was looking for. One other note however, is that specifying the SelectedChoices directly as I did above will not work because the objects will not equal the objects in the Choices array even though the values in them are the same. Instead I've added code to push the selected choices onto the SelectedChoices array after defining it:
self.choices = [{"Id":1,"Name":"one","Selected":true},{"Id":2,"Name":"two","Selected":false}];
self.selectedChoices = ko.observableArray();
$.each(self.choices, function (index, value) {
if(value.Selected) self.selectedChoices.push(value);
});
Use the checkedValue binding:
<li>
<input type="checkbox" data-bind="checkedValue: $data, checked: $root.selectedChoices" />
<span data-bind="text: Name"></span>
</li>
And pick out the Name values when calculating the delimited string:
self.selectedChoicesDelimited = ko.computed(function () {
return ko.utils.arrayMap(self.selectedChoices(), function(v) {
return v.Name;
}).join(",");
});

Handling observables in an array

Note: This is not a question about ObservableArrays.
Let's say I have the following viewmodel:
var viewmodel = {
arrayOfBooleans: [
ko.observable(false),
ko.observable(false),
ko.observable(false)
]
}
And a view like so:
<div data-bind="foreach: arrayOfBooleans">
<button data-bind="click: ????">Set to true</button>
</div>
What can I do inside the foreach to get the <button> to set the observable to true when clicked? Using data-bind="click: someFunction", the first argument someFunction gets is the unwrapped values of observables in the array (not the observables themselves), and seemingly no way to get back at the observables or to pass custom arguments.
Hope it will give some idea .
var viewmodel = {
var self = this;
self.arrayOfBooleans = ko.observableArray([]);
self.arrayOfBooleans.push(new _newBoolean());
self.arrayOfBooleans.push(new _newBoolean());
self.arrayOfBooleans.push(new _newBoolean());
function _newBoolean() {
self.one = ko.observable(false);
}
self.setToTrue = function(index){
self.arrayOfBooleans()[index].one(true);
};
}
If you want it as button
<div data-bind="foreach: arrayOfBooleans">
<button data-bind="click: $root.setToTrue($parent.arrayOfBooleans.indexOf($data))">
Set to true
</button>
<input type=hidden data-bind="value:one" />
</div>
If you want it as radio button than it is much more simple
<div data-bind="foreach: arrayOfBooleans">
<span>Set To True</span><input type=radio data-bind="value:one" />
</div>
If you like this..Please Click uplink
*or*
If it solves your problem .. Mark this as answer

Categories

Resources