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

117

J'ai le JSON suivant fourni par un serveur. Avec cela, je souhaite créer un modèle avec un modèle imbriqué. Je ne sais pas quelle est la manière d’y parvenir.

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

Je veux que ceux-ci soient convertis en deux modèles de dorsale 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 ci-dessous (le cas échéant) dois-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') );
    }
});
Ross
la source

Réponses:

98

J'ai le même problème lorsque j'écris mon application Backbone. Devoir gérer des modèles embarqués / imbriqué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 changer un attribut autour de l'objet, mais tout cela est en fait un code IMO assez impossible à maintenir, et ressemble plus à un hack qu'à une solution.

Voici ce que je suggère pour votre exemple:

Définissez d'abord votre modèle de mise en page comme tel.

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

Alors voici votre modèle d'image:

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 altéré le modèle lui-même, mais simplement renvoyer l'objet souhaité à partir de la méthode d'analyse.

Cela devrait garantir la structure du modèle imbriqué lorsque vous lisez à partir du serveur. Maintenant, vous remarquerez que l'enregistrement ou la configuration n'est en fait pas gérée ici car je pense qu'il est logique que vous définissiez 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 invoquez en fait 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 modelchamp que nécessaire.

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

Rycfung
la source
4
C'est bien .. devrait être la réponse acceptée car c'est beaucoup plus propre que les autres approches. Les seules suggestions que j'aurais est de mettre en majuscule la première lettre de vos classes qui étendent Backbone.Model pour plus de lisibilité .. ie ImageModel et LayoutModel
Stephen Handley
1
@StephenHandley Merci pour le commentaire et votre suggestion. Pour information, j'utilise en fait ceci dans le contexte de requireJS. Donc, pour répondre à la question des majuscules, la var 'imageModel' est en fait retournée à requireJS. Et la référence au modèle serait encapsulée par la construction suivante: define(['modelFile'], function(MyModel){... do something with MyModel}) Mais vous avez raison. Je prends l'habitude de faire référence au modèle par la convention que vous avez suggérée.
rycfung
@BobS Désolé, c'était une faute de frappe. Aurait dû être une réponse. Je l'ai réparé, merci de l'avoir signalé.
rycfung
2
Agréable! Je recommande d'ajouter ceci à la Backbone.Model.prototype.parsefonction. Ensuite, tout ce que vos modèles ont à faire est de définir les types d'objet du sous-modèle (dans votre attribut "modèle").
jasop
1
Cool! J'ai fini par faire quelque chose de similaire (notamment et malheureusement après avoir trouvé cette réponse) et l'ai écrite ici: blog.untrod.com/2013/08/declarative-approach-to-nesting.html La grande différence est que pour les modèles profondément imbriqués Je déclare tout le mappage en même temps, dans le modèle racine / parent, et le code le prend à partir de là et parcourt tout le modèle, hydratant les objets pertinents dans les collections et les modèles Backbone. Mais vraiment une approche très similaire.
Chris Clark
16

Je poste 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 Rails). Ce code est écrit en Coffeescript. J'ai rendu certaines choses explicites pour les personnes qui ne le connaissent 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, en 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;
};
Eric Hu
la source
Props pour l'exemple de code et suggérant de remplacer l'analyse. Merci!
Edward Anderson
11
serait bien d'avoir votre réponse en vrai JS
Jason
6
heureux d'avoir la version coffeescript, merci. Pour les autres, essayez js2coffee.org
ABCD.ca
16
Si la question est un vrai JS, une réponse devrait l'être également.
Manuel Hernandez
12

Utilisation à Backbone.AssociatedModelpartir d' associations de dorsales :

    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
        }
    });
Jaynti Kanani
la source
Belle solution. Il existe également un projet similaire: github.com/PaulUithol/Backbone-relational
michaelok
11

Je ne suis pas sûr que Backbone lui-même ait un moyen recommandé de le faire. L'objet Layout a-t-il son propre ID et son propre enregistrement dans la base de données principale? 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 correctement depuis et vers JSON dans les méthodes saveet parse. Si vous finissez par adopter une approche comme celle-ci, je pense que votre exemple A est plus cohérent avec le backbone car il setsera correctement mis à jour attributes, mais encore une fois, je ne suis pas sûr de ce que fait Backbone avec les modèles imbriqués par défaut. Il est probable que vous aurez besoin d'un code personnalisé pour gérer cela.

Peter Lyons
la source
Ah! Désolé, il manquait l' newopérateur. Je l'ai modifié pour corriger cette erreur.
Ross
Oh, alors j'ai mal interprété votre question. Je mettrai à jour ma réponse.
Peter Lyons
8

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

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

var Image = Backbone.Model.extend({
    relations: [
        {
            type: Backbone.HasOne,
            key: 'layout',
            relatedModel: 'Layout'
        }
    ]
});
philfreo
la source
+1 Backbone-Releational semble bien établi: propre site Web, 1,6k étoiles, plus de 200 fourchettes.
Ross
6

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

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

Vous pouvez créer une liaison pour modifier les niveaux d'événements en profondeur. par exemple: model.on('change:example.nestedmodel.attribute', this.myFunction);

marque
la source
5

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

Dan Fox
la source
11
Je ne prends pas de sucre dans mon JavaScript :)
Ross
2

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

Dans Backbone 1.0.0 , setest appelé constructor, unset, clear, fetchet save.

Considérez le super modèle suivant , pour tous les modèles qui doivent imbriquer 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 model, _setModelet _unsetModelsont laissés en blanc exprès. À ce niveau d'abstraction, vous ne pouvez probablement pas définir d'actions raisonnables pour les rappels. Cependant, vous souhaiterez peut-être les remplacer dans les sous-modèles qui s'étendent CompoundModel.
Ces rappels sont utiles, par exemple, pour lier des écouteurs et propager des changeévénements.


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. Un exemple d'utilisation est également fourni et testé:

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

Production:

{"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}}
Afsantos
la source
2

Je me rends compte que je suis en retard à cette soirée, mais nous avons récemment publié un plugin pour gérer exactement ce scénario. Cela s'appelle backbone-nestify .

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

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

Utilisez ensuite le plugin lors de la définition du modèle conteneur (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 mqui est une instance de Image, et que vous avez défini le JSON à partir de la question m, vous pouvez faire:

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
Scott Bale
la source
2

Utiliser des formes de base

Il prend en charge les formulaires imbriqués, les modèles et toJSON. TOUT EMBRIQUÉ

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);
David Rz Ayala
la source
1

Si vous ne souhaitez pas ajouter encore un autre framework, vous pouvez envisager de créer une classe de base avec substitué setet toJSONet 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 BaseModelde cette réponse (disponible, si vous le souhaitez, comme une base ).

Dan Abramov
la source
1

Nous avons également ce problème et un collaborateur a implémenté un plugin nommé backbone-nested-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

Gustavo Kloh
la source