Modelos aninhados em Backbone.js, como abordar

117

Eu tenho o seguinte JSON fornecido de um servidor. Com isso, quero criar um modelo com um modelo aninhado. Não tenho certeza de qual é a maneira de conseguir isso.

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

Quero que eles sejam convertidos em dois modelos de backbone aninhados com a seguinte estrutura:

// structure
Image
    Layout
...

Então, eu defino o modelo de Layout assim:

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

Mas qual das duas técnicas (se houver) abaixo devo usar para definir o modelo de imagem? A ou B abaixo?

UMA

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
fonte

Respostas:

98

Eu tenho o mesmo problema enquanto escrevo meu aplicativo Backbone. Ter que lidar com modelos embutidos / aninhados. Fiz alguns ajustes que achei uma solução bastante elegante.

Sim, você pode modificar o método de análise para alterar os atributos do objeto, mas tudo isso é, na verdade, um código IMO bastante insustentável e parece mais um hack do que uma solução.

Aqui está o que sugiro para o seu exemplo:

Primeiro defina seu modelo de layout como tal.

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

Então aqui está o seu modelo de imagem:

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

Observe que não adulterei o próprio modelo, mas apenas passei de volta o objeto desejado do método de análise.

Isso deve garantir a estrutura do modelo aninhado quando você estiver lendo do servidor. Agora, você notaria que salvar ou configurar não é tratado aqui porque acho que faz sentido para você definir o modelo aninhado explicitamente usando o modelo adequado.

Igual a:

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

Observe também que, na verdade, você está invocando o método de análise em seu modelo aninhado chamando:

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

Você pode definir quantos modelos aninhados no modelcampo forem necessários.

Claro, se você quiser ir tão longe quanto salvar o modelo aninhado em sua própria tabela. Isso não seria suficiente. Mas, no caso de ler e salvar o objeto como um todo, essa solução deve bastar.

Rycfung
fonte
4
Isso é bom ... deve ser a resposta aceita, pois é muito mais limpa do que as outras abordagens. A única sugestão que eu teria é colocar a primeira letra de suas classes que estendem Backbone.Model para facilitar a leitura, ou seja, ImageModel e LayoutModel
Stephen Handley,
1
@StephenHandley Obrigado pelo comentário e sua sugestão. Para a informação, estou usando isso no contexto de requireJS. Portanto, para responder à questão da capitalização, o var 'imageModel' é realmente retornado para requireJS. E a referência ao modelo seria encapsulada pela seguinte construção: define(['modelFile'], function(MyModel){... do something with MyModel}) Mas você está certo. Tenho o hábito de fazer referência ao modelo pela convenção que você sugeriu.
rycfung,
@BobS Desculpe, foi um erro de digitação. Deveria haver resposta. Eu consertei, obrigado por apontar.
rycfung
2
Agradável! Eu recomendo adicionar isso à Backbone.Model.prototype.parsefunção. Então, tudo que seus modelos precisam fazer é definir os tipos de objeto do submodelo (em seu atributo "modelo").
Jasop
1
Legal! Acabei fazendo algo semelhante (notável e lamentavelmente depois de encontrar esta resposta) e escrevi aqui: blog.untrod.com/2013/08/declarative-approach-to-nesting.html A grande diferença é que para modelos profundamente aninhados Eu declaro todo o mapeamento de uma vez, no modelo raiz / pai, e o código o pega de lá e percorre todo o modelo, hidratando objetos relevantes em coleções e modelos de Backbone. Mas realmente uma abordagem muito semelhante.
Chris Clark
16

Estou postando este código como um exemplo da sugestão de Peter Lyon para redefinir a análise. Eu tive a mesma dúvida e isso funcionou para mim (com um backend Rails). Este código foi escrito em Coffeescript. Eu deixei algumas coisas explícitas para pessoas não familiarizadas com ele.

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, em 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
fonte
Suportes para o código de exemplo e sugestão de análise de substituição. Obrigado!
Edward Anderson
11
seria bom ter sua resposta em JS real
Jason
6
feliz por ter a versão coffeescript, obrigado. Para outros, tente js2coffee.org
ABCD.ca
16
Se a pergunta for JS real, uma resposta também deve ser.
Manuel Hernandez
12

