How to Refresh Knockout ViewModel Using Mapping Plugin - javascript

I have the following Knockout ViewModel:
var EditorViewModel = function () {
var self = this;
self.addData = function (_data) {
ko.mapping.fromJS(_data, {}, self);
};
};
which is used as such:
var viewModel = new EditorViewModel();
viewModel.addData(model);
ko.applyBindings(viewModel);
For info, the data for viewModel.addData() comes from an AJAX call.
The View/ViewModel is populated with no problem the first time round when ko.applyBindings is called. However, if I later pull new data from the server via AJAX and then call either:
viewModel.addData(someNewAjaxData)
or
ko.mapping.fromJS(someNewAjaxData, {}, viewModel);
Then Knockout does not update the view with the new data. Hopefully this is a trivial problem...?!

KnockoutJS Mapping has the ability to specify a key for an item. This lets KnockoutJS know when an item needs to be created versus updated.
var ItemViewModel = function(d){
var self = this;
self.id = ko.observable(d.id);
self.text = ko.observable(d.text);
// We assign it when the object's created to demonstrate the difference between an
// update and a create.
self.lastUpdated = ko.observable(new Date().toUTCString());
}
var EditorViewModel = function(){
var self = this;
self.items = ko.observableArray();
self.addData = function(d){
ko.mapping.fromJS(d, {
'items': {
'create': function(o){
return new ItemViewModel(o.data);
},
'key': function(i){
return ko.utils.unwrapObservable(i.id);
}
}
}, self);
console.info(self);
}
self.addMoreData = function(){
self.addData({
'items': [
{id:1,text:'Foo'},
{id:3,text:'Bar'}
]
});
}
}
var viewModel = new EditorViewModel();
viewModel.addData({
items:[
{id:1,text:'Hello'},
{id:2,text:'World'}
]
});
ko.applyBindings(viewModel);
<link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Hello, world!</h3>
</div>
<ul class="list-group">
<!-- ko foreach:items -->
<li class="list-group-item">
<span class="text-muted" data-bind="text:id"></span>
<span data-bind="text:text"></span>
<span class="text-danger pull-right" data-bind="text:lastUpdated"></span>
</li>
<!-- /ko -->
<li class="list-group-item" data-bind="visible:!items().length">
<span class="text-muted">No items.</span>
</li>
</ul>
<div class="panel-footer">
Update
</div>
</div>
</div>
</div>
</div>
Observe, in the attached snippet, how the first item's lastUpdated value is assigned when it's created, but is left unchanged when it's updated. This is because the mapper sees the ID already exists in items and simply updated the properties. But, for item with the id 3, a new model is created.

All knockout bibdings were subscribed to the first created model. You recreate the bound model on every ajax response. Your problem seems similar that was discussed in this stackoverflow article. The same osbervable should be bound to the markup.
Try this (may be you should support an empty model binding):
// in the start of app
var observableModel = ko.observable();
ko.applyBindings(observableModel);
// on every ajax call
var viewModel = new EditorViewModel();
viewModel.addData(model);
observableModel(viewModel);

Related

How do I set selectedItem and bind to it with knockout

I have a list as follows:
<ul id="blogList" data-bind="foreach: Data">
<li>
<span data-bind="text: Title"> </span>
View
</li>
</ul>
And knockout view model as below:
var ViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
this.currentSelected = ko.observable();
self.viewEntry = function () {
currentSelected = this;
}
};
Setting the currentSelected to this doesn't seem to work because when I try to bind to currentSelected somewhere else I get nothing happening:
<h2 data-bind="text: currentSelected.Title"></h2>
Is that the correct way to bind to currentSelected? The list is working fine but setting the currentSelected and binding to it isn't working.
First let's have a look at your viewEntry method.
self.viewEntry = function () {
currentSelected = this;
}
currentSelected is undefined it should be self.currentSelected additionally since this is an observable you should not set this as var with equal sign but rather treat this as function so it should be self.currentSelected(this)
the other thing is binding inside this part:
<h2 data-bind="text: currentSelected.Title"></h2>
again this is an object so to access its properties you need to use currentSelected as function so it should be
<h2 data-bind="text: currentSelected().Title"></h2>
Here you can find a working sample: https://jsfiddle.net/jz9t5vbo/

Function bound to click action in foreach not being called

