Search code examples
javascriptjquerybackbone.jsbackbone-viewsbackbone-events

Backbone view events do not fire


I have a simple backbone view as follows:

/**
 * Renders a form view for an event object.
 */
APP.EventFormView = Backbone.View.extend({
    tagName: 'form',

    events: {
        'keydown': 'keyPressed',
        'focus input': 'inputChanged',
        'change select': 'selectChanged',
        'change textarea': 'textareaChanged'
    },

    initialize: function() {
        this.template = _.template($('#newevent-form').html());
        this.listenTo(this.model, 'change', this.render);
        this.listenTo(APP.eventTypes, 'update', this.render);
        this.listenTo(APP.selectedEvent, 'update', this.render);
    },

    render: function() {
        var modelJSON = this.model.toJSON();
        if ('id' in modelJSON && modelJSON.id !== "") {
            this.loadForm();
        } else if (!('id' in modelJSON) || modelJSON.id === "") {
            this.loadForm();
        } else {
            this.$el.html('');
        }
        return this;
    },

    loadForm: function() {
        var templateData = $.extend(this.model.toJSON(),
            {"event_types":APP.eventTypes.toJSON()});
        this.$el.html('');
        this.$el.html(this.template($.extend(this.model.toJSON(),
                {event_types: APP.eventTypes.toJSON()})));
        $('.ev-main-container').html('').html(this.el);
    },

    inputChanged: function(e) {
        console.log('inputChanged');
    },

    selectChanged: function(e) {
        console.log('selectChanged');
    },

    textareaChanged: function(e) {
        console.log('textareaChanged');
    },

    keyPressed: function(e) {
        console.log('key pressed');
    }
});

I initialize this view as follows under document.ready:

// Initialize the form view
APP.selectedEvent = APP.selectedEvent || new APP.Event();
APP.eventFormView = new APP.EventFormView({model: APP.selectedEvent});
APP.eventFormView.render();

But none of the events I have defined are firing for some reason, What is it that I am doing wrong here ?

Update:

Ok, I fugred out if i remove $('.ev-main-container').html('').html(this.el); from the loadForm method and instead intialize the view as follows, it works:

APP.eventFormView = new APP.EventFormView({
    model: APP.selectedEvent,
    el: $('.ev-main-container'),
});

I was able to resolve it but I still don't understand why this happens, could anyone throw a little light on what's going on and how this works.


Solution

  • jQuery's html function has a side effect that many people seem to forget about, from the fine manual:

    jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.

    Consider what that means when you do something like this:

    container.html(view.el);
    container.html(view.el);
    

    Everything will be fine after the first container.html() call. But the second will "remove ... event handlers from child elements" (such as view.el) before adding the new content. So after the second container.html() call, all the events on view.el are gone. Sound familiar?

    You have lots of things that will call render on your view and render will eventually do this:

    $('.ev-main-container').html('').html(this.el);
    

    Your events will silently disappear the second time that gets called but the HTML will look just fine.

    Consider this simplified example (http://jsfiddle.net/ambiguous/otnyv93e/):

    var V = Backbone.View.extend({
        tagName: 'form',
        events: {
            'click button': 'clicked'
        },
        initialize: function() {
            this.template = _.template($('#t').html());
        },
        render: function() {
            this.$el.html('');
            this.$el.html(this.template());
            $('.ev-main-container').html('').html(this.el);
            return this;
        },
        clicked: function() {
            console.log('clicked');
        }
    });
    var v = new V;
    v.render();
    $('#re-render').click(function() {
        v.render();
        console.log('Re-rendered');
    });
    

    and you'll see exactly your problem.

    If you make the view's el the .ev-main-container then you'll be using html() to alter the contents of el rather than altering the contents of the element that contains el. Once you're working entirely inside the el you're no longer accidentally re-using an element and no longer accidentally removing the event bindings from that element.

    My rules of thumb for preventing event problems with Backbone:

    1. Never attach views to existing DOM nodes, always let views create and own their own el and let the caller put that el in a container.
    2. Call remove on views to dispose of them when they're no longer needed.
    3. Don't try to re-use views, create them when you need them and remove them when you don't need them.
    4. No view references anything outside its el.

    There are exceptions (of course) and this approach won't solve everything but it is a good starting point and avoids most of the common problems.