Backbone.js obtiene y establece el atributo de objeto anidado

105

Tengo una pregunta simple sobre las funciones get y set de Backbone.js .

1) Con el siguiente código, ¿cómo puedo 'obtener' o 'configurar' obj1.myAttribute1 directamente?

Otra pregunta:

2) En el modelo, además del objeto de valores predeterminados , ¿dónde puedo / debo declarar los otros atributos de mi modelo, de modo que se pueda acceder a ellos a través de los métodos get y set de Backbone?

var MyModel = Backbone.Model.extend({
    defaults: {
        obj1 : {
            "myAttribute1" : false,
            "myAttribute2" : true,
        }
    }
})

var MyView = Backbone.View.extend({
    myFunc: function(){
        console.log(this.model.get("obj1"));
        //returns the obj1 object
        //but how do I get obj1.myAttribute1 directly so that it returns false?
    }
});

Sé que puedo hacer:

this.model.get("obj1").myAttribute1;

pero ¿es una buena práctica?

fortuna arroz
fuente
3
Si bien no es una respuesta a la pregunta: siempre que se especifique un objeto (cualquier cosa pasada por referencia) en defaults(obj1 en este caso), ese mismo objeto se compartirá en todas las instancias del modelo. La práctica actual es definir defaultscomo una función que devuelve un objeto para ser utilizado como predeterminado. backbonejs.org/#Model-defaults (ver la nota en cursiva)
Jonathan F
1
@JonathanF Los comentarios no son para respuestas, por lo que nunca necesitó la declaración :)
TJ

Respuestas:

144

Si this.model.get("obj1").myAttribute1bien está bien, es un poco problemático porque podría tener la tentación de hacer el mismo tipo de cosas para el conjunto, es decir

this.model.get("obj1").myAttribute1 = true;

Pero si hace esto, no obtendrá los beneficios de los modelos Backbone myAttribute1, como eventos de cambio o validación.

Una mejor solución sería nunca anidar POJSO ("objetos JavaScript antiguos y simples") en sus modelos y, en su lugar, anidar clases de modelos personalizadas. Entonces se vería algo como esto:

var Obj = Backbone.Model.extend({
    defaults: {
        myAttribute1: false,
        myAttribute2: true
    }
});

var MyModel = Backbone.Model.extend({
    initialize: function () {
        this.set("obj1", new Obj());
    }
});

Entonces el código de acceso sería

var x = this.model.get("obj1").get("myAttribute1");

pero lo que es más importante, el código de configuración sería

this.model.get("obj1").set({ myAttribute1: true });

que desencadenará eventos de cambio apropiados y similares. Ejemplo de trabajo aquí: http://jsfiddle.net/g3U7j/

Domenic
fuente
24
A esta respuesta, agregaría la advertencia de que esta solución se tambalea sobre las violaciones generalizadas de la Ley de Demeter. Consideraría agregar métodos de conveniencia que oculten la navegación al objeto anidado. Básicamente, las personas que llaman no necesitan conocer la estructura interna del modelo; después de todo, puede cambiar y las personas que llaman no deberían saberlo.
Bill Eisenhauer
7
No puedo hacer que esto funcione para mí. Lanza error:Uncaught TypeError: Object #<Object> has no method 'set'
wilsonpage
1
@ChristianNunciato, pagewil, Benno: Parece que no entendiste el punto de la publicación, que es anidar modelos Backbone dentro de modelos Backbone. No anide objetos simples dentro de los modelos Backbone. Ejemplo de trabajo aquí: jsfiddle.net/g3U7j
Domenic
1
No inspeccioné el código backbone.js, pero a partir de mi prueba, si tiene un modelo personalizado anidado y cambia una propiedad del mismo con set (), su modelo padre no activará un evento de 'cambio' en sí mismo; Tuve que despedir el evento yo mismo. Realmente debería inspeccionar el código, pero ¿también lo entiendes?
Tom
2
@tom eso es correcto. Backbone no es un caso especial para cuando las propiedades de los modelos son instancias de Backbone.Model, y luego comienzan a generar eventos mágicos.
Domenic
74

Creé backbone-deep-model para esto: simplemente extienda Backbone.DeepModel en lugar de Backbone.Model y luego puede usar rutas para obtener / establecer atributos de modelo anidado. También mantiene eventos de cambio.

model.bind('change:user.name.first', function(){...});
model.set({'user.name.first': 'Eric'});
model.get('user.name.first'); //Eric
maldad
fuente
1
Sí, si miras la API, hay un ejemplo como//You can use index notation to fetch from arrays console.log(model.get('otherSpies.0.name')) //'Lana'
tawheed
¡Funciona genial! Pero, ¿la línea 2 de su ejemplo requiere dos puntos en lugar de una coma?
mariachi
16

La solución de Domenic funcionará, sin embargo, cada nuevo MyModel apuntará a la misma instancia de Obj. Para evitar esto, MyModel debería verse así:

var MyModel = Backbone.Model.extend({
  initialize: function() {
     myDefaults = {
       obj1: new Obj()
     } 
     this.set(myDefaults);
  }
});

Consulte la respuesta de c3rin @ https://stackoverflow.com/a/6364480/1072653 para obtener una explicación completa.

Oxidado
fuente
1
Para los futuros lectores, mi respuesta se ha actualizado para incorporar lo mejor de la respuesta de Rusty.
Domenic
2
El autor de la pregunta debe marcar esto como la respuesta aceptada. Domenic's es un gran comienzo, pero esto resuelve un problema.
Jon Raasch
5

Yo utilizo este enfoque.

Si tiene un modelo Backbone como este:

