Les modèles imbriqués dans Backbone.js, comment aborder

J'ai reçu le JSON suivant fourni par un serveur. Avec cela, je veux créer un modèle avec un modèle imbriqué. Je ne suis pas sûr de savoir comment y parvenir.

//json [{ name : "example", layout : { x : 100, y : 100, } }] 

Je veux que ceux-ci soient convertis en deux modèles de squelette imbriqués avec la structure suivante:

 // structure Image Layout ... 

Je définis donc le modèle de mise en page comme suit:

 var Layout = Backbone.Model.extend({}); 

Mais laquelle des deux techniques (le cas échéant) ci-dessous devrais-je utiliser pour définir le modèle d'image? A ou B ci-dessous?

UNE

 var Image = Backbone.Model.extend({ initialize: function() { this.set({ 'layout' : new Layout(this.get('layout')) }) } }); 

Ou, B

 var Image = Backbone.Model.extend({ initialize: function() { this.layout = new Layout( this.get('layout') ); } }); 

J'ai le même problème alors que j'écris ma demande Backbone. Avoir affaire à des modèles intégrés / nichés. J'ai fait quelques ajustements que je pensais être une solution assez élégante.

Oui, vous pouvez modifier la méthode d'analyse pour modifier les attributs autour de l'objet, mais tout cela est en fait un code IMO assez imparable, et ressent plus d'un hack qu'une solution.

Voici ce que je suggère pour votre exemple:

Définissez d'abord votre modèle de présentation comme tel.

 var layoutModel = Backbone.Model.extend({}); 

Alors voici votre image Modèle:

 var imageModel = Backbone.Model.extend({ model: { layout: layoutModel, }, parse: function(response){ for(var key in this.model) { var embeddedClass = this.model[key]; var embeddedData = response[key]; response[key] = new embeddedClass(embeddedData, {parse:true}); } return response; } }); 

Notez que je n'ai pas falsifié le modèle lui-même, mais simplement renvoyer l'objet souhaité à partir de la méthode d'analyse.

Cela devrait assurer la structure du modèle imbriqué lorsque vous lisez le serveur. Maintenant, vous remarquerez que l'enregistrement ou le réglage n'est pas géré ici car je pense qu'il est logique pour vous de définir explicitement le modèle imbriqué en utilisant le modèle approprié.

Ainsi:

 image.set({layout : new Layout({x: 100, y: 100})}) 

Notez également que vous appelez réellement la méthode d'analyse dans votre modèle imbriqué en appelant:

 new embeddedClass(embeddedData, {parse:true}); 

Vous pouvez définir autant de modèles imbriqués dans le champ du model que vous avez besoin.

Bien sûr, si vous voulez aller jusqu'à la sauvegarde du modèle imbriqué dans sa propre table. Cela ne suffirait pas. Mais dans le cas de la lecture et de la sauvegarde de l'objet dans son ensemble, cette solution devrait suffire.

Je publie ce code comme exemple de la suggestion de Peter Lyon de redéfinir l'analyse. J'ai eu la même question et cela a fonctionné pour moi (avec un backend de Rails). Ce code est écrit dans Coffeescript. J'ai fait quelques explications explicites pour les personnes qui ne le connaissaient pas.

 class AppName.Collections.PostsCollection extends Backbone.Collection model: AppName.Models.Post url: '/posts' ... # parse: redefined to allow for nested models parse: (response) -> # function definition # convert each comment attribute into a CommentsCollection if _.isArray response _.each response, (obj) -> obj.comments = new AppName.Collections.CommentsCollection obj.comments else response.comments = new AppName.Collections.CommentsCollection response.comments return response 

Ou, dans JS

 parse: function(response) { if (_.isArray(response)) { return _.each(response, function(obj) { return obj.comments = new AppName.Collections.CommentsCollection(obj.comments); }); } else { response.comments = new AppName.Collections.CommentsCollection(response.comments); } return response; }; 