I'm having trouble binding my click action to the view model function to remove an item from an array (inside a foreach binding)
I've got the following view model
var FileGroupViewModel = function () {
var self = this;
self.files = ko.observableArray();
self.removeFile = function (item) {
self.files.remove(item);
}
self.fileUpload = function (data, e) {
var file = e.target.files[0];
self.files.push(file);
};
}
var ViewModel = function () {
var self = this;
self.FileGroup = ko.observableArray();
self.FileGroup1 = new FileGroupViewModel();
self.FileGroup2 = new FileGroupViewModel();
self.FileGroup3 = new FileGroupViewModel();
self.uploadFiles = function () {
alert("Uploading");
}
}
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
And my view, which basically lists 3 "groups" of buttons, where a user can select files to upload
Everything below is working as expected, except $parent.removeFile isn't removing the file:
<div class="row files">
<h2>Files 1</h2>
<span class="btn btn-default btn-file">
Browse <input data-bind="event: {change: FileGroup1.fileUpload}" type="file" />
</span>
<br />
<div class="fileList" data-bind="foreach: FileGroup1.files">
<span data-bind="text: name"></span>
Remove
<br />
</div>
</div>
Fiddle at https://jsfiddle.net/alexjamesbrown/aw0798p7/
Am I wrong to do $parent.removeFile - it seems this doesn't get called on click?
This is a cut down working example, not the finished product!
You're misunderstanding $parent. It takes you out one context level. Your foreach uses FileGroup1.files as its index, so you might think that the $parent level would be Filegroup1, but it's not. It's the top-level viewmodel, because that is the context outside the foreach.
So your click binding should be
click: $parent.FileGroup1.removeFile

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.

binding knockout observable arrays to multiple <ul> elements

For a navigation menu, I have two groups of links, each group and link showing up or not dependent on a user's role. The roles are looked up when the link structure is being built and the list of links is built accordingly. The returned JSON gets parsed, put into observable arrays with no problem, but when I actually try and apply the bindings, the binding fails because the observables are blank. Here is the HTML...
<ul id="user-menu" class="menu" data-bind="foreach: areas">
<li>
<a data-bind="attr: { href: areaLink }">
<img data-bind="attr: { src: iconUri }" />
<span data-bind="text: areaName"></span>
</a>
</li>
</ul>
<ul id="admin-menu" class="menu" data-bind="foreach: adminAreas">
<li>
<a data-bind="attr: { href: areaLink }">
<img data-bind="attr: { src: iconUri }" />
<span data-bind="text: areaName"></span>
</a>
</li>
</ul>
Knockout view model in the background...
var navigation = (function() {
function Area() {
var self = this;
self.areaName = ko.observable();
self.areaLink = ko.observable();
self.iconUri = ko.observable();
self.sequenceNo = ko.observable();
self.isAdmin = ko.observable();
self.loadFromVM = function (vm) {
self.areaName(vm.name || '');
self.areaLink(vm.link || '');
self.iconUri(vm.iconUri || '');
self.sequenceNo(vm.sequenceNo || '');
self.isAdmin(vm.isAdmin);
}
}
function viewModel() {
var self = this;
self.areas = ko.observableArray([]);
self.adminAreas = ko.observableArray([]);
self.setup = function () {
var data = {}; // population with basic session data
$.getJSON('....php', { JSON.stringify(data) }, function (results) {
for (var i = 0; i < results.length; i++) {
var area = new Area();
area.loadFromVM(results[i]);
if (area.isAdmin()) {
self.adminAreas().push(area);
} else {
self.areas().push(area);
}
}
});
};
}
var vmInstance;
return {
setup: function () {
vmInstance = new viewModel();
vmInstance.setup();
ko.applyBindings(vmInstance, $('#user-menu')[0]);
ko.applyBindings(vmInstance, $('#admin-menu')[0]);
}
};
})();
And then I bring it together with this in the navigation view file...
navigation.setup();
So after I get my JSON back, parse it, and organize it when I loop through the success function of the $.getJSON method, putting a watch on self.areas() and self.adminAreas() does show that the arrays have the exact information I want them to. But by the time they have to be applied, calling vmInstance.areas().length or vmInstance.adminAreas().length returns zero. Even more oddly, putting in an alert with the length of the arrays right after the $.getJSON call but within the setup() function will cause the alert to fire first, show zeroes, then goes through the get, populates the array, then fires zeroes again.
Not exactly sure what's going on here, but I can't remember seeing this kind of behavior in another project so I'm not quite sure what I'm doing wrong here. Any ideas or advice would be greatly appreciated.
EDIT: Nevermind on the Fiddle. It doesn't really capture my actual error.
adminarea object is not initialized.you made the adminArea variable but instead of this you have used same area variable to set values.
var adminArea = new Area();
adminArea.areaName('test admin area');
adminArea.areaLink('#');
adminArea.iconUri('http://evernote.com/media/img/getting_started/skitch/windows8/win8-checkmark_icon.png');
adminArea.sequenceNo(1);
adminArea.isAdmin(true);
Fiddle Demo

