Knockoutjs nested view models - javascript

I'am using Knockoutjs and have a problem getting access to root/parent models data in a submodel. The data I get via Ajax.
The Ajax success creates a new WeekViewModel and this creates a few RowViewModels. And here is my Problem, at this time week is not defined.
After the site is rendered, I can get the Infos over week.
The only solution I've found is pasting the parent and root into the submodel.
But this works not so well cause at the initialsation, the parent is a plain js Obeject. After the site is rendered, and I will paste another rowViewModel from a click event, the parent is a knockout object.
Can anyone give me some suggestions where I'va made a mistake? Or whats a way to get this fixed?!
Here's my code:
$(function() {
var week;
var weekData;
$.when(
$.get('js/dataNew.json', function (res) {
weekData = res;
}),
// another calls ...
).then(function () {
week = new WeekViewModel(weekData);
ko.applyBindings(week, $('#content').get(0));
});
function WeekViewModel(data){
var self = this;
var mapping = {
'Rows': {
create: function(options) {
return new RowViewModel(options.data, self);
}
}
};
this.allDays = allDays;
this.cats = new ko.observableDictionary();
// more code ...
ko.mapping.fromJS(data, mapping, this);
};
function RowViewModel(row, parent){
var self = this;
var mapping = {
'RowDays': {
create: function(options) {
return new DayModel(options.data, self, parent);
}
}
};
if(row){
if(!row.DisplayName) {
// need data from the root here
// parent.cats <---
}
}
// more code ...
ko.mapping.fromJS(row, mapping, this);
}
// another submodel ...
});
Update:
fiddle:
http://jsfiddle.net/LkqTU/23750/
updated fiddle with html:
http://jsfiddle.net/LkqTU/23753/

Related

Returning a knockout view model on pushstate / popstate

I've been trying for some time to save a ko viewmodel to the browser history and return it on a popstate event. Currently no errors are being thrown, but nothing is being changed on the popstate. The basic flow of what I'm trying goes like this:
var storeViewModel = function (){
return ko.toJSON(viewModel);
};
function viewModel() {
var self = this;
self.records = ko.observableArray();
// add an object to the array and save view model
self.addRecord = function () {
self.records.push(new record());
// call the storeViewModel function push viewModel to history
history.pushState(storeViewModel(), "");
}
// set view model to stored view model object on forward / back navigation navigation
window.onpopstate = function (event) {
self = ko.utils.parseJson(event.state);
}
}
ko.applyBindings(new viewModel());
I've read the mozilla documentation for this several times. Everything seems to make sense, but I am having trouble in implementation. Thanks for any help!
You wouldn't save the viewmodel, but the data it contains.
The most convenient way to do that is by employing automatic mapping. Knockout has the [mapping plugin][1] for this; it allows you to easily turn raw data into a working viewmodel, and a working viewmodel back into raw data.
By default the mapping plugin maps all properties of the raw data to observable or observableArray, respectively, but that can be fine-tuned in the mapping definition (see documentation).
This basically works like this:
ko.mapping.fromJS(data, {/* mapping definition */}, self);
and back like this:
ko.mapping.toJS(self);
I'd recommend setting up all your viewmodels so they can bootstrap themselves from raw data:
function Record(data) {
var self = this;
// init
ko.mapping.fromJS(data, Record.mapping, self);
}
Record.mapping = {
// mapping definition for Record objects, kept separately as a constructor
// property to keep it out of the individual Record objects
};
and
function RecordPlayer(data) {
var self = this;
self.records = ko.observableArray();
self.init(data);
self.state = ko.pureComputed(function () {
return ko.mapping.toJS(self);
});
}
RecordPlayer.mapping = {
// mapping rules for ViewModel objects
records: {
create: function (options) {
return new Record(options.data);
}
}
};
RecordPlayer.prototype.init = function (data) {
// extend a default data layout with the actual data
data = ko.utils.extend({
records: []
}, data);
ko.mapping.fromJS(data, ViewModel.mapping, this);
};
RecordPlayer.prototype.addRecord = function () {
this.records.push(new Record());
};
The mapping plugin keeps track of all the properties it mapped in the .fromJS step and only returns those in the .toJS() step. Any other properties, like computeds, will be ignored.
That's also the reason for the ko.utils.extend - to establish the baseline set of properties that you want the mapping plugin to handle.
The state computed now changes every time the state-relevant data changes, due to knockout's built-in dependency tracking.
Now what's left is handling the page load event:
// initialize the viewmodel
var player = new RecordPlayer(/* data e.g. from Ajax */);
// subscribe to VM state changes (except for changes due to popState)
var popStateActive = false;
player.state.subscribe(function (data) {
if (popStateActive) return;
history.pushState(ko.toJSON(data), "");
});
// subscribe to window state changes
player.utils.registerEventHandler(window, "popstate", function (event) {
popStateActive = true;
player.init( ko.utils.parseJson(event.state) );
popStateActive = false;
});
// and run it
ko.applyBindings(player);
You can expand and run the code snippet below to see it in action.
function Record(data) {
var self = this;
// init
ko.mapping.fromJS(data, Record.mapping, self);
}
Record.mapping = {
// mapping definition for Record objects, kept separately as a constructor
// property to keep it out of the individual Record objects
};
function RecordPlayer(data) {
var self = this;
self.records = ko.observableArray();
self.init(data);
self.state = ko.pureComputed(function() {
return ko.mapping.toJS(self);
});
}
RecordPlayer.mapping = {
// mapping rules for RecordPlayer objects
records: {
create: function(options) {
return new Record(options.data);
}
}
};
RecordPlayer.prototype.init = function(data) {
// extend a default data layout with the actual data
data = ko.utils.extend({
records: []
}, data);
ko.mapping.fromJS(data, RecordPlayer.mapping, this);
};
RecordPlayer.prototype.addRecord = function() {
this.records.push(new Record());
};
RecordPlayer.prototype.pushState = function() {
history.pushState(this.state(), "");
};
// initialize the viewmodel
var player = new RecordPlayer( /* optional: data e.g. from Ajax */ );
var popStateActive = false;
// subscribe to VM state changes (except for changes due to popState)
player.state.subscribe(function(data) {
if (popStateActive) return;
history.pushState(ko.toJSON(data), "");
});
// subscribe to window state changes
ko.utils.registerEventHandler(window, "popstate", function(event) {
popStateActive = true;
player.init(ko.utils.parseJson(event.state));
popStateActive = false;
});
// and run it
ko.applyBindings(player);
<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>
<p>Records:
<span data-bind="foreach: records">
<span>(o)</span>
</span>
</p>
<p>There are <span data-bind="text: records().length"></span> records in the player.</p>
<button data-bind="click: addRecord">Add record</button>
<button data-bind="click: function () { history.back(); }">Undo (<code>history.back()<code>)</button>

