How can I copy this knockout observable? - javascript

I am trying to add a "default" object to my Knockout observableArray. The way my code stands currently, I end up adding the exact same object to the array instead of a new instance of the object. I tried using jQuery's .extend(), but, that didn't work out so I am looking for input.
Here is a demonstration to show my problem: http://jsfiddle.net/2c8Fx/
HTML:
<div data-bind="foreach: People">
<input type="text" data-bind="value: Name" />
</div>
<button type="button" data-bind="click: AddPerson">Add Person</button>
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
Script:
function ViewModel() {
var self = this;
self.EmptyPerson = ko.mapping.fromJS({Name: null, Age: null});
self.People = ko.observableArray([self.EmptyPerson]);
self.AddPerson = function() {
self.People.push(self.EmptyPerson);
};
}
ko.applyBindings(new ViewModel());
This doesn't work because the observableArray is actually holding a reference to the same object for each index.
What's the best way to create a new instance of my Knockout object?

function ViewModel() {
var self = this;
self.People = ko.observableArray();
self.AddPerson = function() {
self.People.push(ko.mapping.fromJS({Name: null, Age: null}));
};
}
ko.applyBindings(new ViewModel());
I did this and it works like you think it would. I hope this helps.
Check out a working example here

Related

KnockoutJS Assembling updated data from components