Je ne suis pas sûr que Backbone lui-même ait recommandé de le faire. L'objet Layout a-t-il sa propre ID et son enregistrement dans la base de données arrière? Si c'est le cas, vous pouvez en faire son propre modèle comme vous l'avez fait. Sinon, vous pouvez simplement le laisser en tant que document imbriqué, assurez-vous simplement de le convertir à JSON correctement dans les méthodes d' save et d' parse . Si vous finissez par adopter une approche comme celle-ci, je pense que votre exemple A est plus cohérent avec l'arrière-plan car le set mettra correctement à jour les attributes , mais encore une fois, je ne suis pas sûr de ce que Backbone utilise avec les modèles imbriqués par défaut. Il est probable que vous aurez besoin d'un code personnalisé pour gérer cela.

Utilisez Backbone.AssociatedModel de Backbone-associations :

  var Layout = Backbone.AssociatedModel.extend({ defaults : { x : 0, y : 0 } }); var Image = Backbone.AssociatedModel.extend({ relations : [ type: Backbone.One, key : 'layout', relatedModel : Layout ], defaults : { name : '', layout : null } }); 

J'irais avec l'option B si vous voulez garder les choses simples.

Une autre bonne option serait d'utiliser Backbone-Relational . Vous définissez simplement quelque chose comme:

 var Image = Backbone.Model.extend({ relations: [ { type: Backbone.HasOne, key: 'layout', relatedModel: 'Layout' } ] }); 

J'utilise le plugin Backbone DeepModel pour les modèles et les attributs imbriqués.

https://github.com/powmedia/backbone-deep-model

Vous pouvez vous lier pour modifier les niveaux d'événements n en profondeur. Par exemple: model.on('change:example.nestedmodel.attribute', this.myFunction);

Version CoffeeScript de la belle réponse de Rycfung :

 class ImageModel extends Backbone.Model model: { layout: LayoutModel } parse: (response) => for propName,propModel of @model response[propName] = new propModel( response[propName], {parse:true, parentModel:this} ) return response 

N'est-ce pas si doux? 😉

J'ai eu le même problème et j'ai expérimenté le code dans la réponse de rycfung , ce qui est une excellente suggestion.
Si, cependant, vous ne souhaitez pas set directement les modèles imbriqués ou ne souhaitez pas passer par {parse: true} dans les options , une autre approche serait de redéfinir la set en place.

Dans Backbone 1.0.0 , le set est appelé dans constructor , unset , clear , fetch and save .

