SHT
SHT

Reputation: 720

BackboneJS + HandlebarsJS - How to avoid HTML to be rendered multiple times

I have a Backbone application where I trigger a View through an event-click and the Views HTML gets rendered with HandlebarsJS and displays data from a collection. So far it works except that my HTML gets repeated for each entry

My HTML looks like this:

<header>
   <span class="blackdot" aria-hidden="true" />
   <h1>Overview</h1>
</header>
<div>
   <ul>
      <li>
        <a href="#">{{brand}}</a>
        <p>{{country}}</p>
    </li> 
  </ul>
</div>

Right now, it duplicates the whole HTML code-block, including <header> and <h1>-tag for each entry but what I want to achieve is that my HTML would look like this:

<header>
   <span class="blackdot" aria-hidden="true" />
   <h1>Overview</h1>
</header>
<div>
   <ul>
      <li>
        <a href="#">Audi</a>
        <p>Germany</p>
     </li>
      <li>
        <a href="#">Hyundai</a>
        <p>South Korea</p>
     </li>
      <li>
        <a href="#">Fiat</a>
        <p>Italy</p>
     </li>  
  </ul>
</div>

My backbone View looks like this:

 define(['backbone','handlebars', 'text!templates/Cars.html'],

    function(Backbone,Handlebars, Template) {

        'use strict';

        var CarsView = Backbone.View.extend({

            template: Handlebars.compile(Template),

            events: {
            },

            initialize: function () {
            _.bindAll(this, 'render');
            },

            render: function() {
               var self = this;
               self.collection.each(function(model){
                    self.$el.append(self.template({
                        brand:model.get('brand'),
                        country:model.get('country')
                        })
                    );
               });

                return this;
            }

        });

        return CarsView;

    }
 );

Of course i could define the most of the HTML on the index page, wrap it in a DIV and do display:none and only write the <li>-tag in the Handlebars HTML template, but I want to avoid that and since the data what is returned are strings, I cant do the {{#each}}-thing... so, is there any solution for this?

Upvotes: 0

Views: 262

Answers (1)

Josh Sullivan
Josh Sullivan

Reputation: 1176

To add onto what guzmonne said, the reason you are seeing this is because you are looping over the entire template. What you should be doing is taking the <li>'s and making a new template specifically for them. I've modified your existing code to show how something like what you are trying to do can be accomplished.

The CarsView Handlebars template:

<header>
    <span class="blackdot" aria-hidden="true" />
<h1>Overview</h1>
</header>
<div>
    <ul id="cars_list" />
</div>

The CarsView Backbone View:

var CarsView = Backbone.View.extend({

    template: Handlebars.compile(Template),

    events: {},

    initialize: function () {
        _.bindAll(this, 'render');
    },

    render: function() {
        this.$el.html(this.template()); // this now only renders once
        this.addAll();
        return this;
    },

    addOne: function(car) {
        var view = new CarView({model: car});
        this.$("#cars_list").append(view.render().el);
    },

    addAll: function() {
        this.collection.each(this.addOne, this);
    }
});

The main differences between what I have done and your original CarsView is that firstly, the template itself doesn't contain any <li>'s. Instead, there is a placeholder ID which I have titled "cars_list". This container will give us an entry point to loop through the cars collection and add each item. Secondly, we aren't looping through the collection and re-rendering the CarsView. Instead, we take what CarsView dropped into the DOM and manually apppend to it from there.

Normally when dealing with collections, you can leverage Backbone's this.listenTo() function, which can take an event such as "reset" or "add" and tie it to a function. Since it would appear that the collection has already been fetched, we simply do this.addAll() after the CarsView template has been rendered. It is in here that the collection is looped and added.

To accomplish this, you will need another Backbone view, and another template...

The CarView Handlebars template:

<a href="#">{{brand}}</a>
<p>{{country}}</p>

The CarView Backbone View:

var CarView = Backbone.View.extend({
    tagName:  "li",

    template: Handlebars.compile(Template),

    events: {},

    initialize: function () {
        // model event listeners (in case these list items can be edited/removed)
        this.listenTo(this.model, 'change', this.render);
        this.listenTo(this.model, 'destroy', this.remove);
    },

    render: function() {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
    }
});

The CarView view is short and simple, but takes care of a lot for you. As you can see, this view will generate an <li> tag and take the contents of the model and send it to the handlebars template. No more having to deal with manually fetching model attributes, which is an added bonus.

Upvotes: 1

Related Questions