I can't figure out why we are passing in a model.toJSON() into this template:
app.TodoView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#item-template').html()),
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this; // enable chained calls
}
});
The example comes from this tutorial.
this.template(this.model.toJSON()) is the confusing part to me. The template method doesn't seem to take in an argument right? What is going on?
Underscore _.template function takes a template string as argument (and optionally a settings object) and returns a new pre-compiled template function which takes an object as an argument.
This object is the data used within the template:
// creates a template function
var templateFunc = _.template("<span><%= name %></span>");
// render the template using the passed data
templateFunc({ name: "Émile" }); // <span>Émile</span>
By default, template places the values from your data in the local
scope via the with statement. However, you can specify a single
variable name with the variable setting.
_.template("Using 'with': <%= data.answer %>", {variable: 'data'})({answer: 'no'});
model.toJSON() returns a shallow copy or the attributes hash of the model.
To achieve the equivalent of the above example:
var model = new Backbone.Model({ name: "Émile" });
templateFunc(model.toJSON()); // <span>Émile</span>
For Underscore.js before v1.7, the template function signature was a little different:
_.template(templateString, [data], [settings])
If a data object was passed, it didn't returned a function, but returned the rendered template string directly.
_.template('This is <%= val %>.', { val: "deprecated" });
// This is deprecated.
Related
I've noticed Marionette is very un-opinionated as far things go for the freedom they give you to choose a method to render data. It seems like there are a lot of ways to initially render a template with custom data
Returning a template with data:
template: function () {
var myTemplate = $('.someTemplate')
return _.template(myTemplate.html())({some: data});
}
Very similarly:
render: function () {
var template = this.getTemplate();
var html = Marionette.Renderer.render(template, {
model: this.model.toJSON(),
customData: this.customData
});
this.$el.html(html);
}
Serialize data:
serializeData : function () {
var customData = {some: 'data'};
var model = this.model.toJSON()
return _.extend(customData, model);
}
I've seen a lot of people in different code use variations of the first and the second. I personally prefer using serializeData but am curious: is there an advantage or use case where it would be appropriate to use the first two methods instead of serializeData?
The first case is not efficient - you are recompiling the template every time you want to render.
Anyway, your use case is exactly why Marionette has templateHelpers. Its the most concise way to provide extra data to the template while also passing serialized model.
So you would write:
templateHelpers : function () {
return {some: 'data'};
}
Or if its just static stuff:
templateHelpers: {some: 'data'}
More examples on how to use it here.
I think it's all about exploring natural behaviour of these things. Backbone View render is empty function by default. Marionette ItemView render extends Backbone's with this code.
It takes template by getTemplate method, by default it gives what is stored in template option. You can override getTemplate if you want to choose between several templates.
Then it collects data needed to be rendered by runing serializeData and extending it with templateHelpers. First one by default returns your model or collection toJSON method result, there you can prepare your data some way on every render. Second one is for helpers that will be calculated (if they are functions) if needed in template.
Template and data then go to Marionette.Renderer where just return template(data) by default happens. And then result can be attached to view's element.
I'm trying to use a simple collection and view to write out data from my backbone collection to my website. I just want to iterate over the collection and display properties like Id, Name, etc. in my template.
My collection gets its data from an api controller(a sample of the data is shown below).
My limited knowledge leads me to guess that the api controller is returning an object and not JSON.
So I'm guessing that is messing up my results. I've written out the collection to my Chrome console and attached a screenshot of what I see below.
So looking at the code below, is there a way that I can format the data returned from the api so that my collection can use it effectively?
Here is the code:
var ResearchCollection = Backbone.Collection.extend({
url: '/api/lab',
getresearch: function() {
this.fetch({
url: this.url
});
}
});
var researchCollection = new ResearchCollection();
return Backbone.View.extend({
className: 'labRender',
template: _.template(tmpl, null, { variable: 'x' }),
render: function () {
researchCollection.getresearch();
console.log('collection: ', researchCollection);
}
Basically, I just want to iterate over my collection and display properties like Id, Name, etc. in my template.
Here is the raw data from the api controller that I am using to populate my collection:
{
"odata.metadata":"http://sol.edu/SOM/Api/v1/$metadata#ApiKeys","value":[
{
"odata.id":"http://sol.edu/SOM/Api/v1/ApiKeys('2f2627ed-3a97-43aa-ac77-92f227888835')","Id":"2f2627ed-3a97-43aa-ac77-92f227888835","Name":"VideoSearch","TimeoutInMinutes":20160,"IsDefault":false,"CreateAuthTicketsForResources":false,"ReportAuthFailureAsError":false,"ExcludePrivatePresentations":true,"Internal":true,"ViewOnlyAccessContext":true
}
]
}
when piped to the browser's console(why is each character a separate attribute?):
I think maybe this is because you mixed up collection and model. In Backbone, Model are fundamental unit, a Model can be used to render a template.However, Collection are ordered sets of 'Models'. So, if you just want to transform a data like you describe above, you may better select a Model instead of 'Collection'. Just try this:
var ResearchModel = Backbone.Model.extend({
initialize: function(attributes) {
this.url = 'api/lab'
}
});
// when you initialize a Model or collection, the first parameter is the attribute you want to initialize
var researchModel = new ResearchModel({});
return Backbone.View.extend({
className: 'labRender',
template: _.template(tmpl, null, { variable: 'x' }),
render: function () {
researchModel.fetch();
console.log('collection: ', researchModel);
}
Otherwise, if you just want to use collection, you had better specify its Model.Backbone use JSON, so you can also specify the model with your key.
When instantiating a new view, I'm passing in a model. I have access to that model in the "initialize" property, but I can't reference it when I'm trying pass the model into a template. Any idea why?
var postView = Backbone.View.extend({
initialize : function() {
// returns model
console.log('the model we are interested in',this.model);
this.render();
},
el : "#blog-post",
template : function() {
// returns undefined
var model = this.model;
return _.template($('#postview').html(), {
post : model
});
},
render : function() {
var self = this;
this.$el.html(self.template);
}
});
I'm instantiating it using a method in another view:
readMore : function(e, index) {
var self = this;
var newView = new postView({
model : self.collection.models[index].toJSON()
});
}
You're passing a function to this.$el.html:
this.$el.html(self.template);
That's the same as saying:
var f = this.template;
this.$el.html(f);
So what does html do when you pass it a function? Well, from the fine manual:
.html( function )
A function returning the HTML content to set. Receives the index
position of the element in the set and the old HTML value as
arguments. jQuery empties the element before calling the function; use
the oldhtml argument to reference the previous content. Within the
function, this refers to the current element in the set.
When you pass html a function, it will call that function but this won't be what you think it is, this inside the function will be the DOM element that is having its HTML set.
I think you want to call this.template yourself and hand its return value to html:
this.$el.html(this.template());
// ------------------------^^
That way template will have the view as its this as you expect it to.
Best guess would be this no longer refers to the context of the view. if you log this within the function what does it show.
EDIT-
Actually not sure if this will give the expected results, i normally use handlebars but i think the setup for _.template and hanblebars is pretty similar. Your template template normally wants a plain java object passed to it other wise you would have to access the variables you want in yours like post.attributes.name, however if you just pass the toJSON version of your model you can access the attributes without the need for post.attributes.
Also you can compile your template once then just refer to it, no need place it as a function and have it grab from the DOM every time (assuming it never changes). Below is an example of what i mean. In your template you would then have <%= name %> etc to grab you model attributes.
var postView = Backbone.View.extend({
initialize : function() {
// returns model
console.log('the model we are interested in',this.model);
this.render();
},
el : "#blog-post",
template : _.template($('#postview').html()),
render : function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
Oh also the convention normally is that render returns 'this' so if you want to call it from some where else and attach it to a new part of the page you can do it calling postView.render().el
You may be passing the model, but you're not receiving it in your view. Try:
initialize: function(model) { ...
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.
I have this backbone that is working fine, i just want to render the collection is been fetched!
code:
var SearchView = Backbone.View.extend({
events: {
"keyup": "handleChange"
},
initialize: function(){
this.collection.bind("reset", this.updateView, this);
},
render: function(){
// this is where i need help
this.$el.next('#suggestions').append(// iterate over this.collection and apppend here) //
},
handleChange: function(){
var term = this.$el.val();
this.collection.search(term);
},
updateView: function() {
this.render();
}
});
I just want to iterate over this.collection and display the "text" attribute thats inside each collection and append it to the ('#suggestions') el. thanks
Focusing on your reset, render over collections issue, this is how I would do it. This way assumes that when you create your view, the $el is already present and you're passing it in through the View constructor so it's ready to go.
var SearchView = Backbone.View.extend({
template: _.template('<span><%= term %></span>');
initialize: function(){
this.collection.bind("reset", this.render, this);
},
render: function(){
this.addAllTerms();
return this;
},
addAllTerms: function() {
var self = this;
this.collection.each(function(model) {
self.addTerm(model);
});
},
addTerm: function(someModel) {
this.$el.next('#suggestions').append(this.template(someModel.toJSON()));
}
});
It's a bit different from your approach in a few ways. First, we utilize Underscore's template function. This could be anything from span to li to div whatever. We use the <%= %> to indicate that we're going to interpolate some value (which will come from our model.term attribute).
Instead of going to the handler then render, I just bind the collection to render.
The render() assumes we're always going to refresh the whole thing, build from scratch. addAllTerms() simply cycles through the collection. You can use forEach() or just each() which is the same thing except forEach() is 3 characters too long for my lazy bum. ;-)
Finally, the addTerm() function takes a model and uses it for the value that it will append to the #suggestions. Basically, we're saying "append the template with interpolated value". We defined the template function up above as this View object property to clearly separate the template from data. Although you could have skipped this part and just append('<span>' + someModel.get('term') + '</span>') or what not. The _.template() uses our template, and also takes any sort of object with the property that lines up with the one in our template. In this case, 'term'.
It's just a different way to do what you're doing. I think this code is a little more manageable. For example, maybe you want to add a new term without refreshing the whole collection. The addTerm() function can stand on its own.
EDIT:
Not that important but something I utilize with templates that I found useful and I didn't see it when I first started out. When you're passing the model.toJSON() into the template function, we're essentially passing all the model attributes in. So if the model is like this:
defaults: {
term: 'someTerm',
timestamp: '12345'
}
In our previous example, the attribute timestamp is also passed in. It isn't used, only <%= term %> is used. But we could easily interpolate it as well by adding it to the template. What I want to get at is that you don't have to limit yourself to data from one model. A complex template might have data from several models.
I don't know if it's the best way, but what I do is have something like this:
makeHash: function() {
var hash = {};
hash.term = this.model.get('term');
hash.category = anotherModel.get('category');
var date = new Date();
hash.dateAccessed = date.getTime();
return hash;
}
So you can easily build your own custom hash to throw into a template, aggregating all the data you want to interpolate into a single object to be passed into a template.
// Instead of .toJSON() we just pass in the `makeHash()` function that returns
// a customized data object
this.$el.next('#suggestions').append(this.template(this.makeHash()));
You can also easily pass in whole objects.
makeHash: function() {
var hash = {};
hash.term = this.model.get('term');
var animal = {
name: 'aardvark',
numLegs: 4
};
hash.animal = animal;
return hash;
}
And pass this into our template that looks like this:
template: _.template('<span><%= term %></span>
<span><%= animal.name %></span>
<span><%= animal.numLegs %></span>')
I'm not sure if this is the best way but it helped me understand exactly what data is going into my templates. Maybe obvious but it wasn't for me when I was starting out.
I found the solution to the problem, im gonna put it here for people that might want to know:
render: function(){
this.collection.forEach(function(item){
alert(item.get("text"));
});
}