Search code examples
javascriptbackbone.jsbackbone.js-collectionsbackbone-model

Backbone Collection model duplicate itself


So, I have a table view (parent) and row view (child).

I add every row with this code

addOne: function (model, base) {
    var view = new App.Views.file_manager_item({model: model});
    base.append(view.render());
},

renderList: function () {
    var _this = this;
    var collection = this.files_collection;

    document.getElementById("content").innerHTML = this.templates.table(this.context);

    this.$files = $(document.getElementById('files'));

    collection.each(function(model) {
        _this.addOne(model, _this.$files);
    });
},

The renderList fired by:

this.listenTo(this.files_collection, "change", this.renderList);

App.Views.file_manager_item is

var File_manager_item = Backbone.View.extend({
    tagName: 'tr',

    initialize: function () {
        this.listenTo(this.model, "change", this.render);
    },

    template: Template7.compile(document.getElementById("fm_item_template").innerHTML),

    events: {
        "click .check": "toggleCheck",
    },

    toggleCheck: function () {
        this.test = !this.test;

        this.model.set({
            "checked": this.test
        });
    },

    render: function () {
        console.log(this.model)
        var context = this.model.toJSON();
        this.el.innerHTML = this.template(context);
        return this.$el;
    },
});

and the first run return to console

child {cid: "c3", attributes: Object, ...}
...
...
...
...
child {cid: "c11", attributes: Object, ...}

after toggleCheck function runs twice

child {cid: "c3", attributes: Object, ...}
child {cid: "c3", attributes: Object, ...}
...
...
...
...
child {cid: "c11", attributes: Object, ...}

and after every model change, add new child in console

child {cid: "c3", attributes: Object, ...}

Why models are duplicating?


Solution

  • The models aren't increasing, it's just that the views are still alive even if not on the page anymore. It's a kind of memory leak. There are multiple item views for the same model, all listening to its change event.

    A good way to avoid these leaks is to keep a reference to the item view when creating it, then call .remove() on all of them before re-rendering.

    Your item view

    var File_manager_item = Backbone.View.extend({
        tagName: 'tr',
        template: Template7.compile(document.getElementById("fm_item_template").innerHTML),
    
        events: {
            "click .check": "toggleCheck",
        },
        initialize: function() {
            this.listenTo(this.model, "change", this.render);
        },
    
        toggleCheck: function() {
            this.test = !this.test;
            this.model.set({ "checked": this.test });
        },
    
        render: function() {
            console.log(this.model);
            // use jQuery because it's already available
            this.$el.html(this.template(this.model.toJSON()));
            return this; // return this to chain calls
        },
    });
    

    Then the list view

    var ListView = Backbone.View.extend({
        initialize: function() {
            this.childViews = [];
            this.listenTo(this.files_collection, "change", this.renderList);
        },
        addOne: function(model) {
            var view = new App.Views.file_manager_item({ model: model });
            this.childViews.push(view);
    
            // this.$files is available here, there's no need to pass it around
            this.$files.append(view.render().el);
        },
    
        renderList: function() {
    
            // same thing, use jQuery, it's useless to use the native API to them put it 
            // into a jQuery object, unless a marginal performance gain is the goal.
            this.$("#content").html(this.templates.table(this.context));
            this.$files = this.$('#files');
            this.cleanup();
    
            // collection's each function is just a proxy to the underscore one.
            this.files_collection.each(this.addOne, this); // use the context argument
            return this;
        },
    
        cleanup: function() {
            _.invoke(this.childViews, 'remove');
            this.childViews = [];
        },
    });