web-dev-qa-db-fra.com

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

J'ai le JSON suivant fourni par un serveur. Avec cela, je veux créer un modèle avec un modèle imbriqué. Je ne sais pas quel est le moyen d'y parvenir.

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

Je souhaite que ceux-ci soient convertis en deux modèles de réseau fédéré imbriqués présentant la structure suivante:

// structure
Image
    Layout
...

Donc, je définis le modèle de disposition comme suit:

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

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

A

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') );
    }
});
116
Ross

J'ai le même problème pendant que j'écris mon application Backbone. Avoir à traiter avec des modèles incorporé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 modifier les attributs dans l’objet, mais tout cela est en réalité un code assez difficile à entretenir IMO et qui ressemble davantage à 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 suit.

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 altéré le modèle lui-même, mais simplement renvoyé 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. Vous remarquerez à présent que la sauvegarde ou les paramètres ne sont pas gérés ici, car j'estime qu'il est logique de définir explicitement le modèle imbriqué à l'aide du 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 model que vous le souhaitez.

Bien sûr, si vous voulez aller jusqu'à sauvegarder le 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.

98
rycfung

Je publie ce code comme exemple de la proposition de Peter Lyon de redéfinir l'analyse. J'avais la même question et cela fonctionnait pour moi (avec un backend Rails). Ce code est écrit en Coffeescript. J'ai explicitement expliqué certaines choses aux 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, 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;
};
16
Eric Hu

Utilisation Backbone.AssociatedModel de associations 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
        }
    });
11
Jaynti Kanani

Je ne suis pas sûr que Backbone ait un moyen recommandé de le faire. L'objet Layout a-t-il son propre ID et est-il enregistré 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 le laisser comme un document imbriqué, mais assurez-vous de le convertir correctement vers et depuis JSON dans les méthodes save et parse. Si vous finissez par adopter une telle approche, je pense que votre exemple A est plus cohérent avec le réseau principal puisque set mettra correctement à jour attributes, mais encore une fois, je ne suis pas sûr de ce que Backbone fait avec les modèles imbriqués par défaut. Il est probable que vous aurez besoin d'un code personnalisé pour gérer cela.

11
Peter Lyons

Je choisirais l'option B si vous voulez garder les choses simples.

Une autre bonne option serait d'utiliser Backbone-Relational . Vous voudriez juste définir quelque chose comme:

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

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

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

Vous pouvez vous lier pour changer les événements des niveaux n profonds. par exemple: model.on('change:example.nestedmodel.attribute', this.myFunction);

6
Mark

Version CoffeeScript de rycfung's Belle réponse:

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

5
Dan Fox

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

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

Considérez ce qui suit super model, 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, _setModel et _unsetModel sont laissés en blanc volontairement. À 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 des écouteurs et propager des événements 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. 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 }));

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}}
2
afsantos

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

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

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

Ensuite, utilisez le plugin pour définir le modèle contenant (en utilisant nderscore.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 de Image et que vous avez défini le code JSON à partir de la question sur m, vous pouvez effectuer les opérations suivantes:

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

Utiliser des formes de colonne vertébrale

Il prend en charge les formulaires imbriqués, les modèles et toJSON. TOUS INTÉGRÉS

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);
2
David Rz Ayala

Si vous ne voulez pas ajouter encore un autre framework, vous pouvez envisager de créer une classe de base avec set et toJSON surchargés 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 ).

1
Dan Abramov

Nous avons également ce problème et un membre du personnel de l’équipe a implémenté un plugin nommé backbone-nested-attributs.

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 ensuite aux fruits:

tree.get('fruits')

Vous pouvez voir plus d'informations ici:

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

1
Gustavo Kloh