Modelos anidados en Backbone.js, cómo abordar

117

Tengo el siguiente JSON proporcionado desde un servidor. Con esto, quiero crear un modelo con un modelo anidado. No estoy seguro de cuál es la forma de lograrlo.

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

Quiero que estos se conviertan en dos modelos de backbone anidados con la siguiente estructura:

// structure
Image
    Layout
...

Así que defino el modelo Layout así:

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

Pero, ¿cuál de las dos técnicas (si las hay) a continuación debo utilizar para definir el modelo de imagen? ¿A o B a continuación?

UNA

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

Respuestas:

98

Tengo el mismo problema mientras escribo mi aplicación Backbone. Tener que lidiar con modelos incrustados / anidados. Hice algunos ajustes que pensé que eran una solución bastante elegante.

Sí, puede modificar el método de análisis para cambiar un atributo en el objeto, pero todo eso es en realidad un código bastante inmantenible en mi opinión, y se siente más como un truco que como una solución.

Esto es lo que sugiero para su ejemplo:

Primero defina su modelo de diseño así.

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

Entonces aquí está tu modelo de imagen:

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

Tenga en cuenta que no he manipulado el modelo en sí, sino que simplemente devolví el objeto deseado del método de análisis.

Esto debería garantizar la estructura del modelo anidado cuando lee desde el servidor. Ahora, notará que guardar o configurar en realidad no se maneja aquí porque creo que tiene sentido que configure el modelo anidado explícitamente usando el modelo adecuado.

Al igual que:

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

También tenga en cuenta que en realidad está invocando el método de análisis en su modelo anidado llamando a:

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

Puede definir tantos modelos anidados en el modelcampo como necesite.

Por supuesto, si desea ir tan lejos como para guardar el modelo anidado en su propia tabla. Esto no sería suficiente. Pero en el caso de leer y guardar el objeto como un todo, esta solución debería ser suficiente.

Rycfung
fuente
4
Esto es bueno ... debería ser la respuesta aceptada ya que es mucho más limpia que los otros enfoques. La única sugerencia que tengo es poner en mayúscula la primera letra de sus clases que extienden Backbone.Model para facilitar la lectura ... es decir, ImageModel y LayoutModel
Stephen Handley
1
@StephenHandley Gracias por el comentario y tu sugerencia. Para obtener información, en realidad estoy usando esto en el contexto de requireJS. Entonces, para responder al asunto de las mayúsculas, el var 'imageModel' en realidad se devuelve a requireJS. Y la referencia al modelo estaría encapsulada por la siguiente construcción: define(['modelFile'], function(MyModel){... do something with MyModel}) Pero tienes razón. Tengo el hábito de hacer referencia al modelo por la convención que sugirió.
rycfung
@BobS Lo siento, fue un error tipográfico. Debería haber sido una respuesta. Lo he arreglado, gracias por señalarlo.
rycfung
2
¡Agradable! Recomiendo agregar esto a la Backbone.Model.prototype.parsefunción. Entonces, todo lo que tienen que hacer sus modelos es definir los tipos de objeto del submodelo (en su atributo "modelo").
jasop
1
¡Frio! Terminé haciendo algo similar (notablemente y lamentablemente después de encontrar esta respuesta) y lo escribí aquí: blog.untrod.com/2013/08/declarative-approach-to-nesting.html La gran diferencia es que para los modelos profundamente anidados Declaro el mapeo completo a la vez, en el modelo raíz / padre, y el código lo toma desde allí y recorre todo el modelo, hidratando los objetos relevantes en colecciones y modelos de Backbone. Pero realmente un enfoque muy similar.
Chris Clark
16

Estoy publicando este código como un ejemplo de la sugerencia de Peter Lyon para redefinir el análisis. Tenía la misma pregunta y esto funcionó para mí (con un backend de Rails). Este código está escrito en Coffeescript. Hice algunas cosas explícitas para personas que no están familiarizadas con él.

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

o, 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
fuente
Apoyos para el código de ejemplo y sugerencia de anular el análisis. ¡Gracias!
Edward Anderson
11
Sería bueno tener su respuesta en JS real
Jason
6
feliz de tener la versión coffeescript, gracias. Para otros, pruebe js2coffee.org
ABCD.ca
16
Si la pregunta es JS real, también debería ser una respuesta.
Manuel Hernandez
12

