I am using Knockout to implement a course list selection tool. I am using the approach below to populate the data (MVC3/Razor), so that when the viewmodel is initially populated, I have no issues working with each KO array (i.e. CourseList, ScheduleList). However, when the initial load from the server returns zero rows, meaning that the viewmodel 'ScheduleList' property is empty, then it's not possible to call any methods such as .push() or .removeAll(). Presumably this means that the observable array was never created since there was nothing to fill it with. When the model is filled, the ScheduleList property is populated with a List. What is the best way to instantiate it when the MVC action returns it as empty? There is a jsFiddle that seems to address it, but when I try to use the 'create' option, it renders my entire model blank. I am not sure what the syntax is of the 'create' option. The jsFiddle is here: http://jsfiddle.net/rniemeyer/WQGVC/
// Get the data from the server
var DataFromServer = #Html.Raw(Json.Encode(Model));
// Data property in viewmodel
var data = {
"CourseList": DataFromServer.CourseList ,
"ScheduleList": DataFromServer.ScheduleList
};
$(function() {
// Populate Data property
viewModel.Data = ko.mapping.fromJS(data);
// ko.applyBindings(viewModel, mappingOptions);
ko.applyBindings(viewModel);
});
When the initial page load does not populate ScheduleList, then the following code throws an error. If the initial page load contained data, then you could call .removeAll() and .push() etc.
var oneA= 'abc';
// push not working
this.Data.ScheduleList.push( oneA );
Set up your mapping parameters to make it so on creation, you give it a certain structure. Then it will do the updates for you.
What is most likely happening is that your DataFromServer doesn't actually contain a ScheduleList property at all. So when it is mapped, a corresponding property is never made. The mapper will only map existing properties to observables.
You need to set in your create options for the view model to add empty arrays when either array is not set. That way, your view model will end up with corresponding observable arrays in place.
By ensuring that CourseList or ScheduleList is an array, the mapped view model will map them as observableArray objects so your code will work as you expected.
var DataFromServer = {
'CourseList': [1,2,3]
//, 'ScheduleList': []
};
var dataMappingOptions = {
'create': function (options) {
var data = options.data;
data.CourseList = data.CourseList || [];
data.ScheduleList = data.ScheduleList || [];
return ko.mapping.fromJS(data);
}
};
viewModel.Data = ko.mapping.fromJS(DataFromServer, dataMappingOptions);
var data = {
CourseList: DataFromServer.CourseList || ko.observableArray([]) ,
ScheduleList: DataFromServer.ScheduleList || ko.observableArray([])
};
Related
I Have web form which use knockout and i have to implement a new feature to save form as a draft to db and later load again to modify or submit.
Is there any feature on knockout framework to serialize viewmodel to any other form(like json) that i could save to db. then later load it and populate my view easily.
I know that i can save viewmodel as a json to db and then later i can load it and fill each property on view model like below. but im looking for a feature like serialize and later populate whole viewmodel at once using it.I have lot of properties and i don't want to fill each property by writing a code line as below.
var someJSON = /* fetched the saved viewmodel as a json */;
var parsed = JSON.parse(someJSON);
// Update view model properties
viewModel.firstName(parsed.firstName);
viewModel.pets(parsed.pets);
Use the mapping plugin and replace your code with that one:
var someJSON = /* fetched the saved viewmodel as a json */;
var parsed = JSON.parse(someJSON);
// Update view model properties
viewModel = ko.mapping.fromJS(data);
You can use the mapping plugin as other questions have mentioned here, but it definitely isn't perfect.
Most notably, if you have object properties, they won't be converted into observables.
var pojo = {
someStringProperty: 'lol',
someObjectProperty: { }
};
var vm = ko.mapping.fromJS(pojo);
if(!ko.isObservable(vm.someObjectProperty)) console.log('FAIL!');
I've looked into forking, but it's kinda not worth it. I just post-process the object graph looking for properties that aren't observable and that contain an object and convert them into observable properties.
I am using a rails server that returns this JSON object when going to the '/todos' route.
[{"id":1,"description":"yo this is my todo","done":false,"user_id":null,"created_at":"2015-03-19T00:26:01.808Z","updated_at":"2015-03-19T00:26:01.808Z"},{"id":2,"description":"Shaurya is awesome","done":false,"user_id":null,"created_at":"2015-03-19T00:40:48.458Z","updated_at":"2015-03-19T00:40:48.458Z"},{"id":3,"description":"your car needs to be cleaned","done":false,"user_id":null,"created_at":"2015-03-19T00:41:08.527Z","updated_at":"2015-03-19T00:41:08.527Z"}]
I am using this code for my collection.
var app = app || {};
var TodoList = Backbone.Collection.extend({
model: app.Todo,
url: '/todos'
});
app.Todos = new TodoList();
However, when trying to fetch the data it states that the object is undefined. I originally thought that my function wasn't parsing the JSON correctly. However, that doesn't look to be the case. I created a parse function with a debugger in it to look at the response. In gives back, an array with three objects.
Here what happens when I try testing the fetch().
var todos = app.Todos.fetch()
todos.length // returns undefined
todos.get(1) // TypeError: undefined is not a function
The todos collection doesn't automatically populate the function get() in console. I am running out of ideas of what can be the problem. Please help. Thanks!
Fetch is a ayncronous, you need to listen to the add event:
var todos = app.Todos.fetch()
todos.on('add', function(model){
console.log(todos.length);
});
If you pass the parameter reset, you could listen for the would new models:
var todos = app.Todos.fetch({reset: true})
todos.on('reset', function(model){
console.log(todos.length);
});
You could also read here.
There are two problems:
Fetch is asynchronous; we don't know exactly when we'll have a result, but we do know that it won't be there when you are calling todos.length.
Fetch sets the collection's contents when it receives a response; calling app.Todos.fetch() will result in app.Todos containing whatever models were fetched by the request. Its return value is not useful for inspecting the collection, so var todos = app.Todos.fetch() won't give you what you want in any case.
If you want to inspect what you receive from the server, your best option is to set a success callback:
app.Todos.fetch({
success: function (collection, response, options) {
console.log(collection);
}
});
Let us say that I have the following simplified and generic view model, which uses KnockoutJS and the Knockout Validation library to create/manipulate and validate observables, respectively.
// view declared
// KnockoutJS loaded
// KnockoutValidation loaded
var ChildNodePropertiesVM = function(properties) {
var self = this;
/* data within properties object are assigned to
observables within VM */
};
var ChildNode = function() {
var self = this;
var options = // data from ajax request sent to web service
// list of POJOs
this.availableParentNodes = ko.observableArray();
// a specific POJO from the list above, selected from an HTML select element
// Knockout Validation ensures that a value is present (required)
this.associatedParentNode = ko.observable().extend({
required: {
params: true,
message: "Please choose a parent"
}
});
// a view model, declared above, constructed with value from options
this.childProperties = new ChildNodePropertiesVM(options.childProps);
/* Many more model-members follow, and may be any of the above types
(observable, observableArray, view model, etc) */
};
// apply bindings to view here (in this case, ko.applyBindingsWithValidation)
What I am interested in is, if I want to create a "reset" procedure to clear a majority of the members of a view model (of which there may be many), and keep a minority of the fields as they were, what is the most maintainable way in which I could do so? To be more specific, consider the availableParentNodes object to be one of the observables that I would like to keep, while associatedParentNode must be cleared (as in, self.associatedParentNode(undefined)), and half of the (undeclared) model-members of childProperties must be set to null, as part of the reset process.
SO and search engine queries either suggest that each observable/variable be cleared manually, or that a new view model (in this case, the ChildNode view model) simply be created to replace the old one. Since I have specific fields that I would like to keep, the latter is not an option, and since there are so many observables in these models, the former is unmaintainable.
An option would be to create a generic reset function, with a whitelist parameter for the observables you would like to keep. Here's an example (polyfill indexOf if you need to support outdated browsers):
var reset = function ( obj, whitelist ) {
for ( var prop in obj ) {
if ( obj.hasOwnProperty( prop ) && ko.isObservable( obj[ prop ] ) && whitelist.indexOf( prop ) === -1 ) {
obj[ prop ]( undefined );
}
}
};
Then you can curry this function with your whitelist on your individual models.
SomeModel.prototype.reset = function () {
reset( this, [ 'something', 'somethingElse' ] );
};
The above would set every observable except the ones named something and somethingElse in your model to undefined.
Here's a JSFiddle example.
this could be done in a simple way by creating a function
self.Reset = function(){
self.someobservable(null)
self.someobservbleArray([])
}
And now simply call it
self.Reset()
The values will be reset.
Currently i have a call to server that returns two objects and one of the objects i use to set a collection. (i poll this every 10 seconds, and soon will be using socket.io for this).
I am noticing that my models initialize function is called every time i use set collection with object. I thought the set was smart and only added/changed attr or removed models and for ones unchanged just did nothing.
// All Orders Collection
var AllOrders = Backbone.Collection.extend({
model: Order,
url: '/venues/orders'
});
var Order = Backbone.DeepModel.extend({
idAttribute: 'orderid',
url: '/venues/orders',
initialize : function(){
// this is called everytime i use set even if model is in collection
// do stuff
}
})
*****************
app.allOrders.set( app.getOrders.get('orders') );
If you look at the backbone source,
When merging models into a collection the _prepareModel method is called before merging which creates a new model from the passed in attributes.
Here is the set code,
if (!(model = this._prepareModel(attrs = models[i], options))) continue;
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(model)) {
if (remove) modelMap[existing.cid] = true;
if (merge) {
attrs = attrs === model ? model.attributes : options._attrs;
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
}
So what happens is,
A new model is created from your passed in attributes
The collection is queried for an existing model with a same id as the new one.
If the merge options is true it pushes the new attributes to the existing model.
This depends on how you write your client and services. By default the GET /venues/orders returns the whole order list. If you want to fetch the changes only you have to write your custom sync for the order collection which keeps the current orders, and fetches the changes only from the server (e.g. you send the current ids, or you send the timestamp of the last call). Btw. the initialize will be called by every fetched model because they are created from json and collection.set cannot compare json objects with backbone models, it can only compare backbone models with backbone models.
Possible solutions:
Override set to compare json with backbone models.
Override your app.getOrders.get('orders') & server side code to pull down only the changes, or manually remove the already existing objects from the pullled json.
Move your code from initialize to another place for example to collection.add event handler.
I'm using ember.js RC1 + ember-data rev 11 (but I also need some plain ajax for configuration like models). I want to loop over a simple objects list and display the records (note -here I create just a basic array)
The content I have bound has the following custom find method defined
App.Foo = DS.Model.extend({
name: DS.attr('string')
}).reopenClass({
records: [],
all: function() {
return this.records;
},
find: function() {
var self = this;
$.getJSON('/api/foo/', function(response) {
response.forEach(function(data) {
//say I want to kill everything in the array here for some strange reason...
self.records = [];
//the template still shows the record ... not an empty list ?
}, this);
});
return this.records;
}
});
My other model uses this directly
App.Related = DS.Model.extend({
listings: function() {
return App.Foo.find();
}.property()
});
Now inside my template
{{#each foo in related.listings}}
{{foo.name}}<br />
{{/each}}
The list loads up with whatever I put in the array by default (say I add a simple object using createRecord like so)
add: function(record) {
this.records.addObject(App.Foo.createRecord(record));
},
and when the template is rendered I see anything listed here... but as I put in the comments above, if I decide to remove records or null out the list that is bound it doesn't seem to reflect this in any way.
Is it possible to bind a simple array as I have and yet remove items from it using something basic such as splice? or even a drastic self.records = []; ?
self.records.splice(i, 1);
Even when I query the client manually after the splice or empty work it returns 0
console.log(App.Foo.all().get('length'));
Initially I see records, but then I see they are gone (yet the html doesn't change)
I understood your question this way, that the following remark is the point your are struggling with:
response.forEach(function(data) {
//say I want to kill everything in the array here for some strange reason...
self.records = [];
//the template still shows the record ... not an empty list ?
}, this);
You are wondering, why your template is showing no empty list? It's because you did not tell Ember when to update the template. You can tell Ember this way:
App.Related = DS.Model.extend({
listings: function() {
return App.Foo.find();
}.property("App.Foo.records.#each")
});
Now Ember knows, whenever something is added or removed from your array, it should update the listings property of your model. And therefore it knows that your view needs rerendering.
One additional remark to the orignal question regarding "simple javascript arrays". When you use Ember, you actually do not instantiate simple js arrays. When you declare:
var a = []; // is the same as -> var a = Ember.A();
Ember does some magic and wraps in an enhanced ember version of an array (Ember.NativeArray), which enables you to use such property dependency declarations mentioned above. This enables Ember to use ArrayObservers on those arrays, although they may feel like a plain JS Array.
You need to use the set method when you modify properties and get when you return them, or else Ember won't be able to do its magic and update the template.
In your case, there is an additional problem, which is that in find(), you return a reference to records before your asynchronous getJSON call replaces it with a new empty array. The calling method will never see the new array of records. You probably want to use clear() instead.
Your model should look something like this:
App.Foo = DS.Model.extend({
name: DS.attr('string')
}).reopenClass({
records: [],
all: function() {
// can't use 'this.get(...)' within a class method
return Ember.get(this, 'records');
},
findAll: function() {
var records = Ember.get(this, 'records');
$.getJSON('/api/foo/', function(response) {
records.clear();
// in this case my json has a 'foos' root
response.foos.forEach(function(json) {
this.add(json);
}, this);
}, this);
// this gets updated asynchronously
return records;
},
add: function(json) {
// in order to access the store within a
// class method, I cached it at App.store
var store = App.get('store');
store.load(App.Foo, json);
var records = Ember.get(this, 'records');
records.addObject(App.Foo.find(json.id));
}
});
Note that the addObject() method respects observers, so the template updates as expected. removeObject() is the corresponding binding-aware method to remove an element.
Here's a working jsfiddle.