Considérez le super modèle suivant , pour tous les modèles qui doivent nicher des modèles et / ou des collections.

 /** Compound supermodel */ var CompoundModel = Backbone.Model.extend({ /** Override with: key = attribute, value = Model / Collection */ model: {}, /** Override default setter, to create nested models. */ set: function(key, val, options) { var attrs, prev; if (key == null) { return this; } // Handle both `"key", value` and `{key: value}` -style arguments. if (typeof key === 'object') { attrs = key; options = val; } else { (attrs = {})[key] = val; } // Run validation. if (options) { options.validate = true; } else { options = { validate: true }; } // For each `set` attribute, apply the respective nested model. if (!options.unset) { for (key in attrs) { if (key in this.model) { if (!(attrs[key] instanceof this.model[key])) { attrs[key] = new this.model[key](attrs[key]); } } } } Backbone.Model.prototype.set.call(this, attrs, options); if (!(attrs = this.changedAttributes())) { return this; } // Bind new nested models and unbind previous nested models. for (key in attrs) { if (key in this.model) { if (prev = this.previous(key)) { this._unsetModel(key, prev); } if (!options.unset) { this._setModel(key, attrs[key]); } } } return this; }, /** Callback for `set` nested models. * Receives: * (String) key: the key on which the model is `set`. * (Object) model: the `set` nested model. */ _setModel: function (key, model) {}, /** Callback for `unset` nested models. * Receives: * (String) key: the key on which the model is `unset`. * (Object) model: the `unset` nested model. */ _unsetModel: function (key, model) {} }); 

Notez que le model , _setModel et _unsetModel sont laissés en blanc à dessein. À ce niveau d'abstraction, vous ne pouvez probablement pas définir d'actions raisonnables pour les rappels. Cependant, vous pouvez les remplacer dans les sous-modèles qui étendent CompoundModel .
Ces rappels sont utiles, par exemple, pour lier les auditeurs et propager des événements de change .


Exemple:

 var Layout = Backbone.Model.extend({}); var Image = CompoundModel.extend({ defaults: function () { return { name: "example", layout: { x: 0, y: 0 } }; }, /** We need to override this, to define the nested model. */ model: { layout: Layout }, initialize: function () { _.bindAll(this, "_propagateChange"); }, /** Callback to propagate "change" events. */ _propagateChange: function () { this.trigger("change:layout", this, this.get("layout"), null); this.trigger("change", this, null); }, /** We override this callback to bind the listener. * This is called when a Layout is set. */ _setModel: function (key, model) { if (key !== "layout") { return false; } this.listenTo(model, "change", this._propagateChange); }, /** We override this callback to unbind the listener. * This is called when a Layout is unset, or overwritten. */ _unsetModel: function (key, model) { if (key !== "layout") { return false; } this.stopListening(); } }); 

Avec cela, vous avez la création automatique de modèles imbriqués et la propagation d'événements. L'utilisation de l'échantillon est également fournie et testée:

 function logStringified (obj) { console.log(JSON.stringify(obj)); } // Create an image with the default attributes. // Note that a Layout model is created too, // since we have a default value for "layout". var img = new Image(); logStringified(img); // Log the image everytime a "change" is fired. img.on("change", logStringified); // Creates the nested model with the given attributes. img.set("layout", { x: 100, y: 100 }); // Writing on the layout propagates "change" to the image. // This makes the image also fire a "change", because of `_propagateChange`. img.get("layout").set("x", 50); // You may also set model instances yourself. img.set("layout", new Layout({ x: 100, y: 100 })); 

Sortie:

 {"name":"example","layout":{"x":0,"y":0}} {"name":"example","layout":{"x":100,"y":100}} {"name":"example","layout":{"x":50,"y":100}} {"name":"example","layout":{"x":100,"y":100}} 

Je me rends compte que je suis en retard à cette fête, mais nous avons récemment publié un plugin pour faire face exactement à ce scénario. On l'appelle backbone-nestify .

Ainsi, votre modèle imbriqué reste inchangé:

var Layout = Backbone.Model.extend({...});

Ensuite, utilisez le plugin lors de la définition du modèle contenant (en utilisant Underscore.extend ):

 var spec = { layout: Layout }; var Image = Backbone.Model.extend(_.extend({ // ... }, nestify(spec)); 

Après cela, en supposant que vous avez un modèle m qui est une instance d' Image , et que vous avez défini JSON de la question sur m , vous pouvez:

 m.get("layout"); //returns the nested instance of Layout m.get("layout|x"); //returns 100 m.set("layout|x", 50); m.get("layout|x"); //returns 50 

Utiliser des squelettes

Il prend en charge les formes, les modèles et les JOJS. TOUT NESTÉ

 var Address = Backbone.Model.extend({ schema: { street: 'Text' }, defaults: { street: "Arteaga" } }); var User = Backbone.Model.extend({ schema: { title: { type: 'Select', options: ['Mr', 'Mrs', 'Ms'] }, name: 'Text', email: { validators: ['required', 'email'] }, birthday: 'Date', password: 'Password', address: { type: 'NestedModel', model: Address }, notes: { type: 'List', itemType: 'Text' } }, constructor: function(){ Backbone.Model.apply(this, arguments); }, defaults: { email: "[email protected]" } }); var user = new User(); user.set({address: {street: "my other street"}}); console.log(user.toJSON()["address"]["street"]) //=> my other street var form = new Backbone.Form({ model: user }).render(); $('body').append(form.el); 

Si vous ne souhaitez pas encore ajouter d'autre cadre, vous pourriez envisager de créer une classe de base avec un set toJSON et un toJSON et l'utiliser comme ceci:

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

Vous aurez besoin de BaseModel de cette réponse (disponible, si vous le souhaitez, en tant que gist ).

Nous avons également ce problème et un travailleur d'équipe a mis en place un plugin nommé backbone-anested-attributes.

L'utilisation est très simple. Exemple:

 var Tree = Backbone.Model.extend({ relations: [ { key: 'fruits', relatedModel: function () { return Fruit } } ] }) var Fruit = Backbone.Model.extend({ }) 

Avec cela, le modèle Tree peut accéder aux fruits:

 tree.get('fruits') 

Vous pouvez voir plus d'informations ici:

https://github.com/dtmtec/backbone-nested-attributes