Uso Backbone.AssociatedModelde asociaciones troncales :

    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
fuente
Buena solucion. También hay un proyecto similar: github.com/PaulUithol/Backbone-relational
michaelok
11

No estoy seguro de que Backbone tenga una forma recomendada de hacer esto. ¿El objeto Layout tiene su propio ID y registro en la base de datos back-end? Si es así, puede convertirlo en su propio modelo como lo ha hecho. De lo contrario, puede dejarlo como un documento anidado, solo asegúrese de convertirlo hacia y desde JSON correctamente en los métodos savey parse. Si terminas adoptando un enfoque como este, creo que tu ejemplo A es más coherente con el backbone, ya setque se actualizará correctamente attributes, pero de nuevo no estoy seguro de qué hace Backbone con los modelos anidados de forma predeterminada. Es probable que necesite algún código personalizado para manejar esto.

Peter Lyons
fuente
¡Ah! Lo siento, faltaba el newoperador. Lo he editado para corregir este error.
Ross
Oh, entonces malinterpreté tu pregunta. Actualizaré mi respuesta.
Peter Lyons
8

Yo optaría por la Opción B si quieres mantener las cosas simples.

Otra buena opción sería utilizar Backbone-Relational . Simplemente definirías algo como:

var Image = Backbone.Model.extend({
    relations: [
        {
            type: Backbone.HasOne,
            key: 'layout',
            relatedModel: 'Layout'
        }
    ]
});
philfreo
fuente
+1 Backbone-Releational parece bastante establecido: sitio web propio, 1.6k estrellas, más de 200 bifurcaciones.
Ross
6

Utilizo el complemento Backbone DeepModel para modelos y atributos anidados.

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

Puede enlazar para cambiar eventos en niveles de profundidad. por ejemplo: model.on('change:example.nestedmodel.attribute', this.myFunction);

marca
fuente
5

Versión CoffeeScript de la hermosa respuesta 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

¿No es tan dulce? ;)

Dan Fox
fuente
11
No tomo azúcar en mi JavaScript :)
Ross
2

Tuve el mismo problema y he estado experimentando con el código en la respuesta de rycfung , lo cual es una gran sugerencia.
Sin embargo, si usted no quiere a setlos modelos anidados directamente, o no quieren pasar constantemente {parse: true}en el options, otro enfoque sería redefinir setsí.

En Backbone 1.0.0 , setque se llama en constructor, unset, clear, fetchy save.

Considere el siguiente supermodelo , para todos los modelos que necesitan anidar modelos y / o colecciones.

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

Tenga en cuenta que model, _setModely _unsetModelse dejan en blanco a propósito. En este nivel de abstracción, probablemente no pueda definir acciones razonables para las devoluciones de llamada. Sin embargo, es posible que desee anularlos en los submodelos que se extienden CompoundModel.
Esas devoluciones de llamada son útiles, por ejemplo, para vincular oyentes y propagar changeeventos.


Ejemplo:

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 esto, tiene la creación automática de modelos anidados y la propagación de eventos. También se proporciona y prueba el uso de muestra:

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

Salida:

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

Me doy cuenta de que llego tarde a esta fiesta, pero recientemente lanzamos un complemento para tratar exactamente este escenario. Se llama backbone-nestify .

Entonces su modelo anidado permanece sin cambios:

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

Luego use el complemento cuando defina el modelo contenedor (usando Underscore.extend ):

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

Después de eso, asumiendo que tiene un modelo mque es una instancia de Image, y ha configurado el JSON a partir de la pregunta m, puede hacer:

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
fuente
2

Utilice formas de columna vertebral

Admite formularios anidados, modelos y toJSON. TODOS ANIDADOS

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
fuente
1

Si no desea agregar otro marco, es posible considerar la creación de una clase base con anulado sety toJSONy utilizar de esta manera:

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

Necesitará BaseModelesta respuesta (disponible, si lo desea, como esencia ).

Dan Abramov
fuente
1

También tenemos este problema y un trabajador del equipo ha implementado un complemento llamado backbone-nested-attribute.

El uso es muy sencillo. Ejemplo:

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

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

Con esto, el modelo Tree puede acceder a las frutas:

tree.get('fruits')

Puedes ver más información aquí:

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

Gustavo Kloh
fuente