Verschachtelte Modelle in Backbone.js, Vorgehensweise

117

Ich habe den folgenden JSON von einem Server bereitgestellt. Damit möchte ich ein Modell mit einem verschachtelten Modell erstellen. Ich bin mir nicht sicher, wie ich das erreichen kann.

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

Ich möchte, dass diese in zwei verschachtelte Backbone-Modelle mit der folgenden Struktur konvertiert werden:

// structure
Image
    Layout
...

Also definiere ich das Layoutmodell wie folgt:

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

Aber welche der beiden folgenden Techniken (falls vorhanden) sollte ich verwenden, um das Bildmodell zu definieren? A oder B unten?

EIN

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

oder B.

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

Antworten:

98

Ich habe das gleiche Problem, während ich meine Backbone-Anwendung schreibe. Sich mit eingebetteten / verschachtelten Modellen befassen müssen. Ich habe einige Verbesserungen vorgenommen, die ich für eine recht elegante Lösung hielt.

Ja, Sie können die Analysemethode ändern, um Attribute im Objekt zu ändern, aber all das ist eigentlich ziemlich nicht wartbarer Code IMO und fühlt sich eher wie ein Hack als wie eine Lösung an.

Folgendes schlage ich für Ihr Beispiel vor:

Definieren Sie zuerst Ihr Layoutmodell so.

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

Dann ist hier dein Bildmodell:

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

Beachten Sie, dass ich das Modell selbst nicht manipuliert habe, sondern lediglich das gewünschte Objekt von der Analysemethode zurückgegeben habe.

Dies sollte die Struktur des verschachtelten Modells sicherstellen, wenn Sie vom Server lesen. Jetzt werden Sie feststellen, dass das Speichern oder Einstellen hier tatsächlich nicht behandelt wird, da ich der Meinung bin, dass es für Sie sinnvoll ist, das verschachtelte Modell explizit mit dem richtigen Modell festzulegen.

Wie so:

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

Beachten Sie außerdem, dass Sie die Analysemethode in Ihrem verschachtelten Modell tatsächlich aufrufen, indem Sie Folgendes aufrufen:

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

Sie können so viele verschachtelte Modelle im modelFeld definieren, wie Sie benötigen.

Natürlich, wenn Sie das verschachtelte Modell in einer eigenen Tabelle speichern möchten. Das würde nicht ausreichen. Beim Lesen und Speichern des gesamten Objekts sollte diese Lösung jedoch ausreichen.

rycfung
quelle
4
Das ist schön .. sollte die akzeptierte Antwort sein, da es weitaus sauberer ist als die anderen Ansätze. Ich hätte nur Vorschläge, den ersten Buchstaben Ihrer Klassen, die Backbone.Model für die Lesbarkeit erweitern, groß zu schreiben. Das heißt ImageModel und LayoutModel
Stephen Handley
1
@ StephenHandley Danke für den Kommentar und deinen Vorschlag. Zur Information, ich benutze dies tatsächlich im Kontext von requireJS. Um auf die Frage der Großschreibung zu antworten, wird die Variable 'imageModel' tatsächlich an requireJS zurückgegeben. Und der Verweis auf das Modell würde durch das folgende Konstrukt gekapselt: define(['modelFile'], function(MyModel){... do something with MyModel}) Aber Sie haben Recht. Ich mache es mir zur Gewohnheit, das Modell anhand der von Ihnen vorgeschlagenen Konvention zu referenzieren.
Rycfung
@ BobS Sorry, war ein Tippfehler. Hätte eine Antwort sein sollen. Ich habe es behoben, danke für den Hinweis.
Rycfung
2
Nett! Ich empfehle, dies der Backbone.Model.prototype.parseFunktion hinzuzufügen . Dann müssen Ihre Modelle lediglich die Objekttypen des Untermodells definieren (in Ihrem Attribut "Modell").
Jasop
1
Cool! Ich habe etwas Ähnliches getan (insbesondere und bedauerlicherweise, nachdem ich diese Antwort gefunden habe) und es hier geschrieben: blog.untrod.com/2013/08/declarative-approach-to-nesting.html Der große Unterschied besteht darin, dass es sich um tief verschachtelte Modelle handelt Ich deklariere das gesamte Mapping auf einmal im Root- / Parent-Modell, und der Code nimmt es von dort und geht das gesamte Modell entlang, wobei relevante Objekte in Backbone-Sammlungen und -Modelle hydratisiert werden. Aber wirklich ein sehr ähnlicher Ansatz.
Chris Clark
16

Ich poste diesen Code als Beispiel für den Vorschlag von Peter Lyon, die Analyse neu zu definieren. Ich hatte die gleiche Frage und dies funktionierte für mich (mit einem Rails-Backend). Dieser Code ist in Coffeescript geschrieben. Ich habe ein paar Dinge für Leute explizit gemacht, die damit nicht vertraut sind.

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

oder 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;
};
Eric Hu
quelle
Requisiten für den Beispielcode und Vorschläge zum Überschreiben der Analyse. Vielen Dank!
Edward Anderson
11
wäre schön, Ihre Antwort in echtem JS zu haben
Jason
6
Ich bin froh, die Coffeescript-Version zu haben, danke. Für andere versuchen Sie js2coffee.org
ABCD.ca
16
Wenn die Frage echt JS ist, sollte auch eine Antwort sein.
Manuel Hernandez
12

