I have a few models that don't just contain basic data attributes, but they might have one or two attributes that hold another models object.
This has been okay, but now I want to call
myRootModel.toJSON()
and I've noticed that it doesn't call .toJSON on the other models in my model that I'm trying to call toJSON() on.
Is there a way to override backbone model .toJSON to go through all fields, recursively, whether they are basic attributes, sub-models or collections? If not, can I override toJSON in each model / collection?
I'm aware of backbone-relational, but I don't want to go that route - I'm not using fetch/save, instead our API returns responses that I adjust in the models parse function and simply invoke new MyRootModel(data,{parse:true}).
Here's a way you can achieve such a thing (there's maybe another way):
Backbone.Model.prototype.toJSON = function() {
var json = _.clone(this.attributes);
for(var attr in json) {
if((json[attr] instanceof Backbone.Model) || (json[attr] instanceof Backbone.Collection)) {
json[attr] = json[attr].toJSON();
}
}
return json;
};
http://jsfiddle.net/2Asjc/.
Calling JSON.parse(JSON.stringify(model)) parses the model with all the sub-models and sub-collections recursively. Tried on Backbone version 1.2.3.
Related
I am working on a backbone/signalr POC. I have very simple models working and I can create them client side and retrieve them via signalr.
The problem is this:
If I create a client side version of a model with a nested model I can access the attributes like this:
model.attributes.nestedModel.attributes.attributeName
When I retrieve the model from signalr via
model.fetch()
the model comes back but now to access the nested model properties I need to use
model.attributes.nestedModel.attributeName
the attributes level on the nested model is dropped, so this causes template rendering to fail
How do I get around this? Am I doing something wrong? I am new to signalr/backbone.
BTW, I am using the backbone.signalr nuget package.
Thanks.
this is because when you are using fetch(), the server returns only one JSON object with the model attributes and the attributes of the nested models. for example, the server returns:
{
id: "1",
name: "Model",
nestedModel: {
id: "12",
name: "nestedModel"
}
}
backbone is not smart enough to figure out that nestedModel is actually a "model". It treats "nestedModel" as an attribute on Model. (it's just a regular JSON object, not a backbone object)
that's why this:
model.attributes.nestedModel.attributes.attributeName
does not work.
to make it work, you have to instantiate nestedModel as a Backbone Model. so after fetch is done: (assuming your nestedModel is an instance of NestedModel)
model.fetch().done(function() {
model.set('nestedModel', new NestedModel(model.get('nestedModel')));
});
You can make backbone does this for you automatically by overwriting the parse() method.
in your model:
var NestedModel = Backbone.Model.extend({
//your nested model methods
});
var Model = Backbone.Model.extend({
//do other model stuff
parse: function(response) {
response.nestedModel = new NestedModel(response.nestedModel);
return response;
}
});
this should make your statement work.
but usually I'd use
model.get('nestedModel').get('attributeName')
for more info about parse, see here: http://backbonejs.org/#Model-parse
and to apply this pattern in all other models with more flexibility, you probably wanna read this:
http://www.devmynd.com/blog/2013-6-backbone-js-with-a-spine-part-2-models-and-collections
Let say you are defining a Backbone.js Model. From the documentation we have ...
new Model([attributes], [options])
This seems great for passing some default attributes to a model. When passed the model automatically inherits those attributes and their respective values. Nothing to do. Awesome!
On they other hand lets say we have a Collection of that model.
new Backbone.Collection([models], [options])
Okay, cool we can pass some initial models and some options. But, I have no initial models and no options I need to pass so let's continue. I am going to fetch the models from the server.
collection.fetch([options])
Well I don't have any options, but I want to pass some attributes to add to each models as it is fetched. I could do this by passing them as options and then adding them to the attributes hash in the initialize for the model, but this seems messy.
Is their a Backbone.js native way to do this?
You can pass the attributes as options to fetch and over-ride the collection's parse method to extend the passed options (attributes) on the response.
The solution would look like the following:
var Collection = Backbone.Collection.extend({
url:'someUrl',
parse:function(resp,options) {
if (options.attributesToAdd) {
for (var i=0;i<resp.length;i++)
_.extend(resp[i],options.attributesToAdd);
}
return resp;
}
});
Then to add attributes when you call fetch on the collection, you can:
var collection = new Collection();
collection.fetch({
attributesToAdd:{foo:'bar',more:'foobar'}
});
You may have to tweak the code a bit to work with your JSON structure, but hopefully this will get you started in the correct direction.
When you have a JSON $resource how can you cast the resulting objects into more specific objects once obtained?
For example, right now they come back as an Array of Objects, but I want them to come back as an Array of "Appointment" objects, so that I can have some methods on that Appointment object which would answer questions about that Appointment object. Ex: Does This Appointment have any services associated with it? Is This appointment in the morning or afternoon?
At first I thought transformResponse hook would work from ngResource, but that doesn't appear to work. The return from that is not the actual objects. Seems that with that function you can only modify the actual data prior to the JSON parsing.
Finally, I question if this is even a proper angularJS technique? Or should these helper methods just show up in a controller or some other module and accept the object to work upon? I just think its cleaner to have them wrapped up in the object, but I admit that I'm not very experienced in angularJS.
If you're using a factory and want to add a function you can for example add the function to the prototype of the returned item (DEMO):
app.factory('Appointment', ['$resource', function($resource) {
var Item = $resource('appointments.json',{/*bindings*/},{/*actions*/});
Item.prototype.hasServices = function() {
if(this.services.length > 0) return true;
else return false;
};
Item.prototype.partOfDay = function() {
if(this.time.split(':')[0] > 12) return "afternoon";
else return "morning";
};
return Item;
}]);
And then access it on your resource in the controller:
$scope.appointments = Appointment.query({}, function() {
console.log($scope.appointments[0].partOfDay())
});
Or directly in the view inside for example an ng-repeat:
{{appointment.partOfDay()}}
To answer your last question, I think the above solution is a proper angularjs technique.
As soon as you have functions associated with a specific resource type it's in my opinion the best to directly append them to the respective resource object. Why should you create helper functions in the controller when you have to pass the resource as a parameter and additionaly the functions may be used in multiple controllers or scopes?!
I would like to access the calculated fields I have implemented in the model (backbone.js) from the template.
Do I need always to define a helper to do it?
I think the problem has to do with the way I pass the model to the template.
If I pass this.model.toJSON() I have access to the properties but not to the functions I have defined in it.
If I pass this.model directly I can access the function but not the properties of the backbone model.
Always pass this.model.toJSON() to your templates.
What you need to do to get your calculated values, is override your toJSON method on your model.
MyModel = Backbone.Model.extend({
myValue: function(){
return "this is a calculated value";
},
toJSON: function(){
// get the standard json for the object
var json = Backbone.Model.prototype.toJSON.apply(this, arguments);
// get the calculated value
json.myValue = this.myValue();
// send it all back
return json;
}
})
And now you have access to myValue from the the JSON that is returned by toJSON, which means you have access to it in the view.
The other option, as you mentioned, is to build helper methods and register them with Handlebars. Unless you have some functionality that changes based on how the template is being rendered, and/or what data is being passed to the template, I wouldn't bother with that.
Here is another possibility: (from the model initialize)
initialize: function() {
this.on("change", function () {
this.set({ calculatedColumn: this.get("otherColumn") }, { silent: true });
});
},
Computed properties in Backbone
I have had the same issue. #DerickBailey is right, of course, that overriding toJSON does the job. But it also leaks into the communication with the server (see muu's comment on his answer).
So eventually, I have built a Backbone plugin to specifically handle data export to templates, and do so with a minimum of fuss: Backbone.Marionette.Export. It also deals with nested structures, takes care of circular references etc. See the docs.
Here's how it works. Include the plugin file into your project and declare
MyModel = Backbone.Model.extend({
foo: function () {
return "I am a calculated value";
},
exportable: "foo" // <-- this is the one line you have to add
});
If you are a Marionette user, you are already done at this point. foo shows up in your templates as if it were a model attribute.
In plain Backbone views, just call myModel.export() or myCollection.export() instead of their toJSON counterparts when you render.
For methods taking arguments, there is an onExport handler. Examples, again, are in the docs.
The best way to do it is to add this to your model:
function initialize() {
this.set("calculatedColumn", function () { return this.otherColumn; });
}
A backbone model normally stores the actual data values internally in "model.attributes". That is why when you pass your model directly to the template, it only has functions added directly to model and not any data. And if you use model.toJSON() it is normally implemented in backbone as _.clone(model.attributes) (see backbone.js). So you have the data and not the functions added directly to the model. That is why the above works - you set the function on model.attributes, not on the model object itself. Do not reference model.attributes directly, use model.get("calculatedColumn") and model.set("calculatedColumn", ...).
So model.get("calculatedColumn") returns a function. If you go {{calculatedColumn}} in handlebars (assuming you're using handlebars), it shows the value returned by the function. But calculatedColumn will not be sent to the server because backbone does a JSON.stringify to model.toJSON in sync (in backbone.js) and JSON.stringify ignores functions. If you want JSON.stringify to not ignore the function (so the function is turned into a data value whenever toJSON is run on the model - during view rendering and model sync-ing), override model.toJSON just as #Derick Bailey described.
Also, you can derive your own BaseModel from Backbone.Model and override .toJSON and derive all your models from BaseModel if you need to. Then you would need a generic version of .toJSON that could be applied to any model.
Is there a way to easily clone Backbone Collection? I wonder why there is no build in method like for models. My problem is that I have a model holding a collection of childs. When I clone the model. I've still the collection of childs but only with their default values.
Simplest way:
var cloned = new Backbone.Collection(original.toJSON());
where original is the collection to clone.
Could always then extend Backbone's Collection to add clone support w or w/o inheritance support.
What's your use case that you want to clone the collection?
There isn't a built in clone function for a collection because you do not want to clone the models within the collection. Cloning a model would cause there to be two separate instances of the same model, and if you update one model, the other one won't be updated.
If you want to create a new collection based upon a certain criteria, then you can use the collection's filter method.
var freshmenModels = studentsCollection.filter(function(student) {
return student.get('Year') === 'Freshman';
}
var freshmenCollection = new Backbone.Collection(freshmenModels);
To go ahead and clone the models in the collection, you can write the following code
var clonedCollection = new Backbone.Collection();
studentsCollection.each(function(studentModel) {
clonedCollection.add(new Backbone.Model(studentModel.toJSON()));
});
Use the Backbone Collection clone() method:
var clonedCollection = myCollection.clone();
Another option, if you need the following (which is what I was looking for when I found this question ;) ):
The copy of the collection should be of the same type as the original collection (e.g. you've created your own collection type that extends Backbone.Collection)
The copy of the collection should be created with the same options as the original
The models in the copy of the collection should be created using the model.clone() method
Code:
var models = original.map(function (model) { return model.clone(); });
var options = _.clone(original.options);
var copy = new original.constructor(models, options);
A generic clone method on Backbone.Collection would be awkward because there are always going to be subtleties around whether models and their nested objects get copied by reference or are themselves cloned. Requirements will vary wildly according to your scenario, so it's been left for you to write what you need.