Backbone, not "this.el" wrapping - backbone.js

I do an extensive use of templates, and I like to use full contained templates. I mean that I want to see in the template code all the DOM elements including the root one, like this:
<script type="text/template" id="template-card">
<div class="card box" id="card-<%= id %>">
<h2><%= title %></h2>
<div><%= name %></div>
</div>
</script>
But what Backbone likes is having a template like this:
<script type="text/template" id="template-card">
<h2><%= title %></h2>
<div><%= name %></div>
</script>
And defining the root element and its attributes in the JS code. What I think is ugly and confusing.
So, any good way to avoiding my Backbone View to wrapper my template with an extra DOM element?
I have been checking this issue thread: https://github.com/documentcloud/backbone/issues/546 and I understand there is not any official way to do it.. but maybe you can recommend me a non official way.

You can take advantage of view.setElement to render a complete template and use it as the view element.
setElement view.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use setElement, which will
also create the cached $el reference and move the view's delegated
events from the old element to the new one
Two points you have to account for:
setElement calls undelegateEvents, taking care of the view events, but be careful to remove all other events you might have set yourself.
setElement doesn't inject the element into the DOM, you have to handle that yourself.
That said, your view could look like this
var FullTemplateView = Backbone.View.extend({
render: function () {
var html, $oldel = this.$el, $newel;
html = /**however you build your html : by a template, hardcoded, ... **/;
$newel = $(html);
// rebind and replace the element in the view
this.setElement($newel);
// reinject the element in the DOM
$oldel.replaceWith($newel);
return this;
}
});
And a working example to play with http://jsfiddle.net/gNBLV/7/

Now you can also define a view's tagName as a function and create a class like this:
var MyView = Backbone.View.extend({
template: '#my-template',
tagName: function() {
// inspect the template to retrieve the tag name
},
render: function() {
// render the template and append its contents to the current element
}
});
Here's a working example

Backbone.Decarative.Views provides you with an alternative way to do this, without having to rely on setElement. For more, check out my answer here.

Related

Backbone events are not working

For some reason I don't know why my event in a Backbone View doesn't work.
I tried to Google for some answer but I didn't find anything that would help me.
Basically, my code is this:
Backbone:
var ViniView = Backbone.View.extend({
el: $('.container'),
events: {
"click .clickme" : "render"
},
render: function() {
alert("please, work");
}
});
new ViniView;
HTML
<div class="container">
<button class="clickme">
test
</button>
</div>
Your example works fine for me in this fiddle.
As explunit noted, though, your el should reference an element and should not be a jQuery object. $el takes care of that. According to the docs:
All views have a DOM element at all times (the el property), whether they've already been inserted into the page or not.
Check that you're correctly loading the Jquery, Underscore and Backbone scripts (in that order). Also make sure you're script is being executed once the page is ready and not, say, before your DOM has finished loading (causing your view to not attach to anything).

How to set .el to div which is in underscore.js template?

It is pretty basic thing but I still can't find the answer. I need to set View's el: to div which is in underscore.js template.
<script id="product" type="text/template">
<div class="productInfo"></div>
<div class="prices"></div>
<div class="photos"></div>
</script>
The first view renders the product template. I need to render other Views to divs in this template. I don't know how to set el:, because el: '.prices' just don't work with divs in template.
This Views structure is similar to How to handle initializing and rendering subviews in Backbone.js?. But I use template instead of rendering to existing divs.
So the problem is using a CSS selector string for this.el won't find anything if the matching <div> is not attached to the page's DOM. In the case of you <script> tag template, the contents of the <script> tag are not attached DOM nodes, there are just a single text node.
One option given your HTML would be to just forget about the <script> tag and put your empty <div> tags straight into the HTML. They are empty and thus should be harmless and invisible until you actual render some content within them. If you do that, el: '.productInfo:first' should work fine.
Other than that, you'll need to put logic into your parent view along these lines:
Render the template into a detached DOM node
Search that detached DOM node for subview divs
Map the subview div to the corresponding backbone view subclass
instantiate and render the subview, then use something like this.$el.find('.productInfo').replaceWith(productInfoView.el) to put the rendered HTML into the parent view at the right location
My general comment is that views should render to detached DOM nodes and leave it to other components such as the router or layout managers to decide where in the real DOM they get attached. I think this makes the views more reusable and testable.
Render the parent view, then assign the '.prices' as the el of the child:
var ParentView = Backbone.View.extend({
render : function() {
var html='<h1>Parent View</h1><div class="productInfo"></div><div class="prices"></div><div class="photos"></div>';
this.$el.html(html);
var prices = new PricesView({
el : this.$('.prices')
});
prices.render();
}
});
var PricesView = Backbone.View.extend({
render : function() {
var html='<h2>Prices View</h1>';
this.$el.html(html);
}
});
// Create a parent view instance and render it
var parent = new ParentView({
el : $('.parent')
});
parent.render();
​
Working example on jsfiddle: http://jsfiddle.net/JpeJs/
from version 0.9.9, backbone alaws you to define the el property of a view as a function :
When declaring a View, options, el and tagName may now be defined as functions, if you want their values to be determined at runtime.
Assuming that the subviews are created after the main view, this might help you if your subviews are aware of their parent :
SubView = Backbone.View.extend({
el : function(){
return this.parent.$('.prices');
}
});
var subViewInstance = new SubView({parent : theParentView});

Backbone Underscore Template