Uso Backbone.AssociatedModelde associações de backbone :

    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
fonte
Ótima solução. Existe um projeto semelhante por aí também: github.com/PaulUithol/Backbone-relational
michaelok
11

Não tenho certeza se o próprio Backbone tem uma maneira recomendada de fazer isso. O objeto Layout tem seu próprio ID e registro no banco de dados back end? Nesse caso, você pode torná-lo seu próprio modelo, como você fez. Caso contrário, você pode apenas deixá-lo como um documento aninhado, apenas certifique-se de convertê-lo de e para JSON corretamente nos métodos savee parse. Se você acabar adotando uma abordagem como esta, acho que seu exemplo A é mais consistente com o backbone, pois setserá atualizado corretamente attributes, mas, novamente, não tenho certeza do que o Backbone faz com os modelos aninhados por padrão. É provável que você precise de algum código personalizado para lidar com isso.

Peter Lyons
fonte
Ah! Desculpe, estava faltando a newoperadora. Eu editei para corrigir esse erro.
Ross
Oh, então eu interpretei mal sua pergunta. Vou atualizar minha resposta.
Peter Lyons
8

Eu escolheria a Opção B se você quiser manter as coisas simples.

Outra boa opção seria usar o Backbone-Relacional . Você apenas definiria algo como:

var Image = Backbone.Model.extend({
    relations: [
        {
            type: Backbone.HasOne,
            key: 'layout',
            relatedModel: 'Layout'
        }
    ]
});
philfreo
fonte
+1 Backbone-Releational parece bastante estabelecido: website próprio, 1.6k estrelas, mais de 200 garfos.
Ross
6

Eu uso o plugin Backbone DeepModel para modelos e atributos aninhados.

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

Você pode vincular a eventos de mudança em níveis de profundidade. por exemplo: model.on('change:example.nestedmodel.attribute', this.myFunction);

Marca
fonte
5

Versão CoffeeScript da bela resposta 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ão é fofo? ;)

Dan Fox
fonte
11
Eu não aceito açúcar no meu JavaScript
:)
2

Eu tive o mesmo problema e estou testando o código da resposta de rycfung , o que é uma ótima sugestão.
Se, no entanto, você não quiser setos modelos aninhados diretamente, ou não quiser passar constantemente {parse: true}no options, outra abordagem seria redefinir a setsi mesmo.

Em Backbone 1.0.0 , seté chamado constructor, unset, clear, fetche save.

Considere o seguinte super modelo , para todos os modelos que precisam aninhar modelos e / ou coleções.

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

Observe isso model, _setModele _unsetModelsão deixados em branco propositalmente. Nesse nível de abstração, você provavelmente não pode definir nenhuma ação razoável para os callbacks. No entanto, você pode querer substituí-los nos submodelos dessa extensão CompoundModel.
Esses retornos de chamada são úteis, por exemplo, para vincular ouvintes e propagar changeeventos.


Exemplo:

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

Com isso, você tem criação automática de modelo aninhado e propagação de eventos. O uso de amostra também é fornecido e testado:

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

Resultado:

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

Sei que estou atrasado para esta festa, mas recentemente lançamos um plugin para lidar exatamente com esse cenário. É chamado de backbone-nestify .

Portanto, seu modelo aninhado permanece inalterado:

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

Em seguida, use o plug-in ao definir o modelo contido (usando Underscore.extend ):

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

Depois disso, supondo que você tenha um modelo mque seja uma instância de Imagee tenha definido o JSON a partir da pergunta m, você pode fazer:

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

Use formulários de backbone

Ele suporta formulários aninhados, modelos e toJSON. TODOS ANINHADOS

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

Se você não quiser adicionar outra estrutura, pode considerar a criação de uma classe base com sete substituída toJSONe usá-la assim:

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

Você precisará BaseModeldesta resposta (disponível, se desejar, como uma essência ).

Dan Abramov
fonte
1

Também temos esse problema e um trabalhador da equipe implementou um plugin chamado backbone-nested-atributos.

O uso é muito simples. Exemplo:

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

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

Com isso, o modelo Árvore pode acessar os frutos:

tree.get('fruits')

Você pode ver mais informações aqui:

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

Gustavo Kloh
fonte