var nestedAttrModel = new Backbone.Model({
    a: {b: 1, c: 2}
});

Puede establecer el atributo "ab" con:

var _a = _.omit(nestedAttrModel.get('a')); // from underscore.js
_a.b = 3;
nestedAttrModel.set('a', _a);

Ahora su modelo tendrá atributos como:

{a: {b: 3, c: 2}}

con el evento "cambiar" activado.

Natthakit Susanthitanon
fuente
1
¿Estas seguro acerca de esto? Esto no funciona para mi. meta2= m.get('x'); meta2.id=110; m.set('x', meta2). Esto no desencadena ningún evento de cambio para mí :(
HungryCoder
1
Veo que funciona cuando clono el atributo como _.clone(m.get('x')). gracias
HungryCoder
Gracias @HungryCoder, también funcionó para mí cuando fue clonado. Backbone debe comparar el objeto que está settingcon el objeto que está gettingen el momento establecido. Entonces, si no clona los dos objetos, los dos objetos que se comparan son exactamente iguales en el momento establecido.
Derek Dahmer
Recuerde que los objetos se pasan por referencia y son mutables, a diferencia de las primitivas numéricas y de cadena. Los métodos set y constructor de Backbone intentan un clon superficial de cualquier referencia de objeto pasada como argumento. Cualquier referencia a otros objetos en las propiedades de ese objeto no se clona. Cuando lo configura y lo recupera, la referencia es la misma, lo que significa que puede mutar el modelo sin activar un cambio.
niall.campbell
3

Hay una solución en la que nadie ha pensado todavía y que es muy útil. De hecho, no puede establecer atributos anidados directamente, a menos que use una biblioteca de terceros que probablemente no desee. Sin embargo, lo que puede hacer es hacer un clon del diccionario original, establecer la propiedad anidada allí y luego configurar todo el diccionario. Pedazo de pastel.

//How model.obj1 looks like
obj1: {
    myAttribute1: false,
    myAttribute2: true,
    anotherNestedDict: {
        myAttribute3: false
    }
}

//Make a clone of it
var cloneOfObject1 = JSON.parse(JSON.stringify(this.model.get('obj1')));

//Let's day we want to change myAttribute1 to false and myAttribute3 to true
cloneOfObject1.myAttribute2 = false;
cloneOfObject1.anotherNestedDict.myAttribute3 = true;

//And now we set the whole dictionary
this.model.set('obj1', cloneOfObject1);

//Job done, happy birthday
bicicleta
fuente
2

Tuve el mismo problema que @pagewil y @Benno con la solución de @ Domenic. Mi respuesta fue en su lugar escribir una subclase simple de Backbone.Model que solucione el problema.

// Special model implementation that allows you to easily nest Backbone models as properties.
Backbone.NestedModel = Backbone.Model.extend({
    // Define Backbone models that are present in properties
    // Expected Format:
    // [{key: 'courses', model: Course}]
    models: [],

    set: function(key, value, options) {
        var attrs, attr, val;

        if (_.isObject(key) || key == null) {
            attrs = key;
            options = value;
        } else {
            attrs = {};
            attrs[key] = value;
        }

        _.each(this.models, function(item){
            if (_.isObject(attrs[item.key])) {
                attrs[item.key] = new item.model(attrs[item.key]);
            }
        },this);

        return Backbone.Model.prototype.set.call(this, attrs, options);
    }
});

var Obj = Backbone.Model.extend({
    defaults: {
        myAttribute1: false,
        myAttribute2: true
    }
});

var MyModel = Backbone.NestedModel.extend({
    defaults: {
        obj1: new Obj()
    },

    models: [{key: 'obj1', model: Obj}]
});

Lo que NestedModel hace por usted es permitir que funcionen (que es lo que sucede cuando myModel se configura a través de datos JSON):

var myModel = new MyModel();
myModel.set({ obj1: { myAttribute1: 'abc', myAttribute2: 'xyz' } });
myModel.set('obj1', { myAttribute1: 123, myAttribute2: 456 });

Sería fácil generar la lista de modelos automáticamente al inicializar, pero esta solución fue lo suficientemente buena para mí.

Cory Gagliardi
fuente
2

La solución propuesta por Domenic tiene algunos inconvenientes. Digamos que quiere escuchar el evento "cambiar". En ese caso, el método 'initialize' no se activará y su valor personalizado para el atributo se reemplazará con el objeto json del servidor. En mi proyecto me enfrenté a este problema. Mi solución para anular el método 'establecer' del modelo:

set: function(key, val, options) {
    if (typeof key === 'object') {
        var attrs = key;
        attrs.content = new module.BaseItem(attrs.content || {});
        attrs.children = new module.MenuItems(attrs.children || []);
    }

    return Backbone.Model.prototype.set.call(this, key, val, options);
}, 
yuliskov
fuente
0

Si bien en algunos casos el uso de modelos Backbone en lugar de atributos de objeto anidados tiene sentido como mencionó Domenic, en casos más simples, podría crear una función de establecimiento en el modelo:

var MyModel = Backbone.Model.extend({
    defaults: {
        obj1 : {
            "myAttribute1" : false,
            "myAttribute2" : true,
        }
    },
    setObj1Attribute: function(name, value) {
        var obj1 = this.get('obj1');
        obj1[name] = value;
        this.set('obj1', obj1);
    }
})
Ibrahim Muhammad
fuente
0

Si interactúa con el backend, lo que requiere un objeto con estructura de anidamiento. Pero con backbone más fácil de trabajar con estructura lineal.

backbone.linear puede ayudarte.

noche
fuente