knockoutjs mapping: adding observables in a tree

I'm using knockoutjs to visualize a tree. I have a initial AJAX-call to load the hole tree. Mapping happens using the knockout-mapping-plugin. Everything is working fine :) thanks for this great piece of software.
My problem is: I want to add additional observables to the model. I can do this using mapping options.
Object:
{ id: ko.observable(), parentID: ko.observable(), description: ko.observable(), children: ko.observableArray() }
Mappingoptions:
var mappingOptions = {
create: function (options) {
console.log("create");
var vm = ko.mapping.fromJS(options.data);
vm.newObservable = ko.observable(true);
return vm;
}
};
Mapping:
ko.mapping.fromJS(response, mappingOptions, self.nodes);
response is the list of objects(nodes). self.nodes is the observableArray() holding the list of objects(nodes)
Every node has the children-observalbeArray containing also nodes(with children)
So nothing special, that's basically how a tree works :)
But this only adds this new observable to the root node. I want this extra observable also in every child and child's child (and so on...).
I tried:
var mappingOptions = {
create: function (options) {
console.log("create");
var vm = ko.mapping.fromJS(options.data);
vm.newObservable = ko.observable(true);
return vm;
},
'children': {
create: function (options) {
console.log("children.create");
options.data.newObservable = ko.observable(true);
return ko.mapping.fromJS(options.data);
}
}
};
I found this somewhere on the internet. It is not working.
Can someone help me?
Thanks!
P.S.: i can't provide a fiddle, because the service seems to be broken right know (or my firewall is blocking it :( )
I would try creating a data model. Let's call it Node:
var Node = function Node(data) {
this.parentID = ko.observable( data.parentID );
this.description = ko.observable( data.description );
this.children = ko.observableArray([]);
data.children.forEach(function(child) {
this.children.push( new Node(child) );
}, this);
}
This makes it easy to create any additional properties on every node you wish.
In your viewmodel:
var Viewmodel = function Viewmodel() {
this.nodes = ko.observableArray([]);
this.getResponse = function getResponse() {
// response is loaded here
response.forEach(function(node) {
this.nodes.push( new Node(node) );
}, this);
};
}
Please note that I'm using the native Array.prototype.forEach here. If you need to support IE < 9, you can replace it with Knockouts ko.utils.arrayForEach instead.

