I have the following:
// Child Array is Cards, trying to add computed observable for each child
var CardViewModel = function (data) {
ko.mapping.fromJS(data, {}, this);
this.editing = ko.observable(false);
};
var mapping = {
'cards': { // This never gets hit, UNLESS I remove the 'create' method below
create: function (options) {
debugger;
return new CardViewModel(options.data);
}
},
create: function(options) {
var innerModel = ko.mapping.fromJS(options.data);
innerModel.cardCount = ko.computed(function () {
return innerModel.cards().length;
});
return innerModel;
}
};
var SetViewModel = ko.mapping.fromJS(setData, mapping);
debugger;
ko.applyBindings(SetViewModel);
However I can't get the 'cards' binding to work - that code isn't reached unless I remove the 'create' method. I'm trying to follow the example from the knockout site:
http://knockoutjs.com/documentation/plugins-mapping.html
They do this for the child object definition:
var mapping = {
'children': {
create: function(options) {
return new myChildModel(options.data);
}
}
}
var viewModel = ko.mapping.fromJS(data, mapping);
With the ChildModel defined like this:
var myChildModel = function(data) {
ko.mapping.fromJS(data, {}, this);
this.nameLength = ko.computed(function() {
return this.name().length;
}, this);
}
I've spent the past day on this and cannot for the life of me figure out why this isn't working. Any tips would be awesome.
EDIT: Here's a fiddle of what I'm working with. It's only showing SIDE 1 in the result because "editing" isn't recognized here:
<div data-bind="visible: !$parent.editing()" class="span5 side-study-box">
http://jsfiddle.net/PTSkR/1/
This is the error I get in chrome when I run it:
Uncaught Error: Unable to parse bindings. Message: TypeError: Object
has no method 'editing'; Bindings value: visible: !$parent.editing()
You have overridden the create behavior for your view model. The mapping plugin will not call any of the other handlers for the properties for you. Since you're mapping from within the create method, move your cards handler in there.
var mapping = {
create: function(options) {
var innerModel = ko.mapping.fromJS(options.data, {
'cards': {
create: function (options) {
debugger;
return new CardViewModel(options.data);
}
}
});
innerModel.cardCount = ko.computed(function () {
return innerModel.cards().length;
});
return innerModel;
}
};
updated fiddle
you didnt needed to have parenthesis. I just changed from
!$parent.editing()
to
!$parent.editing
See the updated fiddle here
Related
I am very new to knockout.js, in fact came across this two days ago when I started to realize my UI scripts were getting out of hand.
So, I have several view models, all being populated using the mapping functionality.
I have the models populating OK from the JSON that is being supplied via my MVC controller.
function ChangeViewModel(data) {
var self = this;
changeMapping = {
'CORs': {
create: function (options) {
return new CORViewModel(options.data);
}
}
}
return ko.mapping.fromJS(data, changeMapping, self);
}
function CORViewModel(data) {
var self = this;
corMapping = {
'copy': ['VendorID', 'VendorName', 'ContractID'],
'include': ['CorNo', 'CorName', 'Items'],
'Items': {
create: function (options) {
return new CORItemViewModel(options.data);
}
}
}
self.CorNoName = ko.pureComputed(function () {
if (self.CorVersion() > 1) return self.CorNo() + ":" + self.CorVersion();
return self.CorNo();
});
self.GrandTotal = ko.pureComputed(function () {
var total = 0;
$.each(self.Items(), function () { total += this.Total() })
return total;
});
return ko.mapping.fromJS(data, corMapping, self);
}
function CORItemViewModel(data) {
var self = this;
corItemMapping = {
'copy': ['ChangeItemId', 'VendorCorId', 'VendorName', 'VendorId'],
}
return ko.mapping.fromJS(data, corItemMapping, self);
}
So that's my view models and this is how I load the vm in the $(document).ready function
var vm = new ChangeViewModel(data);
ko.applyBindings(vm);
I have all this displayed in a table with inputs etc and all looks great.
But if I change a Total in the CORItemViewModel the GrandTotal in the parent CORViewModel doesn't update. Can anyone tell me what I may be missing?
I created a fiddle thx to super cools suggestion, and it worked fine...
https://jsfiddle.net/afvh6d6t/14/
code..
So I went back to the original, it turned out to be an issue with a bindingHandler I used ko-money that broke the updating, so I used this bindingHandler..
http://djsharepoint.com/post/2015/01/28/useful-knockout-js-binding-handlers
which sorted the issue.
Thanks to everyone who took the time to post.
How come this line of code doesnt work.
Im using durandal/knockout and i have a structure like this
define(function () {
var vm = function() {
compute: ko.computed(function() {
return _compute(1); // fail
});
var _compute= function(e) {
return e;
}
}
return vm;
});
Basically I am just trying to access the private method _compute - but KO.compute doesnt allow that?
Even if i make it public, I still cant access it.
I trying to implement revealing pattern in this, but still no luck!
var vm = function() {
compute: ko.computed(function() {
return this._compute(1); // still failing
});
this._compute= function(e) {
return e;
}
}
update: so far, only this one works
define(function () {
var vm = function() {
var self = this;
var self._compute= function(e) {
return e;
}
compute: ko.computed(function() {
return this._compute(1); // works
}, self);
}
but like I said, _compute is not meant to be exposed.
Update: actually its another error.
this one now works
define(function () {
var vm = function() {
var self = this;
var _compute= function(e) {
return e;
}
compute: ko.computed(function() {
return _compute(1); // works
});
}
Basically, just need to declare the private function before the ko.computed prop!
Thanks!
Additional Note:
Why does it need to be declared before the computed function? I prefer all my "properties" in the first lines while the functions in the bottom. It is neater i Think.
This syntax does not create a property when in a function:
compute: ko.computed(function() {
return _compute(1); // fail
});
You have to use = instead of :.
Try this
var vm = function() {
var self = this;
var _compute = function(e) {
return e;
}
this.compute = ko.computed(function() {
return _compute(1);
});
}
Also note that this is not how you should use a computed observable. It should contain calls to other observables!
From doc:
What if you’ve got an observable for firstName, and another for
lastName, and you want to display the full name? That’s where computed
observables come in - these are functions that are dependent on one or
more other observables, and will automatically update whenever any of
these dependencies change.
I am trying to get into backbone and have the following which is an attempt at doing an image gallery. I am trying to use render with a model in a collection. I will show the first element of the collection but I would like to add support for simply rerendering with the next element but I don't know how to do this .
I have implemented next and previous on my model like the following:
arc.Item = Backbone.Model.extend({
next: function () {
if (this.collection) {
return this.collection.at(this.collection.indexOf(this) + 1);
}
},previous: function () {
if (this.collection) {
return this.collection.at(this.collection.indexOf(this) - 1);
}
}
});
The problem here (there could be more than the one I am asking about though) is in the loadNext method. How would I get the current location in this collection and to increment it?
arc.ItemsGalleryView = Backbone.View.extend({
el: $('#mig-container'),
events: {'click .next-btn' : 'loadNext',
'click .previous-btn':'loadPrevious' },
template:_.template($('#mig-image-tmp').text()),
initialize: function() {
_.bindAll( this, 'render' );
// render the initial state
var thisModel=this.collection.first();
this.render(thisModel);
},
render: function(xModel) { // <- is it ok to do it this way?
var compiled=this.template(xModel.toJSON());
this.$el.html(compiled);
return this;
},
loadNext: function(){
console.log('i want to load Next');
this.render(this.collection.next()); // <- how would I do this
this.render(this.collection.first().next()); // <- this works by always giving me the second.
// I need to replace first with current
},
loadPrevious: function(){
console.log('i want to load Previous');
}
Or is there a better way to implement this?
thx in advance
edit #1
arc.ItemsGalleryView = Backbone.View.extend({
el: $('#mig-container'),
events: {'click .next-btn' : 'loadNext', 'click .previous-btn':'loadPrevious' },
template:_.template($('#mig-image-tmp').text()),
initialize: function() {
_.bindAll( this, 'render' );
this.render(this.collection.first()); // <- this works correct
},
render: function(xModel) {
console.log(xModel.toJSON());
var compiled=this.template(xModel.toJSON());
this.$el.html(compiled);
return this;
},
loadNext: function(){
console.log('i want to load next');
this.render(this.collection.next()); // <- this doesn't seem to do anything, event is called correctly but doesn't seem to move to next element
},
However if I adjust to this, it will load the 3rd element of the array
loadNext: function(){
console.log('i want to load Previous');
this.render(this.collection.at(2));
},
How would I use this.collection.next() to get this behavior?
thx
What it looks like you're looking for is a way to use the Collection to manipulate the next/prev stuff. What you currently have only puts it on the model. Here's a base Collection I use in my projects:
App.Collection = Backbone.Collection.extend({
constructor: function(models, options) {
var self = this;
var oldInitialize = this.initialize;
this.initialize = function() {
self.on('reset', self.onReset);
oldInitialize.apply(this, arguments);
};
Backbone.Collection.call(this, models, options);
},
onReset: function() {
this.setActive(this.first());
},
setActive: function(active, options) {
var cid = active;
if ( active instanceof Backbone.Model ) {
cid = active.cid;
}
this.each(function(model) {
model.set('current', model.cid === cid, options);
});
},
getActive: function() {
return this.find(function(model) {
return model.get('current');
});
},
next: function() {
return this.at(this.indexOf(this.getActive()) + 1);
},
prev: function() {
return this.at(this.indexOf(this.getActive()) - 1);
}
});
It's probably not perfect, but it works for me. Hopefully it can at least put you on the right track. Here is how I use it:
var someOtherCollection = App.Collection.extend({
model: MyModel
});
kalley's answer is right on, but I will throw in another example.
I would entertain the idea of keeping the current model inside of state model, and work from there. You can store other application information within the state model as well.
Your model declaration would look like the following. Notice I renamed previous to prev. Backbone.Model already has a previous method and we don't want to over-ride it.
var Model = Backbone.Model.extend({
index:function() {
return this.collection.indexOf(this);
},
next:function() {
return this.collection.at(this.index()+1) || this;
},
prev:function() {
return this.collection.at(this.index()-1) || this;
}
});
Have a generic Backbone.Model that holds your selected model:
var state = new Backbone.Model();
In the view you will listen for changes to the state model and render accordingly:
var View = Backbone.View.extend({
el: '#mig-container'
template:_.template($('#mig-image-tmp').html()),
events: {
'click .prev' : 'prev',
'click .next' : 'next'
},
initialize:function() {
this.listenTo(state,'change:selected',this.render);
state.set({selected:this.collection.at(0)});
},
render:function() {
var model = state.get('selected');
this.$el.html(this.template(model.toJSON()));
return this;
},
next:function() {
// get the current model
var model = state.get('selected');
/* Set state with the next model, fires a change event and triggers render.
Unless you are at the last model, then no event will fire.
*/
state.set({selected:model.next()});
},
prev:function() {
var model = state.get('selected');
state.set({selected:model.prev()});
}
});
Here is a demo. I like the state model approach because I'm not storing application-state information within my models.
If you don't like the state model approach, you can always just throw it on the floor:
/ .. code above ../
initialize:function() {
this.model = this.collection.at(0);
this.render();
},
render:function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
next:function() {
this.model = this.model.nxt();
this.render();
},
prev:function() {
this.model = this.model.prev();
this.render();
}
I have binded my json array to knockout by using knockout-mapping plugin
JSON
{
"info":[
{
"Name":"Noob Here",
"Major":"Language",
"Sex":"Male",
"English":"15",
"Japanese":"5",
"Calculus":"0",
"Geometry":"20"
},
{
"Name":"Noob Here",
"Major":"Calculus",
"Sex":"Female",
"English":"0.5",
"Japanese":"40",
"Calculus":"20",
"Geometry":"05"
}
]
}
Binded using knockout-mapping plugin
var data = [];
$.each(data1.info, function (index, element) {
data.push({
English: element.English,
Japanese: element.Japanese,
Calculus: element.Calculus,
Geometry: element.Geometry,
name: element.Name,
major: element.Major,
sex: element.Sex
});
});
dataFunction.prototype = function () {
var getAllItems = function () {
var self = this;
ko.mapping.fromJS(data, {}, self.Items);
};
Now I want to alert the value of English.
I tried alert(this.English()); inside dataFunction.prototype and it doesn't work.
How to alert that value?
JS-Bin code: http://jsbin.com/ipeseq/4/edit
You need to define a proper view model and work from that in your mark-up.
I put together a view model with a custom view model mapping where I map your data into objects I called 'Student' that you can use in your markup. This object I extended with a ko.computed that calculates the total (It is in this object you can read and manipulate your observables).
var Student = function(data) {
var self = this;
ko.mapping.fromJS(data, { }, self);
self.total = ko.computed(function() { // Calculate total here
return self.English() + self.Japanese() + self.Calculus() + self.Geometry();
});
};
var viewModelMapping = { // Map all objects in 'info' to Student objects
'info': {
create: function(options) {
return new Student(options.data);
}
}
};
var ViewModel = function(data) { // Create a view model using the mapping
var self = this;
ko.mapping.fromJS(data,viewModelMapping,self);
}
$(document).ready(function () {
vm = new ViewModel(data);
ko.applyBindings(vm);
});
You can see the resulting JSBin code here
You can read more in the Customizing object construction using “create” and Customizing object updating using “update” sections here
I'm binding data to a page using KnockoutJS, the ViewModel is being populated by an JSON response from an AJAX call using the mapping plugin, like this:
$(function () {
$.getJSON("#Url.Action("Get")",
function(allData) {
viewModel = ko.mapping.fromJS(allData);
viewModel.Brokers.Url = ko.computed(function()
{
return 'BASEURLHERE/' + this.BrokerNum();
});
ko.applyBindings(viewModel);
});
});
The middle part there doesn't work (it works fine without that computed property). "Brokers" is an observable array, and I want to add a computed value to every element in the array called URL. I'm binding that Brokers array to a foreach, and I'd like to use that URL as the href attribute of an anchor. Any ideas?
I've been working through very similar issues and I've found that you can intercept the creation of the Broker objects and insert your own fields using the mapping options parameter:
var data = { "Brokers":[{"BrokerNum": "2"},{"BrokerNum": "10"}] };
var mappingOptions = {
'Brokers': {
create: function(options) {
return (new (function() {
this.Url = ko.computed(function() {
return 'http://BASEURLHERE/' + this.BrokerNum();
}, this);
ko.mapping.fromJS(options.data, {}, this); // continue the std mapping
})());
}
}
};
viewModel = ko.mapping.fromJS(data, mappingOptions);
ko.applyBindings(viewModel);
Here is a fiddle to demonstrate this: http://jsfiddle.net/pwiles/ZP2pg/
Well, if you want Url in each broker, you have to add it to each broker:
$.each(viewModel.Brokers(), function(index, broker){
broker.Url = ko.computed(function(){return 'BASEURLHERE/' + broker.BrokerNum();});
});
I guess BrokerNum is not going to change, so you might as well just calculate Url once:
$.each(viewModel.Brokers(), function(index, broker){
broker.Url = 'BASEURLHERE/' + broker.BrokerNum();
});
You can also add Url property during mapping by providing "create" callback to ko.mapping.fromJS function. See mapping plugin docs for details.
If you only need url to bind to href, just bind the expression in html (within foreach binding):
<a data-bind="attr: {href: 'BASEURLHERE/' + BrokerNum()}">link to broker details</a>
Thanks to Peter Wiles i have very similar solution:
var ViewModel = function (data, ranges) {
var self = this;
this.productList = ko.observableArray();
var productListMapping = {
create: function (options) {
return (new (function () {
//this row above i don't understand...
this.len = ko.computed(function () {
//just test function returning lenght of object name
// and one property of this model
return this.name().length + ' ' + self.cons_slider_1();
}, this);
ko.mapping.fromJS(options.data, {}, this); // continue the std mapping
})());
}
}
this.cons_slider_1 = ko.observable(100);
ko.mapping.fromJS(data, productListMapping, this.productList);
};
Some differences:
I am not mapping to self, but on this.product.
The input json has not parent name like 'Brokers' in above example:
var products = [
{ "id": "pp1", "name": "Blue windy" },
{ "id": "pp1", "name": "Blue windy" }];
So in productMapping i'm typing just 'create:'
But, what i do not understand is the structure of create function. Could somebody explain me why the function returns new function, which has property. Couldn't it be simplified somehow?