Modelli annidati in Backbone.js, come approcciare


117

Ho il seguente JSON fornito da un server. Con questo, voglio creare un modello con un modello nidificato. Non sono sicuro di quale sia il modo per raggiungere questo obiettivo.

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

Voglio che questi vengano convertiti in due modelli di backbone nidificati con la seguente struttura:

// structure
Image
    Layout
...

Quindi definisco il modello di layout in questo modo:

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

Ma quale delle due (se presenti) tecniche di seguito dovrei usare per definire il modello di immagine? A o B sotto?

UN

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

o, B

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

Risposte:


98

Ho lo stesso problema mentre scrivo la mia applicazione Backbone. Avere a che fare con modelli incorporati / annidati. Ho apportato alcune modifiche che pensavo fossero una soluzione piuttosto elegante.

Sì, puoi modificare il metodo di analisi per cambiare un attributo nell'oggetto, ma tutto ciò è in realtà un codice IMO piuttosto irraggiungibile e sembra più un trucco che una soluzione.

Ecco cosa suggerisco per il tuo esempio:

Per prima cosa definisci il tuo modello di layout in questo modo.

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

Quindi ecco la tua immagine Modello:

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

Si noti che non ho manomesso il modello stesso, ma semplicemente restituito l'oggetto desiderato dal metodo di analisi.

Ciò dovrebbe garantire la struttura del modello nidificato durante la lettura dal server. Ora, noterai che il salvataggio o l'impostazione non viene effettivamente gestito qui perché ritengo che abbia senso impostare il modello nidificato in modo esplicito utilizzando il modello appropriato.

Così:

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

Tieni inoltre presente che stai effettivamente invocando il metodo di analisi nel tuo modello annidato chiamando:

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

Puoi definire tutti i modelli nidificati nel modelcampo di cui hai bisogno.

Ovviamente, se vuoi arrivare fino al salvataggio del modello annidato nella sua tabella. Non sarebbe sufficiente. Ma nel caso di lettura e salvataggio dell'oggetto nel suo insieme, questa soluzione dovrebbe essere sufficiente.


4
Questo è carino .. dovrebbe essere la risposta accettata in quanto è molto più pulita rispetto agli altri approcci. L'unico suggerimento che avrei è di scrivere in maiuscolo la prima lettera delle tue classi che estendono Backbone.Model per la leggibilità .. cioè ImageModel e LayoutModel
Stephen Handley

1
@StephenHandley Grazie per il commento e il tuo suggerimento. Per le informazioni, in realtà lo sto usando nel contesto di requireJS. Quindi, per rispondere alla questione delle maiuscole, la var 'imageModel' viene effettivamente restituita a requireJS. E il riferimento al modello sarebbe incapsulato dal seguente costrutto: define(['modelFile'], function(MyModel){... do something with MyModel}) Ma hai ragione. Ho l'abitudine di fare riferimento al modello secondo la convenzione che hai suggerito.
rycfung

@ BobS Scusa, è stato un errore di battitura. Avrebbe dovuto essere la risposta. L'ho risolto, grazie per averlo sottolineato.
rycfung

2
Bello! Consiglio di aggiungerlo alla Backbone.Model.prototype.parsefunzione. Quindi, tutto ciò che i tuoi modelli devono fare è definire i tipi di oggetto del sottomodello (nel tuo attributo "modello").
jasop

1
Freddo! Ho finito per fare qualcosa di simile (in particolare e purtroppo dopo aver trovato questa risposta) e l'ho scritto qui: blog.untrod.com/2013/08/declarative-approach-to-nesting.html La grande differenza è che per i modelli profondamente nidificati Dichiaro l'intera mappatura in una volta, nel modello radice / genitore, e il codice lo prende da lì e percorre l'intero modello, idratando gli oggetti rilevanti nelle collezioni e nei modelli di Backbone. Ma davvero un approccio molto simile.
Chris Clark

16

Sto postando questo codice come esempio del suggerimento di Peter Lyon di ridefinire l'analisi. Avevo la stessa domanda e questo ha funzionato per me (con un backend Rails). Questo codice è scritto in Coffeescript. Ho reso alcune cose esplicite per le persone che non lo conoscono.

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

