best practice for data manipulation + validation - backbone.js

I need to store some data in my model which are dates+times in a Unix standard representation (ms from 1970), but what I do is call set by passing a string like "Tue Jul 30 2013 12:25:43 GMT+0200 (CEST)". The string is what I can obtain from my view and I don't really want to convert it in my view, because I think it's a model's duty to do it. By this way the functionalities are well organized and neat. So:
I put this in my model:
initialize: function() {
var self= this;
//conversion of the string date into milliseconds
this.on("change:start_time", function(model, start_time) {
if(start_time !== null){
var data= new Date(start_time);
self.attributes.start_time= data.valueOf();
}
});
...
and it works like a charm. The problems is when you want to set the start_time property and validate it at the same time from one of my views:
this.model.set({
start_time: startDatePicker.getLocalDate()
}, {validate : true});
what's the matter here? Validation is performed before the event handler I set in the initialization is triggered. I found a workaround: moving the conversion into a new function and call it both in the .on and in the validation. But I think something neater is possible. Any suggestion?

The value start_time will be validated before a change event is fired. Also, it is discouraged to set model.attributes directly, instead use set and pass {silent:true} if you don't want a change event fired.
I suggest one of the following methods and get rid of your event handler.
Do the conversion before calling set:
var dt = new Date(startDatePicker.getLocalDate()).valueOf();
this.model.set({
start_time: dt
}, {validate : true});
If you insist that your model does the conversion, another option is over-riding the set method in your model:
var Model = Backbone.Model.extend({
set:function(key,val,options) {
var attrs;
// from Backbone source
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
if (attrs['start_time'])
attrs['start_time'] = new Date(attrs['start_name']).valueOf();
Backbone.Model.prototype.set.call(this,attrs,options);
}
// your existing code
});
Personally, I would choose the first method.

Related

backbone.js set in model initialize not effecting models in collection

While performing a fetch() on my backbone collection, and instantiating models as children of that collection, I want to add one more piece of information to each model.
I thought that I could do this using set in the model initialize. (My assumption is that fetch() is instantiating a new model for each object passed into it. And therefore as each initialize occurs the extra piece of data would be set.
To illustrate my problem I've pasted in four snippets, first from my collection class. Second the initialize function in my model class. Third, two functions that I use in the initialize function to get the needed information from the flickr api. Fourth, and finally, the app.js which performs the fetch().
First the collection class:
var ArmorApp = ArmorApp || {};
ArmorApp.ArmorCollection = Backbone.Collection.extend({
model: ArmorApp.singleArmor,
url: "https://spreadsheets.google.com/feeds/list/1SjHIBLTFb1XrlrpHxZ4SLE9lEJf4NyDVnKnbVejlL4w/1/public/values?alt=json",
//comparator: "Century",
parse: function(data){
var armorarray = [];
var entryarray = data.feed.entry;
for (var x in entryarray){
armorarray.push({"id": entryarray[x].gsx$id.$t,
"Filename": entryarray[x].gsx$filename.$t,
"Century": entryarray[x].gsx$century.$t,
"Date": entryarray[x].gsx$date.$t,
"Country": entryarray[x].gsx$country.$t,
"City": entryarray[x].gsx$city.$t,
"Type": entryarray[x].gsx$type.$t,
"Maker": entryarray[x].gsx$maker.$t,
"Recepient": entryarray[x].gsx$recipient.$t,
"Flickrid": entryarray[x].gsx$flickrid.$t,
"FlickrUrl": "", //entryarray[x].gsx$flickrurl.$t,
"FlickrUrlBig": ""//entryarray[x].gsx$flickrurlbig.$t,
});
}
return armorarray;
}
});
Second, the initialization in my model.
initialize: function(){
//console.log("A model instance named " + this.get("Filename"));
item = this;
var flickrapi = "https://api.flickr.com/services/rest/?&method=flickr.photos.getSizes&api_key=<my_apikey>&photo_id=" + this.get("Flickrid") + "&format=json&jsoncallback=?";
sources = getFlickrSources(flickrapi);
sources.then(function(data){
sourceArray = parseFlickrResponse(data);
FlickrSmall = sourceArray[0].FlickrSmall;
console.log (FlickrSmall);
item.set("FlickrUrl", FlickrSmall);
console.log(item);
});
Notice here how I'm getting the "Flickrid" and using to get one more piece of information and then trying to add it back into the model with item.set("FlickrUrl", FlickerSmall);
console.log confirms that the property "FlickrUrl" has been set to the desired value.
Third, these are the functions my model uses to get the information it needs for the flicker api.
var getFlickrSources = function(flickrapi){
flickrResponse = $.ajax({
url: flickrapi,
// The name of the callback parameter, as specified by the YQL service
jsonp: "callback",
// Tell jQuery we're expecting JSONP
dataType: "jsonp"})
return flickrResponse;
}
var parseFlickrResponse = function(data){
flickrSourceArray = []
if (data.stat == "ok"){
sizeArray = data.sizes.size;
for (var y in sizeArray){
if (sizeArray[y].label == "Small"){
flickrSourceArray.push({"FlickrSmall": sizeArray[y].source});
}
else if (sizeArray[y].label == "Large"){
flickrSourceArray.push({"FlickrLarge": sizeArray[y].source});
}
}
}
return flickrSourceArray
}
But, fourth, when I try to perform the fetch and render the collection, I only get objects in my collection without the FlickrUrl property set.
//create an array of models and then pass them in collection creation method
var armorGroup = new ArmorApp.ArmorCollection();
armorGroup.fetch().then(function(){
console.log(armorGroup.toJSON());
var armorGroupView = new ArmorApp.allArmorView({collection: armorGroup});
$("#allArmor").html(armorGroupView.render().el);
});
var armorRouter = new ArmorApp.Router();
Backbone.history.start();
The console.log in this last snippet prints out all the objects/models supposedly instantiated through the fetch. But none of them include the extra property that should have been set during the initialization.
Any ideas what is happening?
What is this function ? getFlickrSources(flickrapi)
Why are you using this.get in the initialize function. Honestly it looks over-complicated for what you are trying to do.
If you want to set some parameter when you instantiate your model then do this var model = new Model({ param:"someparam", url:"someurl",wtv:"somewtv"});
If the point is to update your model just write an update function in your model something like update: function (newparam) { this.set;... etc and call it when you need it.
If I read you well you just want to set some params when your model is instantiated, so just use what I specified above. Here is some more doc : http://backbonejs.org/#Model-constructor
I hope it helps.
edit:
Put your call outside your model, you shouldn't (imo) make call inside your model this way it seems kinda dirty.
Sources.then(function(flickrdata) {
var mymodel = new Model({flicker:flickrdata.wtv});
});
It would be cleaner in my opinion.

Nested backbone model results in infinite recursion when saving

This problem just seemed to appear while I updated to Backbone 1.1. I have a nested Backbone model:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""},
parse: function (response) {
response.name = response.set_id;
response.problems = new ProblemList(response.problems);
return response;
}
});
var ProblemList = Backbone.Collection.extend({
model: Problem
});
I initially load in a ProblemSetList, which is a collection of ProblemSet models in my page. Any changes to the open_date or due_date fields of any ProblemSet, first go to the server and update that property, then returns. This fires another change event on the ProblemSet.
It appears that all subsequent returns from the server fires another change event and the changed attribute is the "problems" attribute. This results in infinite recursive calls.
The problem appears to come from the part of set method of Backbone.Model (code listed here from line 339)
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
The comparison on the problems attribute returns false from _.isEqual() and therefore fires a change event.
My question is: is this the right way to do a nested Backbone model? I had something similar working in Backbone 1.1. Other thoughts about how to proceed to avoid this issue?
You reinstantiate your problems attribute each time your model.fetch completes, the objects are different and thus trigger a new cycle.
What I usually do to handle nested models:
use a model property outside of the attributes handled by Backbone,
instantiate it in the initialize function,
set or reset this object in the parent parse function and return a response omitting the set data
Something like this:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""
},
initialize: function (opts) {
var pbs = (opts && opts.problems) ? opts.problems : [];
this.problems = new ProblemList(pbs);
},
parse: function (response) {
response.name = response.set_id;
if (response.problems)
this.problems.set(response.problems);
return _.omit(response, 'problems');
}
});
parse gets called on fetch and save (according to backbone documentation), this might cause your infinite loop. I don't think that the parse function is the right place to create the new ProblemsList sub-collection, do it in the initialize function of your model instead.

