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.
Related
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>
I have a Mission() object which has an observableArray parts of Part()s objects:
function Mission(mission) {
koMapping.fromJS(mission, {
'parts': {
create: function(options) {
return new Part(options.data);
}
}
}, this);
/* PARTS */
this.parts = ko.observableArray([]);
}
function Part(part) {
koMapping.fromJS(part, {
include: ['a bunch of properties']
}, this);
var p = this;
// Computed properties and such like here
}
My Mission() object is stored as self.mission = ko.observable(new Mission()) on my global ViewModel.
I also have an addPart function where I need some help:
addPart = function() {
// How do I push a new Part() object to the parts array on the mission here?
}
What I'd like to know is how, using knockout's mapping plugin, is to add a new Part to each mission, preferably automatically with little manual configuration. Right now, I've tried a bunch of things which did not work or did not pull across the correct properties. What does not work:
addPart = function(partToAdd) {
self.mission().parts.push(new Part(partToAdd));
}
addPart = function() {
koMapping.toJS(partToAdd, {}, self.mission);
}
addPart = function() {
koMapping.toJS(partToAdd, {}, self.mission().parts);
}
So how am I meant to do so and have it map automatically?
Addendum: it's worth noting I'm using koMapping instead of ko.mapping here because I've also employed requireJS...
You can do that like this:
addPart = function(partToAdd) {
self.mission().parts(ko.utils.arrayMap(partToAdd, function (part) {
return new Part(part);
});
}
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/
Working on creating a dirt simply MVC framework for one of my own projects. Rather than using one that is public, I decided to create one since my needs are very unusual.
I've got my structure down for the Controllers and Views, however, I'm having some issues creating my model structure.
This is what I have for my model structure:
model.models = function(args){
init: function(){
this.on_init();
},
on_init: args.on_init || noop,
data: args.data || {},
};
So then, I would call this as a basic formula for all of the models I want to create. For example, I want to create employees, notifications and some other models using this as a basic blueprint, then make some basic adjustments.
I call:
model.employees = new model.models({
on_init: function(){
//something specific
},
data: {
//defaults
}
});
And we're all good up to this point, but here is where I'm having troubles. Now, when I want to create my end result, the model, I cannot create a new object from an object.. it must be a function.
The only thing I can think of is creating a return function for the second method, but that renders some issues in itself. I have done some research looking at other MVC code, but I was unable to wrap my head around it.
Any help would be very much appreciated!
is this what you want ?
model.models = function(args){
var noop = function(){};
var o = {};
var init = args.on_init || noop;
var data = args.data || {};
init();
//handle other initialization
//o.a = xx;
//o.b = xx;
//o.c = data.xxx;
//....
return o;
}
then you can use the new, and it can't appear syntax error
Did a lot of fiddling, came up with this:
var blueprint = function(args){
return {
data: args.data,
on_init: args.on_init,
create: function(args){
this.on_init();
return {
data: this.data,
whatever: function(){
console.log(args);
}
};
}
};
};
var notifs = new blueprint({
on_init: function(){
console.log('init');
},
data: {
test: 'test'
}
});
var res = notifs.create('test');
console.log(blueprint);
console.log(notifs);
console.log(res);
It comes out with a main function that works, the notifs function is customizable for each individual object type, then calling the create method will create the end method.
Boom!
reading this exchange in stackoverflow titled "How to map to an Array coming from server object using Knockout Mapping plugin in templates?" (sorry stackoverflow is putting me limitations on the number of links of the post)
i tried to play making use of the answer (the jsFiddle: http://jsfiddle.net/ueGAA/1)
so the exercise was to make the todo of the knockoutjs Tutorial at learn.knockoutjs.com named "Loading and saving data" but using knockout mapping.
the problem is with the kind of of viewmodel declaration of the answer, which I like, here transposed to the todo:
var viewModel =
{
tasks : ko.mapping.fromJS(data),
newTaskText: ko.observable(),
incompleteTasks: ko.computed(function() {
return ko.utils.arrayFilter(this.tasks(), function(task) { return !task.isDone() });
}),
// Operations
addTask: function() {
alert('Entering add task, count:' + this.tasks().length);
this.tasks.push(new Task({ title: this.newTaskText() }));
this.newTaskText("");
},
removeTask: function(task) { this.tasks.remove(task) }
}
The point is here: inside the declaration of ko.computed(), this is referencing window.
Normal indeed.
The correct behavior can be obtained if I declare the ko.computed() after the vewmodel variable.
this way:
viewModel.incompleteTasks=ko.computed(function() {
return ko.utils.arrayFilter(viewModel.tasks(), function(task) { return !task.isDone() });
});
I don't like it because it references statically the object viewModel in the anonymous function.
the question is: how to declare in an elegant way the incompleteTasks directly in the viewmodel declaration?
the jsFiddle is here http://jsfiddle.net/Yqg8e/
thanks
Switch from using an object literal to a constructor function for your ViewModel.
function ViewModel() {
var self = this;
self.tasks = ko.mapping.fromJS(data);
self.newTaskText = ko.observable();
self.incompleteTasks = ko.computed(function() {
return ko.utils.arrayFilter(self.tasks(), function(task) {
return !task.isDone()
});
});
self.addTask = function() {
alert('Entering add task, count:' + self.tasks().length);
self.tasks.push(new Task({ title: self.newTaskText() }));
self.newTaskText("");
};
self.removeTask = function(task) { self.tasks.remove(task) }
}
ko.applyBindings(new ViewModel());
Note also the use of var self = this; which allows access to the this context even within inner anonymous functions.
This technique is described in the Knockout docs, in the section on Computed Observables (skip down to the section titled Managing 'this').
Here's an updated fiddle: http://jsfiddle.net/Yqg8e/1/