JS templating system with Backbone.js - javascript

I am looking at some good templating systems to be used alongwith an MVC framework like Backbone.js
I am aware of one such system (jQuery Templating). However, the same has been discontinued for some reasons and hence I am looking at some other good options.
Please suggest something which is flexible enough from a view perspective. (e.g. a dynamic view with enabled/disabled button based on some logic, tabular data with different styles based on some logic, etc)

I really like Handlebars.js...
Here's some JavaScript...
var HandlebarsView = Backbone.View.extend({
el: '#result'
initialize: function(){
this.template = Handlebars.compile($('#template').html());
},
render: function(){
var html = this.template(this.model.toJSON());
this.$el.html(html);
}
});
var HandlebarsModel = Backbone.Model.extend({});
var model = new HandlebarsModel({
name: 'Joe Schmo',
birthday: '1-1-1970',
favoriteColor: 'blue'
});
var view = new HandlebarsView({
model: model
});
view.render();
Then the html...
<div id="result">
</div>
<script id="template" type="text/html">
<div>Name:{{name}} Birthday: {{birthday}} Favorite Color: {{favoriteColor}} </div>
</script>
Give that a shot!

You have out of the box Underscore's template system.
With example:
# code simplified and not tested
var myView = Backbone.View.extend({
template: _.template( "<h1><%= title %></h1>" ),
render: function(){
this.$el.html( this.template({ title : "The Title" }) );
return this;
}
});
All the template systems you can find have an integration similar to this.
Of course this is a simplified example, normally the template is fed with the this.model.toJSON(), also you can find tricks to declare the template body into an <script> tag, and you can use Mustache syntax instead of ERB.

I'm using haml-coffee together with rails asset pipeline.
Quite exotic choice, but works great so far.
Inside view it's simple as that:
var MyView = Backbone.View.extend({
template: JST['path/to/mytemplate']
render: function(){
var html = this.template(this.model.toJSON());
this.$el.html(html);
}
})

Related

Javascript templating solution that allows after-rendering use of injected objects?