listening every time a model attribute sets

I have a backbone model like -
ModelA = Backbone.Model.extend({
this.set("prop1",true);
})
and View like -
ViewA = Backbone.View.extend({
initialize : function(){
this.listenTo(this.model,"change:prop1",this.changeProp1)l;
this.model.set("prop1",true);
},
changeProp1 : function(){
// callback doesn't call because I'm setting the same value
}
});
var model1 = new ModelA();
var view1 = new ViewA({model:model1});
Here the callback changeProp1 triggers whenever prop1 changes from true -> false -> true .
But I want to listen everytime whenever I'm setting the same value or different value.
I'd say it's best to leave the change event alone, and implement a new set event (or whatever you want to call it). After all, you want to be notified about things that aren't strictly 'changes'.
You could implement your own version of set() in your model which fires a custom 'set' event and then calls backbone's usual set method afterwards.
var MyModel = Backbone.Model.extend({
set: function(key, val, options) {
// Deal with single name/value or object being passed in
var changes;
if (typeof key === 'object') {
changes = key;
options = val;
} else {
(changes = {})[key] = val;
}
options || (options = {});
// Trigger 'set' event on each property passed in
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('set:' + changes[i], this, this.attributes[changes[i]], options);
}
// Call the usual backbone 'set' method
Backbone.Model.prototype.set.apply(this, arguments);
}
});
and then listen for your new event instead of (or as well as) 'change', where appropriate:
ViewA = Backbone.View.extend({
initialize : function(){
this.listenTo(this.model,"set:prop1",this.changeProp1)l;
this.model.set("prop1",true);
},
However, most of this code is just lifted from Backbone's default set method, and doesn't deal with some other issues such as some option flags and nested events. If you wanted to change the Backbone source itself, the line you want to look for is:
if (!_.isEqual(current[attr], val)) changes.push(attr);
(line 347 in version 1.0.0) and try removing that if clause.
(Code above isn't tested, sorry for any syntax errors)
For implementing above , you have to make changes in change function in backbone.js . Change checks whether the value of the property changed if yes than only it calls the binded function.

Truly change a model attribute silently in Backbone.js?

Is there a way I can change attributes on a model without ever firing a change event? If you pass {"silent":true} right now, the next time an attribute is changed, the silent change event will be triggered. Can I change an attribute safely without the change event ever being triggered?
from change, Backbone 0.9.2:
// Silent changes become pending changes.
for (var attr in this._silent) this._pending[attr] = true;
// Silent changes are triggered.
var changes = _.extend({}, options.changes, this._silent);
this._silent = {};
for (var attr in changes) {
this.trigger('change:' + attr, this, this.get(attr), options);
You can change model attributes directly using model.attributes['xyz'] = 123.
I think the cleanest way if you really want to default to silent (but still be able to do silent:false) sets would be to override set. This should do it:
var SilentModel = Backbone.Model.extend({
set: function(attrs, options) {
options = options || {};
if (!('silent' in options)) {
options.silent = true;
}
return Backbone.Model.prototype.set.call(this, attrs, options);
}
});
item.set(
{
sum: sum
,income: income
},
{silent: true}
);
since backbone 0.9.10

How to rollback backbone.js model changes?

I have a "Cancel" button on my page which should reverts all the changes I made back to the state it was loaded from server..
I guess I need to store an initial state of Backbonejs model and restore a current (changed) state back to initial.
What is the best way to achieve that?
Thank you
FWIW - i wrote a plugin to handle this automatically, specifically with the idea of "cancel" buttons in mind: http://github.com/derickbailey/backbone.memento
model.previousAttributes() returns all of the previous attributes, while model.changedAttributes() returns all the changed attributes, but with their new values (or false if nothing has changed). So you could combine them to write a cancelChanges method in your prototype :
var MyModel = Backbone.Model.extend({
cancelChanges: function() {
var changed = this.changedAttributes();
if(!changed)
return;
var keys = _.keys(changed);
var prev = _.pick(this.previousAttributes(), keys);
this.set(prev, {silent: true}); // "silent" is optional; prevents change event
},
});
I dont believe there's a single method call for returning a model to its unedited state.. but the unedited values are available individually through model.previous(attribute) and collectively via model.previousAttributes.
Here is what I came up with:
var RollbackEnabledModel = Backbone.Model.extend({
initialize: function() {
this._initAttributes = _.clone(this.attributes);
},
parse: function(data) {
this._initAttributes = _.clone(data);
return data;
},
rollback: function() {
this.set(this._initAttributes);
}
});
Take a look at NYTimes' backbone.trackit. It tracks multiple changes to the model instead of only the most recent change like model.changedAttributes() and model.previousAttributes(). From the README:
var model = new Backbone.Model({id:1, artist:'Samuel Beckett', 'work':'Molloy'});
model.startTracking();
model.set('work', 'Malone Dies');
console.log(model.unsavedAttributes()); // >> Object {work: "Malone Dies"}
model.set('period', 'Modernism');
console.log(model.unsavedAttributes()); // >> Object {work: "Malone Dies", period: "Modernism"}
model.save({}, {
success: function() {
console.log(model.unsavedAttributes()); // >> false
}
});
In addition, the library adds functionality to resetAttributes to
their original state since the last save, triggers an event when the
state of unsavedChanges is updated, and has options to opt into
prompting to confirm before routing to a new context.

Resources