oppure, in 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;
};

Supporta il codice di esempio e suggerisce di eseguire l'override dell'analisi. Grazie!
Edward Anderson

11
sarebbe bello avere la tua risposta in vero JS
Jason

6
felice di avere la versione coffeescript, grazie. Per altri, prova js2coffee.org
ABCD.ca

16
Se la domanda è reale JS, dovrebbe esserlo anche una risposta.
Manuel Hernandez


11

Non sono sicuro che Backbone stesso abbia un modo consigliato per farlo. L'oggetto Layout ha il proprio ID e record nel database back-end? Se è così puoi renderlo il suo modello come te. In caso contrario, puoi semplicemente lasciarlo come documento nidificato, assicurati solo di convertirlo in e da JSON correttamente nei metodi savee parse. Se finisci per adottare un approccio come questo, penso che il tuo esempio A sia più coerente con il backbone poiché setsi aggiornerà correttamente attributes, ma ancora una volta non sono sicuro di cosa faccia Backbone con i modelli nidificati per impostazione predefinita. È probabile che avrai bisogno di un codice personalizzato per gestirlo.


Ah! Spiacenti, mancava l' newoperatore. L'ho modificato per correggere questo errore.
Ross

Oh, allora ho interpretato male la tua domanda. Aggiornerò la mia risposta.
Peter Lyons,

8

Andrei con l'opzione B se vuoi mantenere le cose semplici.

Un'altra buona opzione potrebbe essere quella di utilizzare Backbone-Relational . Dovresti semplicemente definire qualcosa come:

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

+1 Backbone-Releational sembra abbastanza consolidato: proprio sito web, 1.6k stelle, 200+ fork.
Ross


5

Versione CoffeeScript della bellissima risposta di 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

Non è così dolce? ;)


11
Non prendo zucchero nel mio JavaScript
:)

2

Ho avuto lo stesso problema e ho sperimentato il codice nella risposta di rycfung , il che è un ottimo suggerimento.
Se, tuttavia, non vuoi setdirettamente i modelli annidati, o non vuoi passare costantemente {parse: true}al options, un altro approccio sarebbe quello di ridefinire setse stesso.

In Backbone 1.0.0 , setè chiamato a constructor, unset, clear, fetche save.

Considera il seguente super modello , per tutti i modelli che devono annidare modelli e / o collezioni.

/** 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) {}
});

Si noti che model, _setModele _unsetModelvengono lasciati vuoti di proposito. A questo livello di astrazione probabilmente non è possibile definire alcuna azione ragionevole per i callback. Tuttavia, potresti voler sovrascriverli nei sottomodelli che si estendono CompoundModel.
Questi callback sono utili, ad esempio, per associare ascoltatori e propagare changeeventi.


Esempio:

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

Con questo, hai la creazione automatica di modelli annidati e la propagazione degli eventi. Viene inoltre fornito e testato l'utilizzo di esempio:

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

Produzione:

{"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

Mi rendo conto di essere in ritardo per questa festa, ma recentemente abbiamo rilasciato un plugin per affrontare esattamente questo scenario. Si chiama backbone-nestify .

Quindi il tuo modello nidificato rimane invariato:

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

Quindi utilizzare il plugin quando si definisce il modello contenitore (utilizzando Underscore.extend ):

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

Dopodiché, supponendo che tu abbia un modello mche è un'istanza di Imagee hai impostato il JSON dalla domanda in poi m, puoi fare:

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

Usa forme dorsali

Supporta form, modelli e toJSON annidati. TUTTO NIDATO

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: "x@x.com"
    }
});

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

1

Se non vuoi aggiungere ancora un altro framework, potresti prendere in considerazione la creazione di una classe base con override sete toJSONe usarla in questo modo:

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

Avrai bisogno BaseModeldi questa risposta (disponibile, se vuoi, come sintesi ).


1

Abbiamo anche questo problema e un collaboratore ha implementato un plug-in denominato backbone-nested-attributes.

L'utilizzo è molto semplice. Esempio:

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

var Fruit = Backbone.Model.extend({
})

Con questo, il modello Tree può accedere quindi ai frutti:

tree.get('fruits')

Puoi vedere maggiori informazioni qui:

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.