Verwendung Backbone.AssociatedModelvon Backbone-Assoziationen :

    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
quelle
Schöne Lösung. Es gibt auch ein ähnliches Projekt: github.com/PaulUithol/Backbone-relational
michaelok
11

Ich bin nicht sicher, ob Backbone selbst einen empfohlenen Weg hat, dies zu tun. Hat das Layout-Objekt eine eigene ID und einen eigenen Datensatz in der Back-End-Datenbank? Wenn ja, können Sie es zu einem eigenen Modell machen, wie Sie es haben. Wenn nicht, können Sie einfach lassen Sie es als verschachteltes Dokument, so stellen Sie sicher , konvertieren Sie es in und aus JSON richtig in den saveund parseMethoden. Wenn Sie am Ende einen solchen Ansatz wählen, ist Ihr A- Beispiel meiner Meinung nach konsistenter mit dem Backbone, da setes ordnungsgemäß aktualisiert attributeswird. Ich bin mir jedoch nicht sicher, was das Backbone standardmäßig mit verschachtelten Modellen macht. Es ist wahrscheinlich, dass Sie dafür benutzerdefinierten Code benötigen.

Peter Lyons
quelle
Ah! Entschuldigung, es fehlte der newOperator. Ich habe es bearbeitet, um diesen Fehler zu beheben.
Ross
Oh, dann habe ich deine Frage falsch interpretiert. Ich werde meine Antwort aktualisieren.
Peter Lyons
8

Ich würde Option B wählen, wenn Sie die Dinge einfach halten möchten.

Eine weitere gute Option wäre die Verwendung von Backbone-Relational . Sie würden einfach so etwas definieren wie:

var Image = Backbone.Model.extend({
    relations: [
        {
            type: Backbone.HasOne,
            key: 'layout',
            relatedModel: 'Layout'
        }
    ]
});
Philfreo
quelle
+1 Backbone-Releational scheint ziemlich etabliert zu sein: eigene Website, 1,6.000 Sterne, mehr als 200 Gabeln.
Ross
6

Ich verwende das Backbone DeepModel-Plugin für verschachtelte Modelle und Attribute.

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

Sie können binden, um Ereignisse und Ebenen tief zu ändern. beispielsweise: model.on('change:example.nestedmodel.attribute', this.myFunction);

Kennzeichen
quelle
5

CoffeeScript-Version von Rycfung's schöner Antwort:

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

Ist das nicht süß? ;)

Dan Fox
quelle
11
Ich nehme keinen Zucker in mein JavaScript
Ross
2

Ich hatte das gleiche Problem und habe mit dem Code in Rycfung's Antwort experimentiert , was ein großartiger Vorschlag ist.
Wenn aber wollen Sie nicht zu setden verschachtelten Modelle direkt, oder wollen nicht ständig passieren {parse: true}in der options, wäre ein weiterer Ansatz, neu zu definieren , setselbst.

In Backbone 1.0.0 , setheißt in constructor, unset, clear, fetchund save.

Betrachten Sie das folgende Supermodell für alle Modelle, die Modelle und / oder Sammlungen verschachteln müssen.

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

Beachten Sie, dass model, _setModelund _unsetModelsind leer absichtlich links. Auf dieser Abstraktionsebene können Sie wahrscheinlich keine vernünftigen Aktionen für die Rückrufe definieren. Möglicherweise möchten Sie sie jedoch in den erweiterten Untermodellen überschreiben CompoundModel.
Diese Rückrufe sind beispielsweise nützlich, um Listener zu binden und changeEreignisse zu verbreiten.


Beispiel:

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

Damit haben Sie die automatische Erstellung verschachtelter Modelle und die Weitergabe von Ereignissen. Die Verwendung von Beispielen wird ebenfalls bereitgestellt und getestet:

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

Ausgabe:

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

Mir ist klar, dass ich zu spät zu dieser Party komme, aber wir haben kürzlich ein Plugin veröffentlicht, um genau dieses Szenario zu behandeln. Es heißt Backbone-Nestify .

Ihr verschachteltes Modell bleibt also unverändert:

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

Verwenden Sie dann das Plugin, um das enthaltene Modell zu definieren (mit Underscore.extend ):

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

Angenommen, Sie haben ein Modell, mdas eine Instanz von ist Image, und Sie haben den JSON von der Frage an festgelegt m, können Sie Folgendes tun:

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

Verwenden Sie Backbone-Formulare

Es unterstützt verschachtelte Formulare, Modelle und toJSON. Alles verschachtelt

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

Wenn Sie nicht noch einen weiteren Rahmen hinzufügen möchten, könnten Sie eine Basisklasse mit überschriebene Erstellung setund toJSONund verwenden Sie es wie folgt aus :

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

Sie benötigen BaseModelaus dieser Antwort (verfügbar, wenn Sie möchten , als Kern ).

Dan Abramov
quelle
1

Wir haben auch dieses Problem und ein Teamarbeiter hat ein Plugin namens Backbone-Nested-Attribute implementiert.

Die Bedienung ist sehr einfach. Beispiel:

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

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

Damit kann das Baummodell dann auf Früchte zugreifen:

tree.get('fruits')

Weitere Informationen finden Sie hier:

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

Gustavo Kloh
quelle