Creating nested models in Backbone with Backbone-relational
Asked Answered
I

4

12

I would like to use backbone-relational to have nested models in my backbone.js application.

I have been able to follow the examples in the documentation to create nested objects (e.g. one-to-many relations). However I don't understand how to bind the lower level elements in a way that will update the upper level objects. I think a working application would be a very helpful tutorial.

So my question is: How do I extend the Todos tutorial using backbone-relational so that:

  • one can add/remove subitems for each item
  • double clicking on any subitem edits it (just like the original Todo example)
  • clicking on an item hides/reveals its subitems
  • subitems are not fetched separately but are simply an array attribute of Todo items

Update: I have created a jsfiddle for this question. So far I have:

  • Imported the Todo example mentioned above
  • Created a TodoSubitem model and a TodoSubitemList collection
  • Altered the Todo model to extend RelationalModel instead of Model, with a HasMany relation to TodoSubitem
  • Added a subitem-template in the html code

But I'm still not sure how to:

  • add an input field for subitems that appears only when you click a Todo div
  • have subitem data as an attribute of Todo objects, but still have TodoSubitemView bind DOM elements to them (e.g. <li> tags).
Irisirisa answered 29/8, 2011 at 8:4 Comment(0)
F
11

I don't think I'd create a separate 'TodoSubItem' in this case - why not create a HasMany relation from Todo->Todo, so a Todo can have 0..* children, and 0..1 parent?

This way, you can re-use the order logic (if you change it to apply per collection), can create deeper nesting levels as desired (or limit that to a certain depth, if you want as well), etc. A number of things will need to be updated though, to accomodate this - for example, keep a list of child views so you can loop over them to mark each as done, and maintaining (and updating from) an ordering per TodoList.