So I'm checking out the changes related to the latest backbone/underscore version. Prior I have a project running with BB 0.5.2 and underscore 1.1.7. I'm noticing something strange with regards to defining a template property within a view in the new version, which gives me reservations in going forward with upgrading.
In my current version I would define a view as such:
var MyView = Backbone.View.extend({
template: _.template($('#exampleTemplate').html()),
initialize: function() {...},
render: function() { $(this.el).html(this.template(someObjectParam)); },
});
However, if I attempt to work in the same manner, using a simplified todo clone attempt as an example, I setup an html with an inline script template as such:
<script>
$(document).ready(function() {
app.init();
});
</script>
<script type="text/template" id="itemViewTemplate">
<div class="item">
<input type="checkbox" name="isComplete" value="<%= item.value %>"/>
<span class="description"><%= item.description %></span>
</div>
</script>
In my included JS file I have:
var ItemView = Backbone.View.extend({
el: 'body',
// Below causes error in underscore template, as the jquery object .html() call
// returns null. Commenting will allow script to work as expected.
templateProp: _.template($('#itemViewTemplate').html()),
initialize: function() {
// Same call to retrieve template html, returns expected template content.
console.log($('#itemViewTemplate').html());
// Defining view template property inside here and calling it,
// properly renders.
this.template = _.template($('#itemViewTemplate').html());
this.$el.html(this.template({item: {value: 1, description: 'Something'}}));
},
});
var app = {
init: function() {
console.log('Fire up the app');
var itemView = new ItemView();
}
}
So I'm left confused as to why defining the template property directly causes the call to retrieve the template html to return a null value, thus breaking the attempt to define an underscore template object (mouthful). However, if the definition is done within the initialize function, the call to retrieve the template html properly finds the template so its contents can be passed to the underscore template. Anyone see what I'm potentially missing?
Thanks in advance!
If this:
var ItemView = Backbone.View.extend({
//...
templateProp: _.template($('#itemViewTemplate').html()),
//...
});
is failing because $('#itemViewTemplate').html() is null, then you have a simple timing problem: you're trying to read the content of #itemViewTemplate before it exists. Your old version should suffer from exactly the same problem.
Either make sure everything is loaded in the right order (i.e. your views after your template <script>s) or compile the template in your view's initialize. You can check for the templateProp in your view's prototype and only compile it on first use if you want:
initialize: function() {
if(!this.constructor.prototype.template)
this.constructor.prototype.template = _.template($('#itemViewTemplate').html());
//...
}
Demo: http://jsfiddle.net/ambiguous/HmP8U/

underscore template is removed from the DOM so can't be re-used

I'm trying to use a simple inline underscore.js template with backbone, and pulling the template from a <script> tag via jQuery's html() method.
The page is supposed to render multiple Recipe callouts for each returned by the Recipe Model. It works on the first thumbnail, but on the second, it seems the <script> tag has been removed from the DOM, so underscore fails to render it and bails with str is null on line 913
For the template, I have
<script type="text/html" id="user-recipe-rated-template">
<div class="user-recipe-rated">
<a class="thumb" href="<%= href %>"></a>
<p><%= title %></p>
</div>
</script>
And the backbone view looks like:
var UserRecipeView = Backbone.View.extend({
initialize : function() {
this.template = _.template($("#user-recipe-rated-template").html());
},
render : function()
{
this.el = this.template({
title: this.model.get("Title"),
href: '#'+this.model.get('UserContentId'),
image_src: this.model.get('ThumbnailSrc')
});
return this;
}
});
So, what I am seeing is that $("#user-recipe-rated-template") exists on the first thumbnail and all is well. On the second, it returns an empty array and underscore can't proceed.
I've trying memoizing the string of html and such, and that might work, but it seems there should be a cleaner way of doing this. What am I doing wrong?
(I'd like to use templates inlined as opposed to external JST for now to keep things simple - it seems nicer to have the template embedded in the page near where it will be loaded when the data comes in)

Backbone.js turning off wrap by div in render

I have model Post and collection Posts. And want to make form with list of all post in <select id="multi" multiple="multiple">. So i have to make a PostView render inside my #multi with just this template:
<option value=""><%= title %></option>
But finally I get it wrapped with div. Is there any solution for not wrapping this template with <div>?
If you don't define an el (or tagName) for the view (in the class or during instantiation) the view will be placed inside a div tag. http://documentcloud.github.com/backbone/#View-el
var PostView = Backbone.View.extend({
tagName: 'option'
});
UPDATE
Starting v0.9.0, Backbone has view.setElement(element) to get this done.
var PostView = Backbone.View.extend({
initialize: function() {
var template = _.template('<option value=""><%= title %></option>');
var html = template({title: 'post'});
this.setElement(html);
}
});
If you don't want to have the view wrap your HTML, you'll have to do a few things:
Replace this.el entirely
Call delegateEvents on the new el
render: function(){
var html = "some foo";
this.el = html;
this.delegateEvents(this.events);
}
Since Backbone generates a div or other tag (based on your tagName setting for the view), you have to replace it entirely. That's easy to do. When you do that, though, you lose your declared events because Backbone uses jQuery's delegate under the hood to wire them up. To re-enable your declared events, call delegateEvents and pass in your events declarations.
The result is that your view.el will be the <option> tag that you want, and nothing more.
In version 0.9.0, Backbone introduced view.setElement(element) to handle this operation.

Resources