Save several Backbone models at once

后端 未结 3 1940
情歌与酒
情歌与酒 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.

    0 讨论(0)
  • 2021-02-06 01:32

    This is what i came up with.

    Backbone.Collection.extend({
        saveAll: function(models, key, val, options) {
    
            var attrs, xhr, wait, that = this;
    
            var transport = {
                url: this.url,
                models: [],
                toJSON: function () {
                    return { models: this.models };
                },
                trigger: function(){
                    return that.trigger.apply(that, arguments);
                }
            };
    
            if(models == null){
                models = this.models;
            }
    
            // Handle both `"key", value` and `{key: value}` -style arguments.
            if (key == null || typeof key === 'object') {
                attrs = key;
                options = val;
            } else {
                (attrs = {})[key] = val;
            }
    
            options = _.extend({validate: true}, options);
            wait = options.wait;
    
            // After a successful server-side save, the client is (optionally)
            // updated with the server-side state.
            if (options.parse === void 0) options.parse = true;
    
            var triggers = [];
    
            _.each(models, function(model){
    
                var attributes = model.attributes;
    
                // If we're not waiting and attributes exist, save acts as
                // `set(attr).save(null, opts)` with validation. Otherwise, check if
                // the model will be valid when the attributes, if any, are set.
                if (attrs && !wait) {
                    if (!model.set(attrs, options)) return false;
                } else {
                    if (!model._validate(attrs, options)) return false;
                }
    
                // Set temporary attributes if `{wait: true}`.
                if (attrs && wait) {
                    model.attributes = _.extend({}, attributes, attrs);
                }
    
                transport.models.push(model.toJSON());
    
                triggers.push(function(resp){
                    if(resp.errors){
                        model.trigger('error', model, resp, options);
                    } else {
                        // Ensure attributes are restored during synchronous saves.
                        model.attributes = attributes;
                        var serverAttrs = options.parse ? model.parse(resp, options) : resp;
                        if (wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
                        if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
                            return false;
                        }
                        model.trigger('sync', model, resp, options);
                    }
                });
    
                // Restore attributes.
                if (attrs && wait) model.attributes = attributes;
            });
    
            var success = options.success;
            options.success = function(resp) {
                _.each(triggers, function(trigger, i){
                    trigger.call(options.context, resp[i]);
                });
                if (success) success.call(options.context, models, resp, options);
            };
            return this.sync('create', transport, options);
        }
    });
    
    0 讨论(0)
  • 2021-02-06 01:45

    You can define a new resource to accomplish this kind of behavior, you can call it MyModelBatch.

    You need to implement a new resource in you server side that is able to digest an Array of models and execute the proper action: CREATE, UPDATE and DESTROY.

    Also you need to implement a Model in your Backbone client side with one attribute which is the Array of Models and a special url that doesn't make use the id.

    About the re-render thing I suggest you to try to have one View by each Model so there will be as much renders as Models have changed but they will be detail re-renders without duplication.

    0 讨论(0)
提交回复
热议问题