Save several Backbone models at once

后端 未结 3 1944
情歌与酒
情歌与酒 2021-02-06 00:44

I have a Backbone collection with a load of models.

Whenever a specific attribute is set on a model and it is saved, a load of calculations fire off and the UI rerenders

3条回答
  •  星月不相逢
    2021-02-06 01:22

    I have ended up augmenting Backbone.Collection with a couple of methods to handle this.

    The saveChangeMethod creates a dummy model to be passed to Backbone.sync. All backbone's sync method needs from a model is its url property and toJSON method, so we can easily knock this up.

    Internally, a model's toJSON method only returns a copy of it's attributes (to be sent to the server), so we can happily just use a toJSON method that just returns the array of models. Backbone.sync stringifies this, which gives us just the attribute data.

    On success, saveChanged fires off events on the collection to be handled once. Have chucked in a bit of code that gets it firing specific events once for each of the attributes that have changed in any of the batch's models.

    Backbone.Collection.prototype.saveChanged = function () {
        var me = this,
            changed = me.getChanged(),
            dummy = {
                url: this.url,
                toJSON: function () {
                    return changed.models;
                }
            },
            options = {
                success: function (model, resp, xhr) {
                    for (var i = 0; i < changed.models.length; i++) {
                        changed.models[i].chnageSilently();
                    }
                    for (var attr in changed.attributes) {
                        me.trigger("batchchange:" + attr);
                    }
                    me.trigger("batchsync", changed);
                }
            };
        return Backbone.sync("update", dummy, options);
    }
    

    We then just need the getChanged() method on a collection. This returns an object with 2 properties, an array of the changed models and an object flagging which attributes have changed:

    Backbone.Collection.prototype.getChanged = function () {
        var models = [],
            changedAttributes = {};
        for (var i = 0; i < this.models.length; i++) {
            if (this.models[i].hasChanged()) {
                _.extend(changedAttributes, this.models[i].changedAttributes());
                models.push(this.models[i]);
            }
        }
        return models.length ? {models: models, attributes: changedAttributes} : null;
    }
    

    Although this is slight abuse of the intended use of backbones 'changed model' paradigm, the whole point of batching is that we don't want anything to happen (i.e. any events to fire off) when a model is changed.

    We therefore have to pass {silent: true} to the model's set() method, so it makes sense to use backbone's hasChanged() to flag models waiting to be saved. Of course this would be problematic if you were changing models silently for other purposes - collection.saveChanged() would save these too, so it is worth considering setting an alternative flag.

    In any case, if we are doing this way, when saving, we need to make sure backbone now thinks the models haven't changed (without triggering their change events), so we need to manually manipulate the model as if it hadn't been changed. The saveChanged() method iterates over our changed models and calls this changeSilently() method on the model, which is basically just Backbone's model.change() method without the triggers:

    Backbone.Model.prototype.changeSilently = function () {
        var options = {},
        changing = this._changing;
        this._changing = true;
        for (var attr in this._silent) this._pending[attr] = true;
        this._silent = {};
        if (changing) return this;
    
        while (!_.isEmpty(this._pending)) {
            this._pending = {};
            for (var attr in this.changed) {
            if (this._pending[attr] || this._silent[attr]) continue;
            delete this.changed[attr];
            }
            this._previousAttributes = _.clone(this.attributes);
        }
        this._changing = false;
        return this;
    }
    

    Usage:

    model1.set({key: value}, {silent: true});
    model2.set({key: value}, {silent: true});
    model3.set({key: value}, {silent: true});
    collection.saveChanged();
    

    RE. RESTfulness.. It's not quite right to do a PUT to the collection's endpoint to change 'some' of its records. Technically a PUT should replace the entire collection, though until my application ever actually needs to replace an entire collection, I am happy to take the pragmatic approach.

提交回复
热议问题