knockoutjs binding to a ko.observableArray of the selected child view model in a main view model

in knockoutjs i have a main view model which has a collection of child view models as in the following
//apply binding on document ready
$( document ).ready(function() {
ko.applyBindings(new MainviewModel);
});
//main view model, contains a child view models ko.observableArray
var MainviewModel = function()
{
// ko.observableArray of subviewmodels
this.subViewModels= ko.observableArray
([
new SubViewModel('A'),
new SubViewModel('B'),
new SubViewModel('C')
]);
//selected subviewmodel
this.selectedSubViewModel = ko.observable();
//set slected subviewmodel
this.selectSubViewModel = function(subviewmodel)
{
this.selectedSubViewModel = selectedSubViewModel(subviewmodel);
}
};
//child view model, contains ko.observableArray of models
var SubViewModel = function(key)
{
this.key = key;
//load items from json based on key value...
//ko.observableArray of subviewmodels
this.items=ko.observableArray([new Model("car","4 wheels"), new Model("plane","can fly")]);
//selected model
this.selectedModel = ko.observable();
//set selected model
this.selectModel = function(item)
{
this.selectedModel = selectedModel(item);
}
};
//model
var Model = function(word,defention)
{
//properties
this.word = word;
this.defention = defention;
};
and in my html i have 2 lists changing selection in the first list should change the source of the second list, however when i change selection in list one nothing happens and when i debug in chrome i get Uncaught ReferenceError: selectedSubViewModel is not defined
<!--main view model list view-->
<div class="list-group" data-bind="foreach: subViewModels">
<a href="#" class="list-group-item" data-bind="css: {active: $parent.selectedSubViewModel() == $data}, click: $parent.selectSubViewModel.bind($parent)">
<h4 class="list-group-item-heading" data-bind="text: key"></h4>
</a>
</div>
<!--selected sub view model list view-->
<div class="list-group" data-bind="foreach: selectedSubViewModel.items">
<a href="#" class="list-group-item" data-bind="css: {active: $parent.selectedModel() == $data}, click: $parent.selectModel.bind($parent)">
<h4 class="list-group-item-heading" data-bind="text: word"></h4>
<p class="list-group-item-text" data-bind="text: defention"></p>
</a>
</div>
Your main problem is that your selectSubViewModel and selectModel are incorectly trying to the set your selectedSubViewModel and selectedModel observables.
The correct syntax is:
this.selectSubViewModel = function(subviewmodel)
{
this.selectedSubViewModel(subviewmodel);
}
this.selectModel = function(item)
{
this.selectedModel(item);
}
See also in the documentation: Reading and writing observables
However it won't make your code to work because when the foreach: selectedSubViewModel.items binding is executed for the first time selectedSubViewModel will be null and KO won't find the selectedSubViewModel.items.
To avoid this you can use the with binding to wrap your foreach. In this case your second list is only rendered if there is something in the selectedSubViewModel and you don't have to write selectedSubViewModel.someproperty it is enough to write foreach: items etc:
<!--selected sub view model list view-->
<!-- ko with: selectedSubViewModel -->
<div class="list-group" data-bind="foreach: items">
<a href="#" class="list-group-item"
data-bind="css: { active: $parent.selectedModel() == $data},
click: $parent.selectModel.bind($parent)">
<h4 class="list-group-item-heading" data-bind="text: word"></h4>
<p class="list-group-item-text" data-bind="text: defention"></p>
</a>
</div>
<!-- /ko -->
Demo JSFiddle.

Categories

Resources