Anyway, a rough outline of a possible solution to get you started, as a sort of diff with your current version (sorry, it's completely untested and could thus contain horrible mistakes):

//Our basic **Todo** model has `text`, `order`, and `done` attributes.
window.Todo = Backbone.RelationalModel.extend({

    relations: [{
        type: Backbone.HasMany,
        key: 'children',
        relatedModel: 'Todo',
        collectionType: 'TodoList',
        reverseRelation: {
            key: 'parent',
            includeInJSON: 'id'
        }
    }],

    initialize: function() {
        if ( !this.get('order') && this.get( 'parent' ) ) {
            this.set( { order: this.get( 'parent' ).nextChildIndex() } );
        }
    },

    // Default attributes for a todo item.
    defaults: function() {
        return { done: false };
    },

    // Toggle the `done` state of this todo item.
    toggle: function() {
        this.save({done: !this.get("done")});
    }

    nextChildIndex: function() {
        var children = this.get( 'children' );
        return children && children.length || 0;
    }
});


// The DOM element for a todo item...
window.TodoView = Backbone.View.extend({

    //... is a list tag.
    tagName:  "li",

    // Cache the template function for a single item.
    template: _.template($('#item-template').html()),

    // The DOM events specific to an item.
    events: {
        'click': 'toggleChildren',
        'keypress input.add-child': 'addChild',
        "click .check"              : "toggleDone",
        "dblclick div.todo-text"    : "edit",
        "click span.todo-destroy"   : "clear",
        "keypress .todo-input"      : "updateOnEnter"
    },

    // The TodoView listens for changes to its model, re-rendering.
    initialize: function() {
        this.model.bind('change', this.render, this);
        this.model.bind('destroy', this.remove, this);

        this.model.bind( 'update:children', this.renderChild );
        this.model.bind( 'add:children', this.renderChild );

        this.el = $( this.el );

        this.childViews = {};
    },

    // Re-render the contents of the todo item.
    render: function() {
        this.el.html(this.template(this.model.toJSON()));
        this.setText();

        // Might want to add this to the template of course
        this.el.append( '<ul>', { 'class': 'children' } ).append( '<input>', { type: 'text', 'class': 'add-child' } );

        _.each( this.get( 'children' ), function( child ) {
            this.renderChild( child );
        }, this );

        return this;
    },

    addChild: function( text) {
        if ( e.keyCode == 13 ) {
            var text = this.el.find( 'input.add-child' ).text();
            var child = new Todo( { parent: this.model, text: text } );
        }
    },

    renderChild: function( model ) {
        var childView = new TodoView( { model: model } );
        this.childViews[ model.cid ] = childView;
        this.el.find( 'ul.children' ).append( childView.render() );
    },

    toggleChildren: function() {
        $(this.el).find( 'ul.children' ).toggle();
    },

    // Toggle the `"done"` state of the model.
    toggleDone: function() {
        this.model.toggle();
        _.each( this.childViews, function( child ) {
            child.model.toggle();
        });
    },

    clear: function() {
        this.model.set( { parent: null } );
        this.model.destroy();
    }

    // And so on...
});
Fylfot answered 31/8, 2011 at 21:27 Comment(3)
Thanks Paul! I will try to incorporate this into my example.Irisirisa
Full code example would be good - I can't make sense of this.Isochromatic
Unless I am mistaken, this doesn't work, because Backbone-relational needs the relatedModel to be defined before it can create the relationship between the models.Winther
W
3

I don't think you can make self-relating models in Backbone-relational (as described an the other answer here). When I have tried this, I get an error: Backbone-relational needs the relatedModel to be defined before it can create relationships with it.

So, I've modified the many-to-many pattern described on the backbone-relational page:

https://github.com/PaulUithol/Backbone-relational#many-to-many-relations

In essence, I am creating a linking model to contain references to the model being referred to, so that this link model can be available to Backbone-relational when it is defining the actual model.

I find it convenient to give this link model a separate relationship with both data models in the relationship, so that either can perform look relational look ups. Alternately, you could simply stuff the second model inside the link model, but then the relationship would be one directional unless you explicitly add your own references to the link model in the data model.

Let us create a 'Person' model that has children who are other 'Person' models.

Person = Backbone.RelationalModel.extend({
relations: [
    {
        type: 'HasMany',
        key: 'Children',
        relatedModel: 'FamilyRelation',
        reverseRelation: {
            key: 'Childrenof'
        }
    },
    {
        type: 'HasMany',
        key: 'Parent',
        relatedModel: 'FamilyRelation',
        reverseRelation: {
            key: 'Parentof'
        }
    }
]
});

FamilyRelation needs to be defined >before< Person is, so Backbone-relational can create the links, so this goes before the Person model definition in your code:

// FamilyRelation is link model between two "Person"s 
// to achieve the Fan/Admiree relation.

FamilyRelation = Backbone.RelationalModel.extend({
})

If we create two "Person"s:

KingKong = new Person({name: 'KingKong'});
SonOfKong = new Person({name: 'SonOfKong'});

Then we can create a FamilyRelationship model that is the 'parentof' SonOfKong, and add it to KingKong's children with this line:

KingKong.get("children").add({"parentof":SonOfKong});

You can then add convenience functions to the Person model, to retrieve the nested models from the FamilyRelationship model, and don't really need to touch FamilyRelation any more, except to make sure it's being saved and retrieved appropriately.

For non-hierarchical relationships (say 'Friend', rather than 'Parent/Child', you still need these two relationships with the linking model in order to be able to retrieve one from the other, which is a bit of a hack, but it works.

Winther answered 9/11, 2012 at 17:31 Comment(0)
P
3

After some fiddling I have found a way to create a true nested model:

var theModel = Backbone.RelationalModel.extend({ [...] });
theModel.prototype.relations.push({
  type: Backbone.HasOne,
  key: 'key',
  relatedModel: theModel
});

At the point where the model is used (when pushing to the relations on the prototype) it is available, thus making everything work.

Penitentiary answered 19/6, 2014 at 14:55 Comment(1)
Wow, so simple. I wish I had found this a few hours ago.Dynamic
S
0

this post is pretty old by now, but I was searching for the same thing and thought I would share the solution I got.

To create a self-referencing model you simply omit relatedModel. So something like this:

Person = Backbone.RelationalModel.extend({ relations: [{ type: 'HasMany', key: 'Children', }] })

It is explained in the docs

Spectre answered 17/6, 2016 at 17:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.