25

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.

fguillen
  • 36,125
  • 23
  • 149
  • 210
  • 4
    What about rendering the full template and using setElement? – nikoshr Jul 21 '12 at 18:55
  • @nikoshr, please elaborate a minimal answer I'll accept it. It is working in my experiment. The only problem is that I have to use the `setElement()` into the `render()` method after the _template_ has been rendered. – fguillen Jul 21 '12 at 19:06
  • Ok, I'll post an answer as soon as I get my hands on a real keyboard – nikoshr Jul 21 '12 at 19:12
  • a pre +1 for @nikoshr. I have used that method before when there are a non trivial amount of attributes on the outer element. If it were a simple element with a class (most common case), I would prefer to use/set the el and className props. – Dennis Burton Jul 21 '12 at 20:56
  • What's the problem with wrapping your template in an empty DIV? Then you can use Backbone properly, while still maintaining your full contained template – bluedevil2k Jul 21 '12 at 19:19
  • The DOM structure changes completely what affects to CSS selectors. And the most important: I feel ill seeing all these _zombie_ div elements around. Also in my last special case I'm working with _draggable_ elements and the extra `divs` are adding weird behavior. – fguillen Jul 21 '12 at 19:22
  • http://stackoverflow.com/questions/14659597/backbonejs-view-self-template-replacewith-and-events – Hontoni Feb 07 '13 at 16:08

3 Answers3

42

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:

  1. setElement calls undelegateEvents, taking care of the view events, but be careful to remove all other events you might have set yourself.
  2. 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/

nikoshr
  • 32,926
  • 33
  • 91
  • 105
  • 1
    and what if the template does not have a main element? http://jsfiddle.net/Antonimo/vrQzF/5/ – Hontoni Feb 07 '13 at 16:11
  • I was so sure this would fix it for me but it didnt. What did though is using `insertAfter` instead of `replaceWith`. Using it like so: `this.render().$el.insertAfter($oldel);` will do exactly as you would expect from replaceWith but also preserving all events (so long as you do something like `this.$el.html(this.template(this.model.toJSON()));` inside `render()` – parliament Aug 23 '13 at 02:41
  • That's the right way to render (specifically dynamic list items) as it makes the render method idempotent. – Emile Bergeron Jun 30 '16 at 15:13
  • Note that `setElement` can take a html string directly and it takes care of making a jQuery object out of it. – Emile Bergeron Jun 30 '16 at 15:14
  • 1
    @Hontoni If your template doesn't have a root element, that's defying the point here. You should just use the one created by Backbone (the default or overriden `tagName`) or encapsulate your template inside a main element. Backbone views *must* be a unique element and can't be arrays of elements. – Emile Bergeron Jun 30 '16 at 15:20
0

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

JVitela
  • 2,472
  • 2
  • 22
  • 20
  • That's hackish at best, and relies on stripping the first tag of the template by only appending its children... – Emile Bergeron Jun 30 '16 at 13:23
  • @EmileBergeron Why hackish? my solution is not defining any code, just a convention that you must implement however fits you best. The code in the example is not meant to be a universal solution. In the other hand I believe using "setElement" in side the "render" method will hurt performance. – JVitela Jul 11 '16 at 14:22
  • It's just a worse version of `this.setElement(this.template)`. I can't think of a situation where this solution could be better and/or more efficient. – Emile Bergeron Jul 11 '16 at 14:41
  • You have not understand it at all. The hole point is not to use `setElement` inside the render method because this is rewiring all the event listeners! The render method in my example is just replacing the view contents. – JVitela Jul 11 '16 at 15:15
  • `setElement` takes care of undelegating and re-delagating DOM events ([annotated source](http://backbonejs.org/docs/backbone.html#section-162)). [Here](http://jsbin.com/jevetot/3/edit?html,js,output)'s your modified example, it's all it takes with `setElement`. – Emile Bergeron Jul 11 '16 at 16:05
  • I even added a new button to re-render and prove that rendering is idempotent with setElement. – Emile Bergeron Jul 11 '16 at 16:07
  • Exactly as you said `setElement` is undelegating and re-delagating DOM events in each call to `render` which is not ideal for performance. [here](http://jsbin.com/femihu/2/edit?html,js,output) you have both versions and you can see the difference is considerable. – JVitela Jul 12 '16 at 08:07
  • The test is a little irrelevant since we rarely re-render a view a 1000 times, but this **proved you right** in that `setElement` is a little slower. But that difference becomes even more irrelevant when rendering a 1000 **new** views, as in a list, which is a more practical use-case. If you're after performance, I think I would still avoid this technique and just use what backbone is providing (setting the _ugly_ `tagName`, `attributes`, `className`). And other than that, the regex could fail since you're not parsing HTML, but a mustache template string. – Emile Bergeron Jul 12 '16 at 13:59
  • I like that you took the time to prove your point with nice tests! – Emile Bergeron Jul 12 '16 at 13:59
0

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.

Community
  • 1
  • 1
hashchange
  • 7,029
  • 1
  • 45
  • 41