Pager.js: How to lazy load bindings

I am trying to figure out how to use Pager.js in conjunction with Knockout.js to lazy-load a page and bind its contents. I am trying to translate the demo example, but I am not familiar with require.js and am just getting lost.
I have spent several hours trying to reimplement the system using jQuery's getJSON instead of require and define, but the bindings are failing silently. I am having two issues:
The view model is a JSON array, so I don't know what the array is called
The code is not actually doing a getJSON request (nothing in the logs). And is failing silently.
Here is the code:
<div data-bind="page: {id: 'history', title: 'History', withOnShow: $root.getHistory }">
var ViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
self.getHistory = function () {
return function (f) {
$.getJSON("#{HistoryR}", function (data) {
viewModel.history = ko.mapping.fromJS(data, {});
f(viewModel.history);
});
}
}
};
$.getJSON("#{HomeR}", function (data) {
viewModel = new ViewModel(data);
pager.extendWithPage(viewModel);
ko.applyBindings(viewModel);
pager.start();
});
I refactored the code some, to fit in with huocp's answer:
self.getExamHistory = function (f) {
$.getJSON("#{ExamHistoryR}", function (data) {
self.history = ko.mapping.fromJSON(data, {});
f(self.history);
});
}
and the getJSON call is getting triggered (and I see the response in my console), but my viewModel.history is still empty.
You did a wrong wrap of withOnShow callback function.
Remove the wrap, you should be fine :-)
self.getHistory = function (f) {
$.getJSON("#{HistoryR}", function (data) {
self.history = ko.mapping.fromJS(data); // can u try self instead of viewModel
f(self.history);
});
};
The reason the Pager.js demo page with extra wrap, is that it use withOnShow: requireVM('invention'), not withOnShow: requireVM. It uses the return value of requireVM function, not the function itself.

Wiring up Knockout with SignalR (Object doesn't support property or method)

I'm getting the following js error (on the line of code marked ###) when starting my app:
JavaScript runtime error: Object doesn't support property or method 'messages'
I've previously had this working without knockout, but am trying to add in knockout as I feel this will be easier to maintain in the long run. I suspect that as this is my first venture into both SignalR and Knockout, I've done something blindingly stupid, so any pointers please let me know. The mappedMessages data is fully populated, it's just when it tries to set self.messages it has the issue. Knockout 3.0.0, SignalR 1.1.3.
The full javascript code is below:
$(function () {
var messageHub = $.connection.messageHubMini;
function init() { }
function Message(data) {
this.Operator = ko.observable(data.Operator);
this.text = ko.observable(data.Text);
}
function MessagesViewModel() {
// Data
var self = this;
self.messages = ko.observableArray([]); //### message on this line
}
// Add a client-side hub method that the server will call
messageHub.client.updateMessages = function (data) {
var mappedMessages = $.map(data, function (item) { return new Message(item) });
self.messages(mappedMessages);
}
// Start the connection
$.connection.hub.start().done(init);
ko.applyBindings(new MessagesViewModel());
});
Thanks :)
You should use the viewModel object in the SignalR client methods. Currently, you're trying to use a variable called self, but that variable isn't available in that SignalR client method scope. I have updated your code into a version which I believe should solve your problem, doing as few changes as possible.
$(function () {
var messageHub = $.connection.messageHubMini;
function init() { }
function Message(data) {
this.Operator = ko.observable(data.Operator);
this.text = ko.observable(data.Text);
}
function MessagesViewModel() {
// Data
var self = this;
self.messages = ko.observableArray([]); //### message on this line
}
var viewModel = new MessagesViewModel();
// Add a client-side hub method that the server will call
messageHub.client.updateMessages = function (data) {
var mappedMessages = $.map(data, function (item) { return new Message(item) });
viewModel.messages(mappedMessages);
}
// Start the connection
$.connection.hub.start().done(init);
ko.applyBindings(viewModel);
});

knockoutjs save multiple viewmodels from one function?

