underscore source examples backbonejs backbone annotated backbone.js nested-attributes

backbone.js - source - underscore js download



Mejor soluciĆ³n para colecciones anidadas de Backbone.js (4)

Muchos de mis modelos de Backbone a menudo tratan con colecciones y modelos anidados, hasta ahora estoy usando una combinación de defaults , parse y toJSON manualmente para lograr el anidado:

ACME.Supplier = Backbone.Model.extend({ defaults: function() { return { contacts: new ACME.Contacts(), tags: new ACME.Tags(), attachments: new ACME.Attachments() }; }, parse: function(res) { if (res.contacts) res.contacts = new ACME.Contacts(res.contacts); if (res.tags) res.tags = new ACME.Tags(res.tags); if (res.attachments) res.attachments = new ACME.Attachments(res.attachments); return res; } }); ACME.Tag = Backbone.Model.extend({ toJSON: function() { return _.pick(this.attributes, ''id'', ''name'', ''type''); } });

He visto algunos complementos que básicamente hacen lo mismo que antes pero con mucho menos control y más repetitivo, por lo que me pregunto si alguien tiene una solución más elegante para este problema común de Backbone.js.

Edit: Terminé con el siguiente enfoque:

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; } }); ACME.Tag = Backbone.Model.extend({ toJSON: function() { return _.pick(this.attributes, ''id'', ''name'', ''type''); } });

Vale la pena señalar que más tarde descubrí que deberá pasar los datos de modelo / colección anidados del constructor al constructor del modelo anidado a través del objeto de options .


Ante el mismo problema, hago algo así (el código de abajo es el resultado del compilador de TypeScript, por lo que es un poco detallado):

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);

Y luego simplemente puedo anular el método fieldToType para definir los tipos de mis campos:

PendingAssignmentOffer.prototype.fieldToType = function () { return { ''creator'': User, ''task_templates'': TaskTemplateModel, ''users'': User, ''school_classes'': SchoolClass }; };


Me he enterado de que, con este enfoque, la función toJSON del proveedor quedará desactualizada, por lo que podría ser una buena idea volver a armar su estado JSON y los datos de los niños.

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(), }); })

});


No queríamos agregar otro marco para lograrlo, así que lo abstraemos en una clase de modelo base. Así es como usted lo declara y lo usa ( disponible como una esencia ):

// 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

Funciona igual de bien con set y toJSON .
Y aquí está 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''); } }); }, }


No veo ningún problema con su enfoque.

En mi humilde opinión, el método Model.parse() si para esto: se debe sobrescribir en caso de que necesite un comportamiento de análisis especial .

El único pensamiento que cambiaría sería algo como esto:

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

Para esto:

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

Debido a que ya tiene una instancia de la colección ACME.Tags , la reutilizaría.

Además, no me gusta mucho la implementación por defaults , estoy acostumbrado a hacer estas inicializaciones en el Model.initialize() pero creo que es una cuestión de gusto.