javascriptbackbone.jsbackbone-relational

Creating nested models in Backbone with Backbone-relational


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:

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

But I'm still not sure how to:


Solution

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