javascriptbackbone.jshandlebars.jsunderscore.js-templatingclient-side-templating

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,


Solution

  • This question is tagged with 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.