Backbone.js with Handlebars.js not rendering options - javascript

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.

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>

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!

changing backbone views

I have a question about the way backbone handles it views.
Suppose I have the following code:
<div id="container">
<div id="header">
</div>
</div>
After this I change header into a backbone view.
How can I now remove that view from the header div again after I'm done with the view and add ANOTHER view to the same div?
I tried just overwriting the variable the view was stored in. This results in the view being changed to the new one...but it will have all the event handlers of the old one still attached to it.
Thanks in advance!
http://documentcloud.github.com/backbone/#View-setElement
This won't automatically remove the original div - you'll want to do that yourself somehow, but then by using setElement you'll have the view's element set to whatever you passed it.. and all of the events will be attached as appropriate. Then you'll need to append that element wherever it is that it needs to go.
--- Let's try this again ----
So, first thing to keep in mind is that views reference DOM elements.. they aren't super tightly bound. So, you can work directly with the jquery object under $el.
var containerView = new ContainerView();
var headerView = new HeaderView();
var anotherHeaderView = new AnotherHeaderView();
containerView.$el.append(headerView.$el);
containerView.$el.append(anotherHeaderView.$el);
anotherHeaderView.$el.detach();
containerView.$el.prepend(anotherHeaderView.$el);
Or you can create methods to control this for you.
var ContainerView = Backbone.View.extend({
addView: function (view) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.$el.append(el);
}
});
Maybe setting the views by view order?
var ContainerView = Backbone.View.extend({
initialize: function () {
this.types = {};
},
addView: function (view, type) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.types[type] = el;
this.resetViews();
},
removeView: function (type) {
delete this.types[type];
this.resetViews();
},
resetViews: function () {
this.$el.children().detach();
_.each(['main_header', 'sub_header', 'sub_sub_header'], function (typekey) {
if(this.types[typekey]) {
this.$el.append(this.types[typekey]);
}
}, this);
}
});

Backbone View Attribute set for next Instantiation?

I have a view that has a tooltip attribute. I want to set that attribute dynamically on initialize or render. However, when I set it, it appears on the next instantiation of that view instead of the current one:
var WorkoutSectionSlide = Parse.View.extend( {
tag : 'div',
className : 'sectionPreview',
attributes : {},
template : _.template(workoutSectionPreviewElement),
initialize : function() {
// this.setDetailsTooltip(); // doesn't work if run here either
},
setDetailsTooltip : function() {
// build details
...
// set tooltip
this.attributes['tooltip'] = details.join(', ');
},
render: function() {
this.setDetailsTooltip(); // applies to next WorkoutViewSlide
// build firstExercises images
var firstExercisesHTML = '';
for(key in this.model.workoutExerciseList.models) {
// stop after 3
if(key == 3)
break;
else
firstExercisesHTML += '<img src="' +
(this.model.workoutExerciseList.models[key].get("finalThumbnail") ?
this.model.workoutExerciseList.models[key].get("finalThumbnail").url : Exercise.SRC_NOIMAGE) + '" />';
}
// render the section slide
$(this.el).html(this.template({
workoutSection : this.model,
firstExercisesHTML : firstExercisesHTML,
WorkoutSection : WorkoutSection,
Exercise : Exercise
}));
return this;
}
});
Here is how I initialize the view:
// section preview
$('#sectionPreviews').append(
(new WorkoutSectionPreview({
model: that.workoutSections[that._renderWorkoutSectionIndex]
})).render().el
);
How can I dynamically set my attribute (tooltip) on the current view, and why is it affecting the next view?
Thanks
You can define attribute property as a function that returns object as result. So you're able to set your attributes dynamically.
var MyView = Backbone.View.extend({
model: MyModel,
tagName: 'article',
className: 'someClass',
attributes: function(){
return {
id: 'model-'+this.model.id,
someAttr: Math.random()
}
}
})
I hope it hepls.
I think your problem is right here:
var WorkoutSectionSlide = Parse.View.extend( {
tag : 'div',
className : 'sectionPreview',
attributes : {} // <----------------- This doesn't do what you think it does
Everything that you put in the .extend({...}) ends up in WorkoutSectionSlide.prototype, they aren't copied to the instances, they're shared by all instances through the prototype. The result in your case is that you have one attributes object that is shared by all WorkoutSectionSlides.
Furthermore, the view's attributes are only used while the the object is being constructed:
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
The _ensureElement call is the thing that uses attributes and you'll notice that it comes before initialize is called. That order combined with the prototype behavior is why your attribute shows up on the next instance of the view. The attributes is really meant for static properties, your this.$el.attr('tooltip', ...) solution is a good way to handle a dynamic attribute.

JS templating system with Backbone.js

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);
}
})

Categories