I am new to KnockoutJS.
I have an app that works in a following way:
when page is loaded a complex data structure is passed to frontend
this datastructure is splitted into smaller chunks and these chunks of data are passed to components
user interacts with components to edit chunks of data
upon clicking a button updated complex data structure should be passed to backend
I have troubles with a fourth step. I've been reading throught documentation and yet I couldn't figure out how I am supposed to get updated data.
Here is a JSFiddle: https://jsfiddle.net/vrggyf45
Here is the same code in snippet. See the bottom of the question for what I've tried.
ko.components.register('firstComponent', {
viewModel: function(params) {
var self = this;
self.firstComponentValue = ko.observable(params.firstComponentValue);
},
template: '<div><pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre><input data-bind="value: firstComponentValue, valueUpdate: \'afterkeydown\'"></div>'
});
ko.components.register('secondComponent', {
viewModel: function(params) {
var self = this;
self.secondComponentValue = ko.observable(params.secondComponentValue);
},
template: '<div><pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre><input data-bind="value: secondComponentValue, valueUpdate: \'afterkeydown\'"></div>'
});
var json = '{"items":[{"componentName":"firstComponent","firstComponentValue":"somevalue"},{"componentName":"secondComponent","secondComponentValue":"someothervalue"}]}';
var data = JSON.parse(json);
var mainVM = {};
mainVM.items = ko.observableArray(
ko.utils.arrayMap(data.items,
function(item) {
return ko.observable(item);
}));
ko.applyBindings(mainVM);
$('input[type=button]').click(function() {
var updatedData = ko.dataFor(document.getElementById('main'));
//get updated json somehow?
console.log(data);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main">
<pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre>
<div data-bind="foreach: {data: items, as: 'item'}">
<hr>
<div data-bind="component: {name:componentName, params: item}">
</div>
</div>
<hr>
<input type="button" value="post data">
</div>
If I had a single view model I could have just used ko.dataFor($("#rootElement")) and send it to backend. However I intend to use components and they have their own viewmodels, which are not connected to the root viewmodel. I could have find them all with jQuery and use ko.dataFor but it looks like a big hack to me.
I also could have define all the viewmodels, including the components in the main viewmodel, but it makes components kind of useless.
Also I tried to change components viewmodels constructors so they would mutate input data and override incomming values with observables, but it seems like a hack to me as well.
Is there a function like ko.components or something that could give me all living viewmodels?
You can pass observables to the components so that their edits are immediately reflected in the parent object. ko.mapping is handy for converting plain JS objects to observables. Then when you want the data back, ko.toJS() it.
ko.components.register('firstComponent', {
viewModel: function(params) {
var self = this;
self.firstComponentValue = params.firstComponentValue;
},
template: '<div><pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre><input data-bind="value: firstComponentValue, valueUpdate: \'afterkeydown\'"></div>'
});
ko.components.register('secondComponent', {
viewModel: function(params) {
var self = this;
self.secondComponentValue = params.secondComponentValue;
},
template: '<div><pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre><input data-bind="value: secondComponentValue, valueUpdate: \'afterkeydown\'"></div>'
});
var json = '{"items":[{"componentName":"firstComponent","firstComponentValue":"somevalue"},{"componentName":"secondComponent","secondComponentValue":"someothervalue"}]}';
var data = JSON.parse(json);
var mainVM = {};
mainVM.items = ko.mapping.fromJS(data.items);
ko.applyBindings(mainVM);
$('input[type=button]').click(function() {
var updatedData = ko.toJS(ko.dataFor(document.getElementById('main')));
//Or, more simply: updatedData = ko.toJS(mainVM);
console.log(updatedData);
});
<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>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main">
<pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre>
<div data-bind="foreach: {data: items, as: 'item'}">
<hr>
<div data-bind="component: {name:componentName, params: item}">
</div>
</div>
<hr>
<input type="button" value="post data">
</div>

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')});
}

Remove items from Knockout observable array

I have the below structure for knockout model. It contains an observable array which in turn contains an object.
function ViewModel() {
var self = this;
self.newItem = ko.observable({
manufacturer: ko.observable(),
itemnumber: ko.observable(),
itemDescription: ko.observable()
});
self.AllItems = ko.observableArray();
self.addItem = function() {
self.newItem().manufacturer("test");
self.newItem().itemDescription("data");
self.AllItems.push(self.newItem);
};
self.removeItem = function(data) {
self.AllItems.remove(data);
};
}
First issue:Through this script I am entering a new itemnumber in the textbox and then clicking on add item to have the new item with the itemnumber from the textbox added to the observable array but when I change the item number and hit add it changes all the itemnumber inside the array. How can i have unique data inside the array.
Second issue: I need to remove the specific items from the array but it's not deleting it. Can someone please tell me how I can delete items from the observable array based on say the itemnumber property.
<input type="text" data-bind="value: newItem().itemnumber"/>
<div>
Items: <button data-bind="click: addItem">Add Item</button>
</div>
<div>
<table>
<tbody data-bind="template: { name: 'itemTemplate', foreach: AllItems }"></tbody>
</table>
</div>
<script type="text/html" id="itemTemplate">
<tr>
<td>
<input data-bind="value: itemnumber" />
Remove Item
</td>
</tr>
</script>
I have created this fiddle for quick view of the issue. Just started learning knockout so any help is appreciated.
http://jsfiddle.net/N3JaW/138/
Try the following for adding new item, which will solve your first issue:-
HTML code
<input type="text" id="textBox" data-bind="value : textBoxVal"/>
<div>
Items: <button data-bind="click: addItem">Add Item</button>
</div>
<div>
<table>
<tbody data-bind="template: { name: 'itemTemplate', foreach: AllItems }"></tbody>
</table>
</div>
<script type="text/html" id="itemTemplate">
<tr>
<td>
<input data-bind="value: itemnumber" />
Remove Item
</td>
</tr>
</script>
JS code:-
function ViewModel() {
var self = this;
self.newItem = ko.observable({
manufacturer: "",
itemnumber: "",
itemDescription: ""
});
self.textBoxVal = ko.observable();
self.AllItems = ko.observableArray();
self.addItem = function() {
self.newItem().manufacturer= "test";
self.newItem().itemDescription= "data";
self.newItem().itemnumber = self.textBoxVal();
self.AllItems.push(self.newItem);
};
self.removeItem = function(data) {
self.AllItems.remove(data);
};
}
$(document).ready(function() {ko.applyBindings(new ViewModel()); });
Your first issue was because, each time you are trying to add a new item, you were changing the value of itemNumber, which is an observable.
Observable value will be changed every where it is binded, when it's value is changed.
Instead you need to create new object and do push into the observableArray.
Refer doc to know more about observableArray.
For your second problem change removeItem as given below:-
self.removeItem = function(data) {
var dtIndex = self.AllItems.indexOf(data); //Get the index of the object you want to remove.
self.AllItems.splice(dtIndex, 1); //Then do splice
};
You can refer the above doc, to know how to use splice.
EDIT based on the suggestion in the comment :-
For working code of edited answer click here.
Hope this will solve your problem.

Knockout multiple click bindings do not work with IE8

The problem:
Multiple click binding do not work in IE8.
The code:
var Cart = function() {
var self = this;
self.books = ko.observableArray();
self.cds = ko.observableArray();
};
var TheModel = function() {
var self = this;
self.cart = ko.observable(new Cart());
self.showAddBook = function() {
self.cart.books.push(/* new book */);
};
self.showAddCD = function() {
self.cart.cds.push(/* new cd */);
};
};
<div data-bind="with: cart">
<h1>Books<h1>
<button data-bind="click: $parent.showAddBook">Add</button>
<div data-bind="foreach: books">
<span data-bind="text: name"></span> <!-- book has a name property -->
</div>
<hr/>
<h3>CDs</h3>
<button data-bind="click: $parent.showAddCD">Add</button>
<div data-bind="foreach: cds">
<span data-bind="text: name"></span> <!-- cd has a name property -->
</div>
</div>
Background:
Apologies in advance. I don't have access to jsFiddle at work.
I have a deadline to get this piece of work complete which is why I am using knockout with jQuery. Would love to use Angular but can't because we have to support IE8. Would love to use Durandal but I have no experience of it and don't have the time just yet to learn it and finish this piece of work.
A user can create a new book or a new cd and add it to a collection. Not real-world example but reflects the problem I am solving.
A user can click on an Add button, this launches a jQuery dialog which captures some information about a book. This is then saved to the observable array on the model, and the list of books gets updated.
Question:
Why does IE8 only seem to bind the first click and not the second? If I click to add a book the dialog is shown. If I click to add a cd, nothing. I have debugged and the function does not get called.
TIA
As far as I can tell, neither of them should work, and not on any browser (rather than just not working on IE8), because both functions have the same problem: They don't unwrap cart:
self.cart.books.push(/* new book */);
// ^^^^^^
cart is an observable, so you need:
self.cart().books.push(/* new book */);
// ^^
...and similarly for the CDs stuff.
If you fix that, it works (even on IE8):
var Cart = function() {
var self = this;
self.books = ko.observableArray();
self.cds = ko.observableArray();
};
var TheModel = function() {
var self = this;
self.cart = ko.observable(new Cart());
self.showAddBook = function() {
self.cart().books.push({name: "New book " + (+new Date())});
};
self.showAddCD = function() {
self.cart().cds.push({name: "New CD " + (+new Date())});
};
};
ko.applyBindings(new TheModel(), document.body);
<div data-bind="with: cart">
<h1>Books<h1>
<button data-bind="click: $parent.showAddBook">Add</button>
<div data-bind="foreach: books">
<span data-bind="text: name"></span> <!-- book has a name property -->
</div>
<hr/>
<h3>CDs</h3>
<button data-bind="click: $parent.showAddCD">Add</button>
<div data-bind="foreach: cds">
<span data-bind="text: name"></span> <!-- cd has a name property -->
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
Apologies for slightly incorrect example but I had missed one level of nesting. My example is not a true reflection of my complex model and implementation but I worked out the cause of the problem.
My Cart in this example has a selectedItem property (an observable) of type object that has the array of books (observable array) and CDs (observable array).
var Items = function () {
this.books = ko.observableArray();
this.cds = ko.observableArray();
}
var Cart = function() {
this.selectedItem = ko.observable(new Items());
}
var Model = function () {
this.cart = new Cart();
}
I was using the knockout 'with' binding and setting the context to cart.selectedItem
<div data-bind="with: cart.selectedItem"> ... </div>
With this approach I noticed that only the first click (add book) was working. Clicking on Add CD was doing nothing.
I changed the context from cart.selectedItem to cart and set the foreach binding (that displays the list of books and cds) to selectedItem().books and selectedItem().cds and that worked in IE8 and other browser.
If I change the context using the knockout 'with' binding back to cart.selectedItem then only the first click works.
Hope that helps anyone else who encounters this problem.

bindings break when instantiating viewmodel for applyBindings in knockout.js

I must be missing something very simple, but I'm not seeing it.
I had a simple viewmodel:
var ViewModel = function() {
var self = this;
self.categories = ko.observableArray([{id:'one',display:'ONE'},{id:'two',display:'TWO'}]);
self.show_subcategories_for = function(category){
alert(category.id);
};
};
ko.applyBindings(ViewModel);
and html for it:
<div id="categories">
<!-- ko foreach: categories -->
<input type="radio" data-bind="attr: { id: id }, click: show_subcategories_for" name="category" />
<label data-bind="attr: { 'for': id }, text: display"></label>
<!-- /ko -->
</div>
working jsfiddle for it: http://jsfiddle.net/J8VNY/2/
Then I was trying to factor out categories array and wanted to pass it to the view model as a parameter, knockout would error out on "show_subcategories_for is not defined ", which is definitely there.
I changed the viewmodel to following:
var ViewModel = function(cats) {
var self = this;
self.categories = ko.observableArray(cats);
self.show_subcategories_for = function(category){
alert(category.id);
};
};
ko.applyBindings(new ViewModel(
[{id:'one',display:'ONE'},{id:'two',display:'TWO'}]
));
here is jsfiddle with the error: http://jsfiddle.net/J8VNY/1/
Everything appears to be correct yet for some reason instantiating viewmodel and passing it an array as a parameter causes knockout some confusion.
Any insight would be appreciated. Thank you
You have to use the $root keyword:
<input type="radio"
name="category"
data-bind="attr: { id: id }, click: $root.show_subcategories_for" />
See Documentation

Categories

Resources