3

Given a model:

MyModel = Backbone.Model.extend({
  defaults: {
    name: '',
    age: -1,
    height: '',
    description: ''
  }
});

and a View to render the model as a list:

MyView  = Backbone.View.extend({
  tagName: 'ul',
  className: 'MyView',

  render() {
    var values = {
      name: this.model.get('name'),
      age: this.model.get('age'),
      height: this.model.get('height'),
      description: this.model.get('description')
    }

    var myTemplate = $('#MyView-Template').html();
    var templateWithValues = _.template(myTemplate , values);
  }
});

and a template loaded by the View:

<script type="text/template" id="MyView-Template">  
  <li class="name"><%= name %></li>
  <li class="age"><%= age %></li>
  <li class="name"><%= height%></li>
  <li class="name"><%= description%></li>
</script>

everything works fine, although it is a contrived example, the real code has many, many more attributes in the model. The problem I'm experiencing is how to handle updates to the model.

I create an HTML form which has an appropriate input element for each field. The form is modelled and loaded as a template:

<script type="text/template" id="MyEditView-Template">  
  <input type"text" value="<%= name %>" /> <br />
  <input type"text" value="<%= age%>" /> <br />
  <input type"text" value="<%= height%>" /> <br />
  <input type"text" value="<%= description%>" /> 
</script>

and loaded into a view:

MyEditView  = Backbone.View.extend({
      tagName: 'form',
      className: 'MyEditView',

      render() {
        var values = {
          name: this.model.get('name'),
          age: this.model.get('age'),
          height: this.model.get('height'),
          description: this.model.get('description')
        }

        var myTemplate = $('#MyEditView-Template').html();
        var templateWithValues = _.template(myTemplate , values);
      }
    });

When the user saves the form, the new values are set in the model (MyModel). However I do not want to re-render the entire original view, it takes too long and has many nested elements. I only want to update the HTML elements which have had their value changed in the model.

The problem is how can I elegantly link a model's attributes to HTML elements, so that I can do the following to an already rendered view:

  1. Iterate over a model's attributes.
  2. Determine which attributes have been modified.
  3. Only update the UI for modified attributes.
  4. Hide UI for any previously rendered attributes which should no longer be shown.

A the moment I have a rather ugly solution of a JavaScript lookup table (just an object) which maps an attribute name onto an HTML element string:

var AttributesMap = {
    name: {
        htmlRef: 'li.name',
        attributeName: 'name'
    },
    age: {
        htmlRef: 'li.age',
        attributeName: 'age'
    }
    ...
}

This feels hacky and has resulted in some pretty bloated code.

Jack
  • 10,313
  • 15
  • 75
  • 118
  • 3
    use `.toJSON` instead of creating your `values` collection manually: `this.model.toJSON()` returns the same structure that you've been creating by hand. – Derick Bailey May 06 '12 at 23:15

3 Answers3

6

there is actually two questions hidden in your post. You have problem with attributes of the model and you are not aware if how to subscribe to model change events. Luckily both of these are possible easily with backbone.js. Change your view code to below

1

render: function () {
    var model = this.model;
    $(this.el).empty().html(_.template(this.template, this.model.toJSON()))
    return this;
}

where el is property of the view that defines the container. toJSON() is a method you can invoke on the model to serialize it format that can be transferred over the wire.

2

Views are supposed to subscribe to model change events in their initialize function or more aptly use the delegated event support. When ever a model attribute changes a change event is invoked on that model which you can subscribe to like here and example below.

window.ListView = Backbone.View.extend({
    initialize: function () {
        //pass model:your_model when creating instance
        this.model.on("change:name", this.updateName, this);
        this.model.on("change:age", this.changedAge, this);
    },
    render: function () {
        var model = this.model;
        $(this.el).empty().html(_.template(this.template, this.model.toJSON()))
        return this;
    },
    updateName: function () {
        alert("Models name has been changed");
    },
    changedAge: function () {
        alert("Models age has been changed");
    }
});

JsBin example

http://jsbin.com/exuvum/2

