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.