Search code examples
javascriptbackbone.jsbackbone-viewsbackbone-eventsbackbone-stickit

(Re)rendering Backbone view in change event handler does not work


I'm having two form elements, both 2-way-databinded via backbone.stickit. The second form element (#input) is just cosmetics - there for showing it's actually working.

The idea is that my View gets (re)rendered,every time the option inside the dropdown (#select) menu gets changed.

I'm trying to achieve that by catching the the 'changed' event of #select and call this.render() to (re)render the view.

Apparently that doesn't work. The selected option doesn't get saved back into the model and I fail to understand why.

I'm not looking for a solution, rather than an explanation, why the following code doesn't work. The solution (as in: works for me) is part of the fiddle - commented out.

HTML:

<script type="text/template" id="tpl">
  <h1>Hello <%= select %></h1>
  <select id="select">
  </select>
  <p>Select:
    <%= select %>
  </p>
  <hr>
  <input type="text" id="input">
  <p>Input:
    <%= input %>
  </p>
</script>

<div id="ctr"></div>

JavaScript:

Foo = Backbone.Model.extend({
  defaults: {
    select: "",
    input: "",
  }
});
FooView = Backbone.View.extend({
  el: '#ctr',
  template: _.template($('#tpl').html()),
  initialize() {
    this.model.bind('change', function() {
      console.log("model change:");
      console.log(this.model.get('select'));
      console.log(this.model.get('input'));
    }, this);
    //this.model.bind('change:select', function() { this.render(); }, this); // <--------------------- WORKS
  },
  render: function() {
    this.$el.html(this.template(this.model.toJSON()));
    this.stickit();
    return this;
  },
  events: {
    'change #select': function(ev) {
      console.log('change event triggered:');
      console.log(this.model.get('select'));
      console.log(this.model.get('input'));
      this.render(); // <--------------------- DOES NOT WORK - WHY?
    },
    /* 'click #render': function(ev) {
      console.log('render event triggered:');
      console.log(this.model.get('select'));
      console.log(this.model.get('input'));
      this.render();
    } */
  },
  bindings: {
    '#input': 'input',
    '#select': {
      observe: 'select',
      selectOptions: {
        collection: function() {
          return [{
            value: '1',
            label: 'Foo'
          }, {
            value: '2',
            label: 'Bar'
          }, {
            value: '3',
            label: 'Blub'
          }]
        }
      }
    },
  },
});
new FooView({
  model: new Foo()
}).render();

https://jsfiddle.net/r7vL9u07/9/


Solution

  • The reason it does not work to call this.render() from within your change #select event handler is because you are disrupting the two-way data binding that Backbone.stickit is providing you. The flow goes something like the following:

    • User changes the value of '#select'.
    • Your change #select handler fires and calls this.render().
    • render repopulates #ctr with a new select menu with no selected option.
    • Backbone.stickit responds to the change to #select.
    • Backbone.stickit tries to obtain the value of #select, but since it contains no selected option the value is undefined.
    • Backbone.sticket sets the model's select attribute to undefined.

    The reason it works if you move the this.render() call to within the model's change:select handler is because Backbone.stickit is able to correctly update the model without the DOM changing before it gets the chance.