Javascript – Backbone.js Adding Model to Collection Issue

backbone.jsjavascriptjavascript-frameworkjquery

I am building a test application in Backbone.js (my first app using Backbone). The app goes like this:

  1. Load Data from server "Plans"
  2. Build list of plans and show to screen
  3. There is a button to add a new plan
  4. Once new plan is added, add to collection ( do not save to server as of now )
  5. redirect to index page and show the new collection ( includes the plan you just added)

My issue is with item 5. When I save a plan, I add the model to the collection then redirect to the initial view. At this point, I fetch data from the server. When I fetch data from the server, this overwrites my collection and my added model is gone.

How can I prevent this from happening? I have found a way to do this but it is definitely not the correct way at all. Below you will find my code examples for this. Thanks for the help.

PlansListView View:

var PlansListView = Backbone.View.extend({

    tagName : 'ul',

    initialize : function()
    {
        _.bindAll( this, 'render', 'close' );
        //reset the view if the collection is reset 
        this.collection.bind( 'reset', this.render , this );

    },
    render : function()
    {
        _.each( this.collection.models, function( plan ){

            $( this.el ).append( new PlansListItemView({ model: plan }).render().el );

        }, this );

        return this;
    },
    close : function()
    {
        $( this.el ).unbind();
        $( this.el ).remove();  

    }   
});//end

NewPlanView Save Method

var NewPlanView = Backbone.View.extend({

    tagName : 'section',
    template : _.template( $( '#plan-form-template' ).html() ),
    events : {
        'click button.save' : 'savePlan',
        'click button.cancel' : 'cancel'

    },
    intialize: function()
    {
        _.bindAll( this, 'render', 'save', 'cancel' );  

    },
    render : function()
    {
        $( '#container' ).append( $( this.el ).html(this.template( this.model.toJSON() )) );                                    
        return this;
    },
    savePlan : function( event )
    {

        this.model.set({

            name : 'bad plan',
            date : 'friday',
            desc : 'blah',
            id : Math.floor(Math.random()*11),
            total_stops : '2'           
        });

        this.collection.add( this.model );

        app.navigate('', true );
        event.preventDefault();

    },
    cancel : function(){}
});

Router (default method):

    index : function()
    {
        this.container.empty();
        var self = this;

        //This is a hack to get this to work
        //on default page load fetch all plans from the server
        //if the page has loaded ( this.plans is defined) set the updated plans collection to the view
        //There has to be a better way!!
        if( ! this.plans )
        {
            this.plans = new Plans();


            this.plans.fetch({

                success: function()
                {
                    self.plansListView = new PlansListView({ collection : self.plans });
                    $( '#container' ).append( self.plansListView.render().el );
                    if( self.requestedID ) self.planDetails( self.requestedID );
                }   
            });
        }
        else
        {
            this.plansListView = new PlansListView({ collection : this.plans });
            $( '#container' ).append( self.plansListView.render().el );
            if( this.requestedID ) self.planDetails( this.requestedID );

        }
    },

New Plan Route:

    newPlan : function()
    {   var plan = new Plan({name: 'Cool Plan', date: 'Monday', desc: 'This is a great app'});
        this.newPlan = new NewPlanView({ model : plan, collection: this.plans });
        this.newPlan.render();
    }   

