2

I have a model and a view. The view displays attributes of a model and allows the user to manipulate these attributes. The problem is that when an attribute is modified it re-renders the whole view which causes a lot of problems for me.

Example blur event on a text input saves the new input to an attribute and thus fires render. Which means that if the user clicked from that text input straight to a button on the same view that event will never fire as the first event that fires will be blur causing the whole view to re-render and thus losing the button click event.

I have two ideas:

  1. Have a single view where every attribute is in a separate template. Then I bind to a particular attribute change event and in render I update only the html of the changed attribute. This seems like a hack, as there is a lot of work to force the view to update only the changed attribute. It will add a lot of unnecessary complexity to an already complex view.
  2. Create a master view which consists of views, where each of them represents a model's attribute. This will create a lot of views, with nearly no functionality.

I seem to prefer the 2. option. What do you think? What are the best practices? Is there any better way to handle this?

AndraD
  • 2,830
  • 6
  • 38
  • 48
Ben
  • 2,435
  • 6
  • 43
  • 57
  • See http://stackoverflow.com/questions/13527569/remove-li-from-ul-backbone/13528152#13528152 for a similar issue – Pramod Jan 10 '13 at 07:36

2 Answers2

1

I think you can do this quite easily.

Take a step back and think about where you are binding your events. It seems that you are binding them directly on top of each individual element instead of using a parent delegate.

Here's an example

Backbone.View.extend({
  el: $("div.parent"),
  events: function() {
    this.$el.on("click", "input[type=button]", function(){});
    // jquery cross browser on this
    this.$el.on("blur", "input[type=text]", function(){});
  },
  initialize: function() {
    this.model.bind("change", this.render, this);
  },
  render: function() {
    this.$el.html('<input type="text" /><input type="button" />');
  }
});

Here's what el and it's structure looks like

<div class="parent">
  <input type="text" />
  <input type="button" />
</div>

So this.$el points to div.parent. I can constantly rerender the contents of this.$el, and as long as the html structure dosen't change, I don't have to worry about events getting unbound. The other solution is that if I really cannot do delegation, I would just call the events method whenever I render again.

adrian
  • 2,326
  • 2
  • 32
  • 48
1

Like you said yourself, both of your options seem very complex. But sometimes additionaly complexity is a necessary evil. However, if the updated fields are something relatively simple (like binding a value to an element or an input field), I would simply update the DOM elements without creating additional View/Template abstractions on top of them.

Say you have a model:

var person = new Person({ firstName: 'John', lastName: 'Lennon', instrument:'Guitar' });

And a view which renders the following template:

<div>First Name: <span class="firstName">{{firstName}}</span></div>
<div>Last Name: <span class="lastName">{{lastName}}</span></div>
<div>Instrument: <input class="instrument" value="{{instrument}}"></input></div>

You could declare in the view which property change should update which element, and bind the model change event to a function which updates them:

var PersonView = Backbone.View.extend({

  //convention: propertyName+"Changed"
  //specify handler as map of selector->method or a function.
  firstNameChanged:  { '.firstName': 'text' },
  lastNameChanged:   { '.lastName': 'text' },
  instrumentChanged: { '.instrument': 'val' },
  otherFieldChanged: function(val) { //do something else },

  initialize: function (opts) {
    this.model.on('change', this.update, this);
  },

  //called when change event is fired
  update: function(state) {
    _.each(state.changed, function(val, key) {
      var handler = this[key + "Changed"];
      //handler specified for property?
      if(handler) {
        //if its a function execute it
        if(_.isFunction(handler)) {
          handler(val);
        //if its an object assume it's a selector->method map
        } else if(_.isObject(handler)) {
          _.each(handler, function(prop, selector) {
            this.$(selector)[prop](val);
          }, this);              
        }
      }
    }, this);
  }

A solution like this doesn't scale to very complex views, because you have to add classed elements to the DOM and maintain them in the View code. But for simpler cases this might work quite well.

In addition it's always good to try to compose views of multiple, smaller views, if they naturally divide into sections. That way you can avoid the need to update single fields separately.

jevakallio
  • 35,324
  • 3
  • 105
  • 112