Deeptechtons
  • 10,945
  • 27
  • 96
  • 178
  • The problem with part 1. of your answer is that the values may be undefined, in which case I don't want the template to render a UI element for that attribute at all. Loading in all the attributes using toJSON() doesn't solve that problem. It's still just a dump of attributes. – Jack May 07 '12 at 08:30
  • With part 2, the methods which handle the update for a specific attribute still need to 'find' their respective HTML element in the DOM and update it. Although part 2 does help me out and will clean up my code! Thank you. – Jack May 07 '12 at 08:33
  • @Jack You are required to override `defaults:{}` to defeat the comment #1. for comment #2 Offcouse you wanted to update individual attributes to UI element right ? if not you just need to subscribe to `change` event without the attribute name hook render function as the callback for this – Deeptechtons May 07 '12 at 08:38
  • So if an attribute has a value which matches that defined in `defaults{}` - it will not be included in the object returned from `toJSON()`? – Jack May 07 '12 at 08:43
  • @Jack Hi sorry for the delay, i have added a example to jsbin for you to learn. Do note that empty strings needs to be validated through `validate` method. If not specified `defaults` are taken – Deeptechtons May 07 '12 at 09:44
  • Your example actually demonstrates the problem I'm having. It still shows the UI element for 'name' even if there is no name, hence the extra `,`. This is the problem I'm having. How can I elegantly load values into a template, but if some values are empty, or undefined, or whatever, then don't how that UI element. – Jack May 07 '12 at 13:46
  • @Jack every model provides `validate` method that needs overriding for validating the models state in your example check if name is empty,name has invalid characters. this method will be invoked when you call `Set` or `Save` on model( you certainly will save them won't you)? to say simply the model displayed in the jsbin example is not valid. It is not the work of view to fill empty values or validate the state, validations should be done on model only. If more help is need please reply back – Deeptechtons May 08 '12 at 06:00
  • But there must be a way for the View to adjust appropriately depending on the values in the model....for example, if the value is not valid, the view should not attempt to display that value? – Jack May 08 '12 at 16:58
  • @Jack **It is not the work of view to determine what model should look like** write that down somewhere if you remember that piece developing with backbone is easy. I made another fiddle to demonstrate how it is done note the `initialize` method of the model http://jsbin.com/exuvum/3/edit#javascript,html,live – Deeptechtons May 09 '12 at 04:22
1

I faced a similar problem where I wanted to have a template only show fields for which there was data. I approached the problem from the underscore template side because <%= undefinedKey %> throws an exception. The solution, for me, was to pass a wrapper object to the template that contains the model data. The wrapping of model data looks like:

this.$el.html(this.template({my_data: this.model.toJSON()}));

Your template checks for existence of the desired property:

<% if(my_data.phone) { %><p><%= my_data.phone %> </p><% } %>

You can automatically render the entire view each time the model changes. Using this method new values will appear and deleted values will disappear from the UI.

Some further information related to your requirements:

Iterate over a model's attributes. Determine which attributes have been modified.

If you want to know what attributes have changed since the last time a model's "change" event was fired you can use the Backbone Model's changedAttributes method.

Only update the UI for modified attributes. Hide UI for any previously rendered attributes which should no longer be shown.

In lieu of rendering the entire view for each attribute change that occurs you could surgically update only the portions of the UI that have changed attributes by having each UI field be a discrete Backbone View. All Views would be listening to a shared model for the change event of a specific model attribute:

    this.model.on("change:phone", this.render, this);
0

Well, Backbone use event delegation and one doesn't need re-subscribe for element events after replacing this.el content. But you destroy the DOM-subtree with every template synchronization, meaning you losing the state of your form. Just try subscribing your model for input/change events. So user types in input elements and form validates. Restoring the state of controls (let's say input[type=file]) would be challenging and resource-hungry.

I believe the best way is to go with a DOM-based template engine, that updates only target elements where it is necessary. E.g. mine is https://github.com/dsheiko/ng-template

You can have a template like this:

<form id="heroForm" novalidate>
  <div class="form-group">
    <label for="name">Name</label>
    <input id="name" type="text" class="form-control" required >
    <div class="alert alert-danger" data-ng-if="!name.valid">
      Name is required
    </div>
  </div>
  <div class="form-group">
    <label for="power">Hero Power</label>
    <select id="power" class="form-control"  required>
      <option data-ng-for="let p of powers" data-ng-text="p" >Nothing here</option>
    </select>
    <div class="alert alert-danger" data-ng-if="!power.valid">
      Power is required
    </div>
  </div>
   <button type="submit" class="btn btn-default" data-ng-prop="'disabled', !form.valid">Submit</button>
</form>

Here we bound models name, power and form. Whenever their states change (e.g. while user is typing), the template reacts. It may hide/show error messages, or disable/enable submit button.

If it's interesting - how to bundle it with Backbone, here is a little free online book https://dsheiko.gitbooks.io/ng-backbone/

Dmitry Sheiko
  • 2,130
  • 1
  • 25
  • 28