FULL CODE
( function( $ ){

var Plan = Backbone.Model.extend({

    defaults: {
        name : '',
        date : '',
        desc : ''   
    }   
});

var Plans = Backbone.Collection.extend({

    model : Plan,
    url : '/data/'  

});     






$( document ).ready(function( e ){

    var PlansListView = Backbone.View.extend({

        tagName : 'ul',

        initialize : function()
        {
            _.bindAll( this, 'render', 'close' );
            //reset the view if the collection is reset 
            this.collection.bind( 'reset', this.render , this );

        },
        render : function()
        {
            _.each( this.collection.models, function( plan ){

                $( this.el ).append( new PlansListItemView({ model: plan }).render().el );

            }, this );

            return this;
        },
        close : function()
        {
            $( this.el ).unbind();
            $( this.el ).remove();  

        }   
    });//end

    var PlansListItemView = Backbone.View.extend({

        tagName : 'li',
        template : _.template( $( '#list-item-template' ).html() ),
        events :{

            'click a' : 'listInfo'  
        },
        render : function()
        {
            $( this.el ).html( this.template( this.model.toJSON() ) );
            return this;            
        },
        listInfo : function( event )
        {


        }

    });//end


    var PlanView = Backbone.View.extend({

        tagName : 'section',
        events : {
            'click button.add-plan' : 'newPlan'
        },
        template: _.template( $( '#plan-template' ).html() ),
        initialize: function()
        {
            _.bindAll( this, 'render', 'close', 'newPlan' );            
        },
        render : function()
        {
            $( '#container' ).append( $( this.el ).html( this.template( this.model.toJSON() ) ) );
            return this;
        },
        newPlan : function( event )
        {
            app.navigate( 'newplan', true );
        },
        close : function()
        {
            $( this.el ).unbind();
            $( this.el ).remove();  
        }   

    });//end


    var NewPlanView = Backbone.View.extend({

        tagName : 'section',
        template : _.template( $( '#plan-form-template' ).html() ),
        events : {
            'click button.save' : 'savePlan',
            'click button.cancel' : 'cancel'

        },
        intialize: function()
        {
            _.bindAll( this, 'render', 'save', 'cancel' );  

        },
        render : function()
        {
            $( '#container' ).append( $( this.el ).html(this.template( this.model.toJSON() )) );                                    
            return this;
        },
        savePlan : function( event )
        {

            this.model.set({

                name : 'bad plan',
                date : 'friday',
                desc : 'blah',
                id : Math.floor(Math.random()*11),
                total_stops : '2'           
            });

            this.collection.add( this.model );

            app.navigate('', true );
            event.preventDefault();

        },
        cancel : function(){}
    });



    var AppRouter = Backbone.Router.extend({

        container : $( '#container' ),

        routes : {

            ''              : 'index',
            'viewplan/:id'  : 'planDetails',
            'newplan'           : 'newPlan'
        },
        initialize: function(){

        },
        index : function()
        {
            this.container.empty();
            var self = this;

            //This is a hack to get this to work
            //on default page load fetch all plans from the server
            //if the page has loaded ( this.plans is defined) set the updated plans collection to the view
            //There has to be a better way!!
            if( ! this.plans )
            {
                this.plans = new Plans();


                this.plans.fetch({

                    success: function()
                    {
                        self.plansListView = new PlansListView({ collection : self.plans });
                        $( '#container' ).append( self.plansListView.render().el );
                        if( self.requestedID ) self.planDetails( self.requestedID );
                    }   
                });
            }
            else
            {
                this.plansListView = new PlansListView({ collection : this.plans });
                $( '#container' ).append( self.plansListView.render().el );
                if( this.requestedID ) self.planDetails( this.requestedID );

            }
        },

        planDetails : function( id )
        {

            if( this.plans )
            {
                this.plansListView.close();
                this.plan = this.plans.get( id );

                if( this.planView ) this.planView.close();
                    this.planView = new PlanView({ model : this.plan });
                this.planView.render();
            }
            else{

                this.requestedID = id;
                this.index();
            }

            if( ! this.plans ) this.index();
        },

        newPlan : function()
        {   var plan = new Plan({name: 'Cool Plan', date: 'Monday', desc: 'This is a great app'});
            this.newPlan = new NewPlanView({ model : plan, collection: this.plans });
            this.newPlan.render();
        }   
    });



    var app = new AppRouter();
    Backbone.history.start();

}); 








})( jQuery );

Best Answer

Are you fetching the data every time you hit the index page because you want the list to be in sync with the database (i.e. someone else added an item and you want it reflected)?

If that is not the case, what you are doing seems fine to me; you are only fetching when the data does not exist.

If you, however, do want the list to be in sync; but you still wish to have your newly added items rendered among them; you can either merge from the fetched list or use another list to hold the newly added data.

If you use another list, you just have to render both list; the unsaved list should probably exist either at the top or the bottom of the persisted list, grouped together.