javascriptbackbone.jsbackbone-eventsbackbone-model

this.model.on change event not firing


The "change" event is not firing in the following code.

var PageView = Backbone.View.extend({
    el: $("body"),
    initialize: function(){
        this.model.on("change:loading", this.loader, this);
    },
    loader: function(){
        if(this.model.get("loading")){
            this.$el.find('.loader').fadeIn(700);
        }
        else 
            this.$el.find('.loader').fadeOut(700);
    },
});

var PageModel = Backbone.Model.extend({
    defaults: {
        loading: null,
    },
    initialize: function(){
        this.set({loading:false});
    },
});

$(function(){
    var pageModel = new PageModel({});
    var pageView = new PageView({model: pageModel});
})

It works if I'm adding this in the model's initialize function:

 setTimeout(function() {
     this.set({'loading': 'false'});
 }, 0);

I can leave it this way, but this is a bug.


Solution

  • The situation explained

    Here's the order the code runs:

    1. the model is created,
    2. model's initialize function is called, setting the loading attribute to false,
    3. then the model is passed to the view,
    4. then a listener is registered for the "change:loading"

    The event handler is never called because the event never occurs after it was registered.

    Quick fix

    First remove the set from the model.

    var PageModel = Backbone.Model.extend({
        defaults: {
            loading: null
        }
    });
    

    Then, after creating the view, set the loading attribute.

    var pageModel = new PageModel();
    var pageView = new PageView({ model: pageModel });
    
    pageModel.set('loading', false); // now the event should trigger
    

    Since the listener is now registered before the model's loading attribute is changed, the event handler will be called.

    Optimized solution

    Use Backbone's best practices:

    A view is an atomic component that should only care about itself and its sub-views.

    While in your case, it wouldn't matter much that you use the el property on the view, it still goes beyond the responsibilities of the view. Let the calling code deal with passing the element to use for this view.

    var PageView = Backbone.View.extend({
        initialize: function() {
            this.model = new PageModel();
            this.$loader = this.$('.loader');
            this.listenTo(this.model, "change:loading", this.loader);
        },
        loader: function() {
            this.$loader[this.model.get("loading")? 'fadeIn': 'fadeOut'](700);
        },
        render: function() {
            this.loader();
            return this;
        }
    });
    

    Put the defaults where they belong.

    var PageModel = Backbone.Model.extend({
        defaults: {
            loading: false
        }
    });
    

    Here we choose the body as the element to use for the view, using the el option, and then call render when ready.

    $(function() {
        var pageView = new PageView({ el: 'body' }).render();
    });
    

    The event won't be triggered by the listener right away, instead, we use the render function to put the view in its default state. Then, any subsequent changes of the loading attribute will trigger the callback.


    I have listed the most useful answers I've written about Backbone on my profile page. You should take a look, it goes from the beginning to advanced and even provides some clever Backbone components that solves common problems (like detecting a click outside a view).