I have spent the last few days researching the following KnockoutJS issue.
I have a page with 3 viewmodels on. I am using div's with id's to specify the bindings i.e:
ko.applyBindings(new viewModel(datasource), $("#sectionQualifications")[0]);
I am also using RequireJS which has really helped with making my app modular and works well with KnockoutJS of course.
My question relates to having (as mentioned) 3 viewmodels on my page.. none overlap, but each viewmodel has a SAVE function. So a quick look at one of the viewmodel snippets:
function viewModel(data) {
self = this;
self.quals = ko.observableArray(data),
self.addQual = function () {
self.quals.push(new qualification());
},
self.remove = function (item) {
// Remove from the database IF we have an actual record in our viewmodel
if (item.Id !== 0) {
dataservice_qualifications.deleteEntity(item.Id, ko.toJSON(item),
{
success: function (ret) {
common.notifyOK('Qualification removed');
},
error: function (err) {
common.notifyError('Cannot remove that qualification');
console.log('Qualification Remove Error', err);
console.log('Remove error object', this.Id);
}
}
);
}
// Remove from the actual view model
self.quals.remove(item);
}
// Save and move on.. we need to iterate through the qualifications, update any existing rows (ID NOT 0) or
// add new entries (ID IS 0)
self.save = function () {
var saveData = ko.toJS(this.quals);
for (var i in saveData) {
// New qualification entry
if (saveData[i].Id === 0) { // New qualification entry
dataservice_qualifications.postEntity(ko.toJSON(saveData[i]),
{
success: function (ret) {
},
error: function (error) {
common.notifyError('Cannot add qualification ' + saveData[i].qualificationName);
console.log('Qualification add error', error);
}
}
);
} // eof NEW qualification
if (saveData[i].Id > 0) {
dataservice_qualifications.putEntity(saveData[i].Id, ko.toJSON(saveData[i]),
{
success: function (ret) {
},
error: function (error) {
common.notifyError('Cannot update qualification ' + saveData[i].qualificationName);
console.log('UPDATED: ERROR:', error);
}
}
);
} // eof UPDATED qualification
} // eof saveData loop
common.notifyOK('Qualifications updated');
} // eof savenext function
return;
};
So from that above sample, I would have 2 other viewmodels that are similar, which have the SAVE function as above. So of course I want to use say jQuery to click a button and save all 3 viewmodels (i.e. via that SAVE function on each).
Because I am using RequireJS, I have tried exposing a "public" function that in turn tries to internally call the viewModel.save() function as follows in this snippet:
function saveModel() {
viewModel.save();
}
// PUBLIC INTERFACE
return {
saveModel: saveModel,
loadViewModel: koMapData
}
So the theory being I can call the "saveModel" function from wherever which should trigger the save of the viewmodels?
Any help really appreciated. By the way I have gone down the path of trying to create the viewmodel like:
var viewModel = {
save: function() {
blah blah...
}
}
however no real luck in that either? I am sure I am missing something simple, because I would think you could/should be able to trigger a function from a viewmodel externally somehow?
EDIT
Just for reference, the models DO NOT overlap..
Thanks in advance,
David.
You can merge view models in an object like this:
var mainVModel = {
vModel1: { // has save method},
vModel2: { // has save method},
saveAll : function(){
mainVModel.vModel1.save();
mainVModel.vModel2.save();
}
}
ko.applyBindings(new mainVModel());
Actually thanks #XGreen for your suggestion, and I can see that working well, however I am using requirejs, and so my app structure isn't quite a match.
However, I have been successful in the following solution:
First, I created the viewmodel in a slightly different way..
var viewModel = {
quals: ko.observableArray([]),
addQual: function () {
viewModel.quals.push(new qualification());
},
remove: function (item) {
// Do the remove bit..
},
save: function () {
var saveData = ko.toJS(viewModel.quals);
// Do the save stuff..
} // eof savenext function
}; // eof viewModel def
So the viewmodel is defined, and then I had a helper function to access just the SAVE function of the viewmodel:
// Private: Allows external access to save the viewmodel
var viewModelFunctions = {
saveModel: function () {
viewModel.save();
}
}
.. and then finally, because I am using the revealing module pattern within the requirejs structure, I created a public interface function as follows:
function koMapData(incomingData) {
datasource = (incomingData === null) ? [new qualification()] : incomingData;
viewModel.quals = ko.observableArray(ko.toJS(datasource));
ko.applyBindings(viewModel, $("#sectionQualifications")[0]);
}
// PUBLIC INTERFACE
return {
viewModelFunctions: viewModelFunctions,
loadViewModel: koMapData
}
So you can see the last part with the viewModelFunctions, which means in another module (or wherever) I can reference/trigger the SAVE function remotely by:
mymodule.viewModelFunctions.saveModel()
Which also means (in my case because I have 3 viewmodels I need to trigger save for from one event) I can have the flexibility of saving when/how I want to. Obviously it would be good to return any errors too etc up to the calling module, but that is in principle how it works for me.

Categories

Resources