Search code examples
javascriptbackbone.jsbackbone-events

Backbone events triggering on phantom views


Scenario

I have inherited older backbone app with master-details scenario. On master view I have list of items (project) and when I click on an item I can edit it on details view (ProjectFormView).

The problem

When I edit the project on ProjectFormView, all the previously opened project are edited with the same values as well.

Details:

I've discovered, that the UI events like input change are triggered also on previously opened ProjectFormViews, so it look like some kind of memory leak.

This is how the view is instantiated:

displayProject: function(appType, appId, projectId) {
    if (this.applicationDetailsModel === undefined || 
        this.applicationDetailsModel.get('formType') !== appType || 
        this.applicationDetailsModel.get('id') !== appId)
    {
        this.navigateTo = 'projects';
        this.navigateToItem = projectId;
        this.navigate('form/' + appType + '/' + appId, { trigger: true });
        return;
    }
    var that = this;
    require(['views/projectFormView'], function(ProjectFormView) {
        var tooltips = that.tooltipsCollection
            .findWhere({ form: 'project' })
            .get('fields');
        if (that.isCurrentView(that.projectFormView, appId, appType) === false) {
            that.projectFormView = new ProjectFormView({
                el: $content,
                tooltips: tooltips,
                projectScale: that.projectScale,
                workTypes: that.workTypes
            });
        }
        that.projectFormView.listenToOnce(that.projectScale, 'sync', that.projectFormView.render);
        that.projectFormView.listenToOnce(that.workTypes, 'sync', that.projectFormView.render);
        that.renderItem(that.projectFormView, that.projectsCollection, projectId, 'projects');
        that.highlightItem('projects');
    });
},

And the view. Notice the comment in SetValue

return ApplicantFormView.extend({
    events: {
        'change #newProject input': 'processProject',
        'change #newProject select': 'processProject',
        'change #newProject textarea': 'processProject',
    },

    template: JST['app/scripts/templates/projectForm.hbs'],
    initialize: function (options) {
        this.projectScale = options.projectScale;
        this.workTypes = options.workTypes;
        this.tooltips = options.tooltips;
    },
    render: function () {
        Backbone.Validation.bind(this, {
            selector: 'id'
        });
        this.$el.html(this.template(
            {
                project: this.model.attributes,
                projectScale: this.projectScale.toJSON(),
                workTypes: this.workTypes.toJSON(),
                appType: profileModel.get('loadedAppType'),
                appId: profileModel.get('applicationId')
            }
        ));

        this.$('.datepicker').datepicker({
            endDate: 'today',
            minViewMode: 1,
            todayBtn: 'linked',
            orientation: 'top auto',
            calendarWeeks: true,
            toggleActive: true,
            format: 'MM yyyy',
            autoclose: true
        });
        this.$('.datepicker').parent().removeClass('has-error');
        this.$('.error-msg').hide();
        this.$el.updatePolyfill();
        this.revalidation();
        return this;
    },


    processProject: function (event) {
        this.setValue(event);
        this.save();
    },

    setValue: function (event) {
        //This is called on each input change as many times as many projects were previously opened.
        event.preventDefault();
        var $el = $(event.target),
            id,
            value;

        if ($el.attr('type') === 'checkbox') {
            id = $el.attr('id');
            value = $el.is(':checked');
        } else if ($el.attr('type') === 'radio') {
            id = $el.attr('name');
            value = $('input:radio[name ="' + id + '"]:checked').val();
        } else {
            id = $el.attr('id');
            value = $el.val();
        }
        this.model.set(id, value);
        Dispatcher.trigger('upd');
    },

});

Do you have any tips what could be causing the memory leak?


Solution

  • Looks like all the views are attached to $content. Whenever you create a new view to this element, a new set of event listeners would be attached to this element.

    Ideally you should remove existing views before creating new ones to free up memory using view's remove method.

    If you can't do this for some reason and want to keep all view objects created in memory at the same time, they need to have their own elements to bind events to.

    You can do this by removing el: $content, This let's backbone create an element for each view.

    Then do $content.append(view.el) after the view creation. You'll have to detach these element from $content while creating new views.