So, I'm building an application based on Backbone.js, by using templates for rendering some objects.
It's working, however now I need to dynamically reference the objects at runtime, and I'm not sure it's possible with the templating solutions I've seen (underscore, handlebar, ...), that "flatten" the javascript.
To illustrate, I have a list of objects, let's say Tasks.
I have a model that can be simplified as such :
{{#each tasks.models as |task|}}
<div>
{{task.name}}
</div>
{{/each}}
Now, I will need to use the 'task' object dynamically, after the rendering is finished. For example, do something like this :
<div>
{{task.name}} - <button onClick="task.setComplete()" />
</div>
Of course this way doesn't work ; and neither do something like {{task}}.setComplete(), as {{task}} is transformed to a string when rendering.
Is there a way to do this?
I was thinking I need closures to keep the objects, the only way to obtain them is not to flatten the html, as everything is transformed to string otherwise.
Any idea? Maybe are there templating libraries that would allow to generate directly DOM objects, that I could add to my document ?
Thanks in advance,
This question is tagged with backbone.js so you should use Backbone's normal view event handling system instead of onclick handlers. You mention tasks.models so presumably tasks is a collection.
One approach would be to use data-attributes to stash the model ids. Your template would look like this:
{{#each tasks}}
<div>
{{name}} - <button data-id="{{id}}" type="button">Completed</button>
</div>
{{/each}}
and then your view would be set up like this:
Backbone.View.extend({
events: {
'click button': 'completed'
},
render: function() {
var t = Handlebars.compile($('#whatever-the-template-is').html());
this.$el.append(t({
tasks: this.collection.toJSON()
}));
return this;
},
completed: function(ev) {
var id = $(ev.currentTarget).data('id');
var m = this.collection.get(id);
// Do whatever needs to be done to the model `m`...
}
});
Demo: https://jsfiddle.net/ambiguous/z7go5ubj/
All the code stays in the view (where all the data is already) and the template only handles presentation. No globals, nice separation of concerns, and idiomatic Backbone structure.
If the per-model parts of your view are more complicated then you could have one view for the collection and subviews for each model. In this case, your per-model templates would look like this:
<div>
{{name}} - <button type="button">Completed</button>
</div>
No more data-attribute needed. You'd have a new per-model view something like this:
var VM = Backbone.View.extend({
events: {
'click button': 'completed'
},
render: function() {
var t = Handlebars.compile($('#whatever-the-template-is').html());
this.$el.append(t(this.model.toJSON()));
return this;
},
completed: function() {
console.log('completed: ', this.model.toJSON());
}
});
and the loop would move to the collection view:
var VC = Backbone.View.extend({
render: function() {
this.collection.each(function(m) {
var v = new VM({ model: m });
this.$el.append(v.render().el);
}, this);
return this;
}
});
Demo: https://jsfiddle.net/ambiguous/5h5gwhep/
Of course in real life your VC would keep track of its VMs so that VC#remove could call remove on all its child VMs.
My answer is just a hint, I tried to keep close to the question. Refer to this answer for a better solution: https://stackoverflow.com/a/32493586/1636522.
You could use the index or any other information to find the item. Here is an example using Handlebars, assuming each task can be identified by an id:
var tasks = [];
var source = $('#source').html();
var template = Handlebars.compile(source);
tasks[0] = { id: 42, label: 'coffee', description: 'Have a cup of coffee.' };
tasks[1] = { id: 13, label: 'help', description: 'Connect to StackOverflow.' };
tasks[2] = { id: 40, label: 'smile', description: 'Make μ smile.' };
$('#placeholder').html(template({ tasks: tasks }));
function onClick (id) {
var task, i = 0, l = tasks.length;
while (i < l && tasks[i].id !== id) i++;
if (i === l) {
// not found
}
else {
task = tasks[i];
alert(task.label + ': ' + task.description);
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.2/handlebars.min.js"></script>
<script id="source" type="text/x-handlebars-template">
{{#each tasks}}
<button
type="button"
class="task"
style="margin-right:.5em"
onclick="onClick({{id}})"
>{{label}}</a>
{{/each}}
</script>
<div id="placeholder"></div>

Backbone.js with Handlebars.js not rendering options

I'm building my first Backbone.js app and want to use Handlebars to insert my data.
I've got the backbone.View rendering ok but the {{ variables }} are not being replaced with the data I pass in.
This is my view
OrderRow = Backbone.View.extend({
template: Handlebars.compile(
'<div class="orderContainer">' +
'<p class="row-top-container left">{{id}}</p>' +
'<p class="row-top-container right">{{time_stamp}}</p>' +
'<p class="row-top-container right">{{queue_time}}</p><br />' +
'<p class="items-contaier">{{items}}</p>' +
'</div>'
),
initialize: function(){
console.log("options used: " + this.options.sayHello );
this.render();
},
render: function(){
this.$el.html(this.template(this.options));
return this;
}
});
var data = {
id:657543,
name:"name"
}
var row = new OrderRow({ el: $('.row'), options:data });
And this is the DOM after the view's been rendered
Does anyone know what I'm missing?
Any help is appreciated.
Thanks!
You have several problems:
Backbone no longer automatically sets this.options in views so your code will break when you upgrade Backbone. You can solve this by doing it yourself in initialize:
initialize: function(options) {
this.options = options;
//...
}
You have multiple .row elements so saying el: $('.row') will bind the view to all of them (depending on the Backbone version) and that could cause strange things to happen. You'd be better off binding one view to each <li> using the id attributes on the <li>s:
new OrderRow({ el: '#152293', ... });
You're not feeding your template what you think you are. You're passing the data to your view in the options option:
new OrderRow({ ..., options: data });
so that data will end up in this.options.options inside the view and that means that you want to say:
this.$el.html(this.template(this.options.options))
or use this.options = options.options in initialize and keep using this.options elsewhere.
Demo: http://jsfiddle.net/ambiguous/9w3AW/
Wrapping your data in a Backbone model would probably make more sense though.

Connecting predefined HTML to Models and Views in Backbone

I'm starting out with Backbone.js so I must say I'm not yet very familiar with the concepts.
I have predefined HTML and I want to use Backbone to manage this. This is important and I want to keep it like this.
Say this is (part of) my HTML:
<div class="pig" data-id="1">
<h1>Harry</h1>
<input type="text" value="Harry">
</div>
<div class="pig" data-id="2">
<h1>Jill</h1>
<input type="text" value="Jill">
</div>
<div class="pig" data-id="3">
<h1>Bob</h1>
<input type="text" value="Bob">
</div>
Now the idea is that when you change the input this should update my Backbone Model and render the view, resuling in the h1 being updated with the new name. I'm not sure how I should set up my models and views.
I kind of have the structure of my models and my views, but I don't know how I should use them.
At the moment I have something like this:
var PigModel = Backbone.Model.extend()
var pigs = new PigModel()
pigs.reset([
{"id": "1", "name": "Harry"},
{"id": "2", "name": "Jill"},
{"id": "3", "name": "Bob"}
])
var PigView = Backbone.View.extend({
el: '.pig',
events: {
'change input': function() {
// update appropriate Model ?
this.render()
}
},
render: function() {
var new_name = // get new name from model ?
var pig_template = _.template($('#pig_template').html())
var new_contents = pig_template({ name: new_name })
// render only the pig that was changed ?
}
})
So at the places where I added comments in the code I don't know what to do:
At the change event, how do I find the model that belongs to this view? I could loop through all the models but that seems inefficient.
Again in the render method, how do I get the model that belongs to this view?
In the render method, how can I only render the pig view the event was on, and not render all the pigs?
Maybe I'm going for a total wrong approach over here. If so, please point me in the right direction.
Ok, I managed to figure it out.
The idea is to loop through your existing HTML using jQuery, then creating instances of views and models of it using the jquery selectors and the preloaded json.
HTML:
<div class="pig">
<h1>Harry</h1>
<input type="text" value="Harry" />
</div>
<div class="pig">
<h1>Jill</h1>
<input type="text" value="Jill" />
</div>
<div class="pig">
<h1>Bob</h1>
<input type="text" value="Bob" />
</div>
Javascript:
$(function() {
var PigModel = Backbone.Model.extend()
var PigView = Backbone.View.extend({
events: {
'change input': function(e) {
this.model.set('name', e.currentTarget.value)
this.render()
}
},
render: function() {
this.$el.html(
'<h1>' + this.model.get('name') + '</h1>' +
'<input type="text" value="' + this.model.get('name') + '" />'
)
}
})
var pig_data = [
{"name": "Harry"},
{"name": "Jill"},
{"name": "Bob"}
]
// the magic starts here
var pig_number = 0
$('.pig').each(function() {
new PigView({
el: this,
model: new PigModel(pig_data[pig_number])
})
})
})
Jsfiddle: http://jsfiddle.net/tU3Mt/1/
Like this I can serve a complete HTML page from the server, then load the desired elements into my backbone views/models and manage them from there.
About wether this is the way backbone should be used or not:
It may not be the most logical/efficient way to do this from a developer point of view. However, I think this approach has some important benefits:
The client get's HTML served directly and won't have to process any javascript before the page can be presented. This will become more noticable as your HTML gets bigger/more complex.
Search engines/web crawlers can read your page because it serves the complete HTML.
For some people these points may not be that important, because a webapp will be fast after it has been loaded and search engines won't have to crawl it. However in some situations you might have a mix of a website and a webapp, where the page needs to load fast, be crawlable but also have a responsive interface which is complex enough to make a developer want to use something like backbone. If somebody has other ideas about this I sure like to hear them.
I would take a little bit of a different approach. Create a collection of Pig models. Have a view that knows how to render a single pig, PigView. Let it be responsible for updating the one pig and it's corresponding h1. Have a higher level view that renders the collection of pigs into whatever parent element you want.
var PigModel = Backbone.Model.extend()
// You want a collection of pig models
var pigs = new Backbone.Collection([
{"id": "1", "name": "Harry"},
{"id": "2", "name": "Jill"},
{"id": "3", "name": "Bob"}
], {model: PigModel});
var PigView = Backbone.View.extend({
// Want to set the class for the generated el not pass a selector
className : "pig",
// Dummy template from your markup in question
template : function (modelAttrs) {
return "<h1>"+modelAttrs.name+"</h1><input type='text' value='"+modelAttrs.name+"'>";
},
events: {
'change input': function(e) {
// You have a reference to the model through this
this.model.set({name : e.currentTarget.value});
this.render();
}
},
// Bare bones render
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
// Parent view for responsible for rendering the collection of pigs somewhere.
var HigherLevelView = Backbone.View.extend({
render : function () {
pigs.each(function (pig) {
this.$el.append(new PigView({model : pig}).render().el);
});
}
});
$("body").append(new HigherLevelView().render().el);
Fiddle
Attach to existing html is possible, but it is not very common approach. Because of that, there is almost no real info on this :(
You could try to follow steps described in following article: http://lostechies.com/derickbailey/2011/09/26/seo-and-accessibility-with-html5-pushstate-part-2-progressive-enhancement-with-backbone-js/
Idea is, to add some data-attribute into rendered html, so you will be able to match model with rendered html.

My fake case statement in Emberjs is blowing up. What's the right way?

I'm trying to render a different handlebars template based on the current value of a property in my model, and there could be quite a few options (hence I'd rather not use a lot of {{#if}}s). The best thing I can think of is this:
Ember.Handlebars.registerBoundHelper('selectorType', function(name, options) {
return Ember.Handlebars.compile("{{template _selectors_" + name + "}}")(options.contexts[0], options);
});
And I use that in my template like:
{{selectorType selector.name}}
(instead of like a hundred {{#if}}s)
The problem is that I get this error during render: "You can't use appendChild outside of the rendering process"
Clearly I'm doing something wrong. What's the right way to do this?
I don't think there's any need to create a helper to do this. You can do it from within the view by modifying the templateName and then calling the rerender method once you've changed its templateName:
init: function() {
this.set('templateName', 'firstOne');
this._super();
},
click: function() {
this.set('templateName', 'secondOne');
this.rerender();
}
We can use the init method for setting the empty templateName before the template has been rendered. We'll then call the _super method to complete the insertion of the view into the DOM. We can then trigger the change of the view on the click event. We update the templateName variable and then call rerender() to re-render this particular view.
I've set you up a JSFiddle as an example: http://jsfiddle.net/pFkaE/ try clicking on "First One." to change the view to the secondOne.
I ended up solving this using a ContainerView with dynamic childViews, see Ember.js dynamic child views for a discussion on how.
The relevant code is (coffeescript):
App.SelectorType = Ember.Object.extend
name: null
type: null
typeView: null
options: null
App.SelectorTypes = [
App.SelectorType.create(
name: 'foo'
type: 'bar'
) #, more etc
]
App.SelectorTypes.forEach (t) ->
t.set 'typeView', Ember.View.create
templateName: "selectors/_#{t.get('viewType')}_view"
name: t.get('name')
App.SelectorDetailView = Ember.ContainerView.extend
didInsertElement: ->
#updateForm()
updateForm: (->
type = #get('type')
typeObject = App.SelectorTypes.findProperty('type', type)
return if Ember.isNone(type)
view = typeObject.get('typeView')
#get('childViews').forEach (v) -> v.remove()
#get('childViews').clear()
#get('childViews').pushObject(view)
).observes('type')
And the template:
Selector Type:
{{view Ember.Select
viewName=select
contentBinding="App.SelectorTypes"
optionValuePath="content.type"
optionLabelPath="content.name"
prompt="Pick a Selector"
valueBinding="selector.type"
}}
<dl>
<dt><label>Details</label></dt>
<dd>
{{view App.SelectorDetailView typeBinding="selector.type"}}
</dd>
</dl>
Seems too hard, though, would be interested to see better solutions!

Backbone.js backed list not being refreshed by jQuery mobile (listview(‘refresh’))

I’m trying to add sort options to a JQM list which is backed by a backbone.js collection. I’m able to sort the collection (through the collection’s view) and rerender the list, but JQM isn’t refreshing the list.
I’ve been searching and I found several questions similar to mine (problems getting the JQM listview to refresh) but I’ve been unable to get it to work.
I’ve tried calling $(‘#list’).listview(‘refresh’) and $(‘#list-page’).page() etc. to no avail. I suspect that Perhaps I’m calling the refresh method in the wrong place (to early), but I’m not sure where else I should put it (I’m just starting out with backbone).
Here’s the markup and js.
HTML:
<div data-role="page" id="Main">
<div data-role="header"><h1>Main Page</h1></div>
<div data-role="content">
<ul data-role="listview">
<li>Page 1</li>
</ul>
</div>
<div data-role="footer"><h4>Footer</h4></div>
</div>
<div data-role="page" id="Page1">
<div data-role="header">
Back
<h1>Items</h1><a href="#dvItemSort" >Sort</a></div>
<div data-role="content">
<div id="dvTest">
<ul id="ItemList" data-role="listview" data-filter="true"></ul>
</div>
</div><div data-role="footer"><h4>Footer</h4></div></div>
<div data-role="page" id="dvItemSort">
<div data-role="header"><h4>Sort</h4></div>
<a href="#Page1" type="button"
name="btnSortByID" id="btnSortByID">ID</a>
<a href="#Page1" type="button"
name="btnSortByName" id="btnSortByName">Name </a>
</div>
Javascript:
$(function () {
window.Item = Backbone.Model.extend({
ID: null,
Name: null
});
window.ItemList = Backbone.Collection.extend({
model: Item
});
window.items = new ItemList;
window.ItemView = Backbone.View.extend({
tagName: 'li',
initialize: function () {
this.model.bind('change', this.render, this);
},
render: function () {
$(this.el).html('<a>' + this.model.get('Name') + '</a>');
return this;
}
});
window.ItemListView = Backbone.View.extend({
el: $('body'),
_ItemViews: {},
events: {
"click #btnSortByID": "sortByID",
"click #btnSortByName": "sortByName"
},
initialize: function () {
items.bind('add', this.add, this);
items.bind('reset', this.render, this);
},
render: function () {
$('#ItemList').empty();
_.each(items.models, function (item, idx) {
$('#ItemList').append(this._ItemViews[item.get('ID')].render().el);
}, this);
$('#ItemList').listview('refresh'); //not working
// $('#ItemList').listview();
// $('#Page1').trigger('create');
// $('#Page1').page(); //also doesn't work
},
add: function (item) {
var view = new ItemView({ model: item });
this._ItemViews[item.get('ID')] = view;
this.$('#ItemList').append(view.render().el);
},
sortByName: function () {
items.comparator = function (item) { return item.get('Name'); };
items.sort();
},
sortByID: function () {
items.comparator = function (item) { return item.get('ID'); };
items.sort();
}
});
window.itemListView = new ItemListView;
window.AppView = Backbone.View.extend({
el: $('body'),
initialize: function () {
items.add([{ID: 1, Name: 'Foo 1'}, {ID:2, Name: 'Bar 2'}]);
},
});
window.App = new AppView;
});
EDIT: I realized that the first line of html markup I posted wasn't displaying in my post so I pushed it down a line.
EDIT 2: Here's a link to a jsfiddle of the code http://jsfiddle.net/8vtyr/2/
EDIT 3 Looking at the resulting markup, it seems like JQM adds some of the classes to the list items. I tried adding them manually using a flag to determine whether the list was being reRendered as a result of a sort and the list then displays correctly.
However, besides being somewhat of an ugly solution, more importantly my backbone events on the “item” view no longer fire (in the code example I posted I didn’t put the code for the events because I was trying to keep it as relevant as possible).
EDIT 4 I sort of got it working by clearing my cache of views and recreating them. I posted my answer below.
EDIT 5
I updated my answer with what i think is a better answer.
I'm not sure if this should be its own answer or not (i did look through the FAQ a bit), so for now I’m just updating my previous answer.
I have now found a better way to sort the list using my cached views. Essentially the trick is to sort the collection, detach the elements from the DOM and then reattach them.
So
The code now would be
$list = $('#ItemList')
$('li', $list ).detach();
var frag = document.createDocumentFragment();
var view;
_.each(item.models, function (mdl) {
view = this._ItemViews[item.get('ID')];
frag.appendChild(view.el);
},this);
$list.append(frag);
OLD ANSWER
I sort of solved the problem. I was examing the rendered elements and I noticed that when the elements were “rerendered” (after the sort) they lost the event handlers (I checked in firebug). So I decided to clear my cache of views and recreate them. This seems to do the trick, though I’m not really sure why exactly.
For the code:
Instead of:
$('#ItemList').empty();
_.each(items.models, function (item, idx) {
$('#ItemList').append(this._ItemViews[item.get('ID')].render().el);
}, this);
$('#ItemList').listview('refresh'); //not working
I clear the view cache and recreate the views.
$('#ItemList').empty();
this._ItemViews = {};
_.each(items.models, function (item, idx) {
var view = new ItemView({ model: item });
this._ItemViews[item.get('ID')] = view;
this.$('#ItemList').append(view.render().el)
}, this);
$('#ItemList').listview('refresh'); //works now
I think it would probably be better if I didn’t need to regenerate the cache, but at least this is a working solution and if I don't get a better answer then I'll just accept this one.
I had some luck in solving this, but the reason remains obscure to me.
Basically, at the top of my render view after establishing the html() of my element, I call listview(). Then, any further items I might add to a list call listview('refresh').

Categories