Access Nested Backbone Model Attributes from Mustache Template - backbone.js

I have one Backbone model which has an attribute that is a reference to another Backbone model. For example, a Person has a reference to an Address object.
Person
FirstName
LastName
Address
Street
City
State
Zip
These are classes that extend the Backbone model. So, then if I construct an object like the following...
var address = new Address({ Street: "123 Main", City: "Austin" });
var person = new Person({ FirstName: "John", Address: address });
I cannot seem to figure out how to access it in my Mustache template.
Hi {{FirstName}}, you live in {{Address.City}}.
Obviously does not work. When I look at the internals in Firebug, Address is an object, but the City is an attribute within the attributes object of Address. I cannot find any examples of how to access these attributes of associated objects.
I appreciate any help! Thanks!

I ended up solving this issue with the following approach.
I switched from Mustache.js to Handlebars.js for the templating engine. This allowed me to use path based expressions to access nested or associated objects and their attributes.
Hi {{FirstName}}. You live in {{Address.City}}.
But, I also had to change the way I was passing a JSON object to the template. I was using the toJSON method that is part of the Backbone.Model class. But, this was not generating JSON for the associated Address correctly (for the templating to work.) It was burying the address attributes in a member titled "attributes". So, instead, I ended up doing this...
var jsonForTemplate = JSON.parse(JSON.stringify(person));
This gave me a "raw" version of the objects and their associated objects which the template could access using the syntax shown above. JSON.parse and JSON.stringify are both part of json2.js.

I handled this by making another version of toJSON called deepToJSON that recursively traverses nested models and collections. The return value of that function can then be passed to a handlebars.js template.
Here is the code:
_.extend(Backbone.Model.prototype, {
// Version of toJSON that traverses nested models
deepToJSON: function() {
var obj = this.toJSON();
_.each(_.keys(obj), function(key) {
if (_.isFunction(obj[key].deepToJSON)) {
obj[key] = obj[key].deepToJSON();
}
});
return obj;
}
});
_.extend(Backbone.Collection.prototype, {
// Version of toJSON that traverses nested models
deepToJSON: function() {
return this.map(function(model){ return model.deepToJSON(); });
}
});

Try using Handlebars, a templating engine based on Mustache with nested properties support.
Then it would be as easy as {{Address/City}}.
If you don't want to change your templating engine, you can flatten results from Address object and pass them as properties directly on the Person.

The way to go about the same in Mustache would be as follows:
Hi
{{FirstName}}, you live in {{#Address}}{{City}} {{/Address}}
Hope it helps..

Related

Reference to HTML elements in view, a convention?

I'm currently in the progress of learning Backbone.js and I'm using the book Developping Backbone Applications.
I have a questions about the reference to HTML elements and how they are stored. For example:
initialize: function() {
this.$input = this.$('#new-todo');
Here the HTML element with ID to-do is stored in the this.$input, why do we use the $ in front of input, is this merely a convention? If I change this.$input to this.input my code works fine. I find this confusing because the book states:
The view.$el property is equivalent to $(view.el) and view.$(selector) is equivalent to $(view.el).find(selector).
I would think that $(view.el) does something completely different than (view.el).
How is this.$input saved in Backbone.js? If I console.log it, it produces:
Object[input#new-todo property value = "" attribute value = "null"]
Could someone give me some insight? :)
Using $ infront of a variable name is just a naming convention. It helps developer in distinguishing variable holding jQuery objects from others.
view.$el is a helper variable provided by Backbone, so that we can use it directly, instead of explicitly forming the jQuery object. Hence view.$el is equivalent to $(view.el).
view.$el is assigned in setElement method:
setElement: function(element, delegate) {
// Some code
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
// Some code
}
Backbone.$ is reference to $ global variable exported by jQuery.
view.$(selector) is a method defined in View. It's definition does exactly same as $(view.el).find(selector)
$: function(selector) {
return this.$el.find(selector);
}

Returning nested model from backbone via signalr drops attribute property

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

Is there anything similar to "KO.mapping.fromJS" in AngularJS?

I wanted to attach some new calculated property to a complex json object returned from a REST Service. This can be easily achieved through KnockoutJS's Mapping pluggin.
But I have decided to go for AngularJS this time. Is there any modules/pluggins similar to knockout's mapping pluggin ??
my PROBLEM is as shown below:
JSON Returned from server is something like:
{
id:2,
name: 'jhon',
relatives:[
{id:1,name:'linda', score:{A:10,B:33,C:78} },
{id:2,name:'joseph', score:{A:20,B:53,C:68} },
{id:3, name:'keith', score:{A:40,B:83,C:30} }
]
}
in the above json object, I want to attach some calculated property to each objects inside "relatives" collection based on the score each relative has.
Try using
angular.extend($scope, data);
I'm also starting to use Angular, coming from Knockout and Durandal :) Hope this might work for you. The data should be accessible in your view ($scope) directly.
Edit: See this thread.
Basically in angular there is no thing similar to Observable variables.
AngularJS makes observing separately from $scope itself.
To make map from Json in AngularJS you can use angular.fromJson to bind data from Json.
To add fields to your scope you also can use angular.extend.
But anyway adding calculated field is thing that you need to make by yourself, for this purpose you can try to use watch methods: $scope.watch, $scope.watchGroup,
watchCollection.

Get rid of surrounding elements in Backbone.Marionette

I have the following view:
return Marionette.ItemView.extend({
el: '<section>',
template: JST['app/scripts/templates/grid.ejs'],
that is called like this:
// a Layout
regions: {
grid: '#grid',
detail: '#detail'
},
onShow: function () {
var detailModel = new DetailModel();
var g = new GridView(detailModel);
this.grid.show(g);
}
The question is: How do I get rid of the surrounding section element ? I tried to omit the el property but that gives me the following strange looking div:
<div productname>
Regards Roger
The surrounding element is required for backbone to work. It is essentially a container/placeholder for your view to sit in, whether its contents have been rendered or not.
If you really insist on not having the container then I would consider resorting to the following:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#set-how-views-el-is-attached
Marionette.Region.prototype.open = function(view){
this.$el.empty().append(view.$el.children());
}
I say 'resorting' because, in my opinion, this is not how Backbone is supposed to be used and may have side-effects. (im not quite sure what will happen when the view in that region tries to re render; what will it's el element be pointing to?)
To expand on Scott's answer, it's probably a very bad idea to try and force the removal of the surronding view tags.
All Backbone views are contained within an DOM element. Given this fact, you have 2 main options:
have Backbone put your view into the default div element
specify which element you want Backbone to wrap your view with, using the el or tagName attributes
If the "extra" tags are creating issues (e.g. you need to generate a specific HTML set for use with a plugin), then you're not defining the wrapping element properly. For more on the subject, take a look at this blog post of mine: http://davidsulc.com/blog/2013/02/03/tutorial-nested-views-using-backbone-marionettes-compositeview/
Edit based on jsFiddle: the reason for your strange behavior is that you were passing a model instance to the initialize function. This is then interpreted as attributes for the view and get set as HTML attributes.
The correct way to provide a model instance to a view is :
new App.FooterView({
model: new App.Model()
})
In other words, you provide a javascript object to the view, with a model property. If you want to learn Marionette basics quickly, check out the free preview to my book: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf (You'll find how to instantiate a view with a model on pages 15-21)

How to access a calculated field of a backbone model from handlebars template?

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.

Resources