Better solution for nested Backbone.js collections

前端 未结 4 541
一个人的身影
一个人的身影 2021-01-30 12:04

Many of my Backbone models often deal with nested models and collections, so far I\'m using a combination of defaults, parse and toJSON ma

相关标签:
4条回答
  • 2021-01-30 12:22

    I'v found out that with this approach Supplier's toJSON function will get outdated, so it might be a good idea to do reassemble back it's JSON state from it's, and it's children's data.

    ACME.Supplier = Backbone.Model.extend({
        initialize: function(options) {
            this.tags = new ACME.Tags(options.tags);
        },
    
        parse: function(res) {
            res.tags && this.tags.reset(res.tags);
    
            return res;
        },
    
        toJSON: function({
            return _.extend(
                _.pick(this.attributes, 'id', 'attr1', 'attr2'), {
                tags: this.tags.toJSON(),
            });
        })
    

    });

    0 讨论(0)
  • 2021-01-30 12:22

    Faced with the same problem, I do something like that (the code below is output of TypeScript compiler so it's a bit verbose):

      var Model = (function (_super) {
        __extends(Model, _super);
        function Model() {
            _super.apply(this, arguments);
        }
        Model.prototype.fieldToType = function () {
            return {};
        };
    
        Model.prototype.parse = function (response, options) {
            _.each(this.fieldToType(), function (type, field) {
                if (response[field]) {
                    if (_.isArray(response[field])) {
                        response[field] = _.map(response[field], function (value) {
                            return new type(value, { parse: true });
                        });
                    } else {
                        response[field] = new type(response[field], { parse: true });
                    }
                }
            });
            return _super.prototype.parse.call(this, response, options);
        };
        Model.prototype.toJSON = function () {
            var j = _super.prototype.toJSON.call(this);
            _.each(this.fieldToType(), function (type, field) {
                if (j[field]) {
                    if (_.isArray(j[field])) {
                        j[field] = _.map(j[field], function (value) {
                            return value.toJSON();
                        });
                    } else {
                        j[field] = j[field].toJSON();
                    }
                }
            });
            return j;
        };
        return Model;
    })(Backbone.Model);
    

    And then I can simply override the fieldToType method to define types of my fields:

    PendingAssignmentOffer.prototype.fieldToType = function () {
        return {
            'creator': User,
            'task_templates': TaskTemplateModel,
            'users': User,
            'school_classes': SchoolClass
        };
    };
    
    0 讨论(0)
  • I don't see any problem with your approach.

    IMHO the Model.parse() method if for this: to be overwritten in case special parse behavior needs.

    The only think I'd change would be things like this:

    if (res.tags) res.tags = new ACME.Tags(res.tags);
    

    For this:

    if (res.tags) this.tags.reset(res.tags);
    

    Due you already have an instance of ACME.Tags collection I'd reuse it.

    Also I don't really like the defaults implementation, I'm used to do this initializations in the Model.initialize() but I think is a matter of taste.

    0 讨论(0)
  • 2021-01-30 12:40

    We didn't want to add another framework to achieve that so we abstracted it away in a base model class. Here's how you declare and use it (available as a gist):

    // Declaration
    
    window.app.viewer.Model.GallerySection = window.app.Model.BaseModel.extend({
      nestedTypes: {
        background: window.app.viewer.Model.Image,
        images: window.app.viewer.Collection.MediaCollection
      }
    });
    
    // Usage
    
    var gallery = new window.app.viewer.Model.GallerySection({
        background: { url: 'http://example.com/example.jpg' },
        images: [
            { url: 'http://example.com/1.jpg' },
            { url: 'http://example.com/2.jpg' },
            { url: 'http://example.com/3.jpg' }
        ],
        title: 'Wow'
    }); // (fetch will work equally well)
    
    console.log(gallery.get('background')); // window.app.viewer.Model.Image
    console.log(gallery.get('images')); // window.app.viewer.Collection.MediaCollection
    console.log(gallery.get('title')); // plain string
    

    It works equally well with set and toJSON.
    And here's BaseModel:

    window.app.Model.BaseModel = Backbone.Model.extend({
      constructor: function () {
        if (this.nestedTypes) {
          this.checkNestedTypes();
        }
    
        Backbone.Model.apply(this, arguments);
      },
    
      set: function (key, val, options) {
        var attrs;
    
        /* jshint -W116 */
        /* jshint -W030 */
        // Code below taken from Backbone 1.0 to allow different parameter styles
        if (key == null) return this;
        if (typeof key === 'object') {
          attrs = key;
          options = val;
        } else {
          (attrs = {})[key] = val;
        }
        options || (options = {});
        // Code above taken from Backbone 1.0 to allow different parameter styles
        /* jshint +W116 */
        /* jshint +W030 */
    
        // What we're trying to do here is to instantiate Backbone models and collections
        // with types defined in this.nestedTypes, and use them instead of plain objects in attrs.
    
        if (this.nestedTypes) {
          attrs = this.mapAttributes(attrs, this.deserializeAttribute);
        }
    
        return Backbone.Model.prototype.set.call(this, attrs, options);
      },
    
      toJSON: function () {
        var json = Backbone.Model.prototype.toJSON.apply(this, arguments);
    
        if (this.nestedTypes) {
          json = this.mapAttributes(json, this.serializeAttribute);
        }
    
        return json;
      },
    
      mapAttributes: function (attrs, transform) {
        transform = _.bind(transform, this);
        var result = {};
    
        _.each(attrs, function (val, key) {
          result[key] = transform(val, key);
        }, this);
    
        return result;
      },
    
      serializeAttribute: function (val, key) {
        var NestedType = this.nestedTypes[key];
        if (!NestedType) {
          return val;
        }
    
        if (_.isNull(val) || _.isUndefined(val)) {
          return val;
        }
    
        return val.toJSON();
      },
    
      deserializeAttribute: function (val, key) {
        var NestedType = this.nestedTypes[key];
        if (!NestedType) {
          return val;
        }
    
        var isCollection = this.isTypeASubtypeOf(NestedType, Backbone.Collection),
            child;
    
        if (val instanceof Backbone.Model || val instanceof Backbone.Collection) {
          child = val;
        } else if (!isCollection && (_.isNull(val) || _.isUndefined(val))) {
          child = null;
        } else {
          child = new NestedType(val);
        }
    
        var prevChild = this.get(key);
    
        // Return existing model if it is equal to child's attributes
    
        if (!isCollection && child && prevChild && _.isEqual(prevChild.attributes, child.attributes)) {
          return prevChild;
        }
    
        return child;
      },
    
      isTypeASubtypeOf: function (DerivedType, BaseType) {
        // Go up the tree, using Backbone's __super__.
        // This is not exactly encouraged by the docs, but I found no other way.
    
        if (_.isUndefined(DerivedType['__super__'])) {
          return false;
        }
    
        var ParentType = DerivedType['__super__'].constructor;
        if (ParentType === BaseType) {
          return true;
        }
    
        return this.isTypeASubtypeOf(ParentType, BaseType);
      },
    
      checkNestedTypes: function () {
        _.each(this.nestedTypes, function (val, key) {
          if (!_.isFunction(val)) {
            console.log('Not a function:', val);
            throw new Error('Invalid nestedTypes declaration for key ' + key + ': expected a function');
          }
        });
      },
    }
    
    0 讨论(0)
提交回复
热议问题