Backbone.js Wine Cellar Tutorial — Part 3: Deep Linking and Application States

UPDATE: I posted a “Postface” to this series with some lessons learned and an improved version of the app. Make sure you read it here.

In Part 1 of this tutorial, we set up the basic infrastructure for the Wine Cellar application. In Part 2, we added the ability to create, update, and delete (CRUD) wines.

There are a few remaining issues in the application. They are all related to “deep linking”. The application needs to continuously keep its URL in sync with its current state. This allows you to grab the URL from the address bar at any point in time during the course of the application, and re-use or share it to go back to the exact same state.

In this last installment of the tutorial, we add support for deep linking in the Wine Cellar application.

Problem 1: Linking to a specific wine

The problem: Select a specific wine from the list. The URL looks like this: http://www.coenraets.org/backbone-cellar/part2/#wines/[id]. Now do one of these two things:

  1. Grab that URL from the address bar and try to access it from another browser window or tab
  2. Simply click your browser’s Refresh button

You get an empty screen with no data. If you look at a debug console (for example, in Chrome’s Developer Tools), you’ll get this message:

Let’s take a look at what’s going on here:

var AppRouter = Backbone.Router.extend({

    routes:{
        "":"list",
        "wines/:id":"wineDetails"
    },

    initialize:function () {
        $('#header').html(new HeaderView().render().el);
    },

    list:function () {
        this.wineList = new WineCollection();
        this.wineListView = new WineListView({model:this.wineList});
        this.wineList.fetch();
        $('#sidebar').html(this.wineListView.render().el);
    },

    wineDetails:function (id) {
        this.wine = this.wineList.get(id);
        if (app.wineView) app.wineView.close();
        this.wineView = new WineView({model:this.wine});
        $('#content').html(this.wineView.render().el);
    }

});

The problem is on line 20: we are assuming that a wine collection (this.wineList) already exists, and are trying to “get” a specific item from that list. That works well when we start the application with the default (“”) route. But this.wineList won’t exist if we start the application with the “wines/:id” route. There are different ways to address the issue:

We could modify the wineDetails function to fetch the requested item directly:

	wineDetails: function(id) {
		this.wine = new Wine({id: id});
		this.wineView = new WineView({model: this.wine});
		this.wine.fetch();
  	}

That takes care of loading the wine details in the form, but the wine list remains empty when we start the application with “wines/:id” route. We could add the following line of code to load the list if it doesn’t exist:

if (!this.wineList) this.list();

But now, the wine model that is part of the collection and the wine model fetched separately are two different objects, which means that data binding and View synchronization will not work as expected.

Another approach is to check if the collection exists in the wineDetails function. If it does, we simply “get” the requested item and render it as we did before. If it doesn’t, we store the requested id in a variable, and then invoke the existing list() function to populate the list. We then modify the list function: When we get the list from the server (on success), we check if there was a requested id. If there was, we invoke the wineDetails function to render the corresponding item.

var AppRouter = Backbone.Router.extend({

    routes:{
        "":"list",
        "wines/new":"newWine",
        "wines/:id":"wineDetails"
    },

    initialize:function () {
        $('#header').html(new HeaderView().render().el);
    },

    list:function () {
        this.wineList = new WineCollection();
        var self = this;
        this.wineList.fetch({
            success:function () {
                self.wineListView = new WineListView({model:self.wineList});
                $('#sidebar').html(self.wineListView.render().el);
                if (self.requestedId) self.wineDetails(self.requestedId);
            }
        });
    },

    wineDetails:function (id) {
        if (this.wineList) {
            this.wine = this.wineList.get(id);
            if (this.wineView) this.wineView.close();
            this.wineView = new WineView({model:this.wine});
            $('#content').html(this.wineView.render().el);
        } else {
            this.requestedId = id;
            this.list();
        }
    },

    newWine:function () {
        if (app.wineView) app.wineView.close();
        app.wineView = new WineView({model:new Wine()});
        $('#content').html(app.wineView.render().el);
    }

});

 

Problem 2: Updating the URL after a wine is created

The problem: Add a new Wine, and click Save. The id that has been assigned to the newly created wine appears in the form field. However the URL is still:
http://localhost/backbone-cellar/part2/ when it should really be: http://localhost/backbone-cellar/part2/#wines/[id].

You can easily fix that issue by using the router’s navigate function to change the URL. The second argument (false), indicates that we actually don’t want to “execute” that route: we just want to change the URL.

if (this.model.isNew()) {
	var self = this;
	app.wineList.create(this.model, {
		success: function() {
			app.navigate('wines/'+self.model.id, false);
		}
	});
} else {
	this.model.save();
}

 

Problem 3: Updating the URL while creating a new wine

The problem: Select a wine in the list. The URL looks like this: http://localhost/backbone-cellar/part2/#wines/[id]

Now, click the “Add Wine” button. You get an empty form to enter a new wine, but notice that the URL is still unchanged (http://localhost/backbone-cellar/part2/#wines/[id]). In other words, it doesn’t reflect the current state of the application.

In this case, we are going to add a new “route”:

	routes: {
		""			: "list",
		"wines/new"	: "newWine",
		"wines/:id"	: "wineDetails"
	},

The router’s newWine method is implemented as follows:

	newWine: function() {
		console.log('MyRouter newWine');
		if (app.wineView) app.wineView.close();
		app.wineView = new WineView({model: new Wine()});
		app.wineView.render();
	}

And we can change the HeaderView newWine() method as follows:

newWine: function(event) {
	app.navigate("wines/new", true);
	return false;
}

 

Putting it all together

You can run the application (Part 3) here. The create/update/delete features are disabled in this online version. Use the link at the bottom of this post to download a fully enabled version.

Here is the final version of the code:

// Models
window.Wine = Backbone.Model.extend({
    urlRoot:"../api/wines",
    defaults:{
        "id":null,
        "name":"",
        "grapes":"",
        "country":"USA",
        "region":"California",
        "year":"",
        "description":"",
        "picture":""
    }
});

window.WineCollection = Backbone.Collection.extend({
    model:Wine,
    url:"../api/wines"
});


// Views
window.WineListView = Backbone.View.extend({

    tagName:'ul',

    initialize:function () {
        this.model.bind("reset", this.render, this);
        var self = this;
        this.model.bind("add", function (wine) {
            $(self.el).append(new WineListItemView({model:wine}).render().el);
        });
    },

    render:function (eventName) {
        _.each(this.model.models, function (wine) {
            $(this.el).append(new WineListItemView({model:wine}).render().el);
        }, this);
        return this;
    }
});

window.WineListItemView = Backbone.View.extend({

    tagName:"li",

    template:_.template($('#tpl-wine-list-item').html()),

    initialize:function () {
        this.model.bind("change", this.render, this);
        this.model.bind("destroy", this.close, this);
    },

    render:function (eventName) {
        $(this.el).html(this.template(this.model.toJSON()));
        return this;
    },

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

window.WineView = Backbone.View.extend({

    template:_.template($('#tpl-wine-details').html()),

    initialize:function () {
        this.model.bind("change", this.render, this);
    },

    render:function (eventName) {
        $(this.el).html(this.template(this.model.toJSON()));
        return this;
    },

    events:{
        "change input":"change",
        "click .save":"saveWine",
        "click .delete":"deleteWine"
    },

    change:function (event) {
        var target = event.target;
        console.log('changing ' + target.id + ' from: ' + target.defaultValue + ' to: ' + target.value);
        // You could change your model on the spot, like this:
        // var change = {};
        // change[target.name] = target.value;
        // this.model.set(change);
    },

    saveWine:function () {
        this.model.set({
            name:$('#name').val(),
            grapes:$('#grapes').val(),
            country:$('#country').val(),
            region:$('#region').val(),
            year:$('#year').val(),
            description:$('#description').val()
        });
        if (this.model.isNew()) {
            var self = this;
            app.wineList.create(this.model, {
                success:function () {
                    app.navigate('wines/' + self.model.id, false);
                }
            });
        } else {
            this.model.save();
        }

        return false;
    },

    deleteWine:function () {
        this.model.destroy({
            success:function () {
                alert('Wine deleted successfully');
                window.history.back();
            }
        });
        return false;
    },

    close:function () {
        $(this.el).unbind();
        $(this.el).empty();
    }
});

window.HeaderView = Backbone.View.extend({

    template:_.template($('#tpl-header').html()),

    initialize:function () {
        this.render();
    },

    render:function (eventName) {
        $(this.el).html(this.template());
        return this;
    },

    events:{
        "click .new":"newWine"
    },

    newWine:function (event) {
        app.navigate("wines/new", true);
        return false;
    }
});


// Router
var AppRouter = Backbone.Router.extend({

    routes:{
        "":"list",
        "wines/new":"newWine",
        "wines/:id":"wineDetails"
    },

    initialize:function () {
        $('#header').html(new HeaderView().render().el);
    },

    list:function () {
        this.wineList = new WineCollection();
        var self = this;
        this.wineList.fetch({
            success:function () {
                self.wineListView = new WineListView({model:self.wineList});
                $('#sidebar').html(self.wineListView.render().el);
                if (self.requestedId) self.wineDetails(self.requestedId);
            }
        });
    },

    wineDetails:function (id) {
        if (this.wineList) {
            this.wine = this.wineList.get(id);
            if (this.wineView) this.wineView.close();
            this.wineView = new WineView({model:this.wine});
            $('#content').html(this.wineView.render().el);
        } else {
            this.requestedId = id;
            this.list();
        }
    },

    newWine:function () {
        if (app.wineView) app.wineView.close();
        app.wineView = new WineView({model:new Wine()});
        $('#content').html(app.wineView.render().el);
    }

});

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

 

Download

The source code for this application is hosted on GitHub here (see part3). And here is a quick link to the download.

You will need the RESTful services to run this application. A PHP version (using the Slim framework) is available as part of the download.

UPDATE (1/11/2012): A version of this application with a Java back-end (using JAX-RS and Jersey) is also available on GitHub here. You can find more information on the Java version of this application here.

46 Responses to Backbone.js Wine Cellar Tutorial — Part 3: Deep Linking and Application States

  1. Joe December 8, 2011 at 7:13 pm #

    Well done. Really useful for something I am working on. Thanks

    • Garry Bajaj December 3, 2012 at 2:15 pm #

      Now is the time to start earning. Be your own boss with www,pleasurebuilder.com

    • Mike Jones December 18, 2012 at 7:12 pm #

      Too bad no relational models (Backbone.HasOne and Backbone.HasMany) to make it trully a real world example.

  2. Sander December 11, 2011 at 5:19 pm #

    Thanks. This series of posts really helps to get into Javascript development coming from a PHP / ActionScript background.

  3. Damen December 19, 2011 at 12:13 am #

    Hi Christophe, Just wanted to say thanks for the effort that went into this tutorial, its is very smart, the best,
    Regards.

  4. Andre Venter December 19, 2011 at 4:49 am #

    Hi Christophe,

    Your recent articles on REST PHP and JS are excellent! Thank you!

    I also looked at Backbone and think it is very powerful, but after reading “JavaScript Web Applications 2011″ by Alex MacCaw I really see how his framework “Spine” — http://spinejs.com/ goes further to simplify and structure JS applications as RIA front-ends. Have a look at “Spine” – It would be great to see how you would structure the “Client” for your REST example in “Spine”

    Thanks again for your GREAT contributions!

  5. Greg D January 2, 2012 at 3:54 pm #

    Christophe, nice post. Can you post a link or a post on your usage of the close method in your views? The topic of cleaning up view, etc is not explained very clearly. thanks.

  6. Esente February 7, 2012 at 10:49 pm #

    Hi,

    There is a bug there. When you click on New Wine to display the form, then refresh, obviously, Backbone would not know requestedId ‘new’, and fails to load.

    So, I modified Router.newQuiz() as:

    newQuiz: function(e) {
    if (this.quizView) this.quizView.close();
    if (!this.quizListView) this.list();
    this.quizView = new QuizBank.Views.QuizView({model: new QuizBank.Models.Quiz()});
    this.quizView.render();
    }

    • Marta February 14, 2012 at 9:31 am #

      Which file are we talking about? I cant get new working :-( at all…

    • Marta February 14, 2012 at 9:42 am #

      How do I change it so it fit into the wine tutorial? Thx

  7. jpk February 16, 2012 at 12:38 pm #

    Great series of posts! It was just what I needed to get started with backbone.

    The link to the postface in update at the top of this post, though, goes to a wordpress login screen (I think you linked to the edit page for the post). You should totally fix that because I totally want to read it. :)

  8. pavpad February 18, 2012 at 1:13 am #

    Hi, Thanks for wonderful example. I tried the same example with Java REST services. I have the JSON format with JSON returned. The example fails to list items and collection.models are not populated. I have the JSON as follows.

    [{“assets”:[{“name”:”Asset1″,”description”:”Updating the asset description”,”type”:”SERVER”,”id”:2.452397991448183e+28},{“name”:”Asset2″,”description”:”Updating the asset description”,”type”:”SERVER”,”id”:2.4523982183431353e+28}]}]

    In the example for format the JSON is returned is without the JSON header “assests”, when the REST sends single record the code works fine. The issue is when I JSON list returned. The code does not throw errors. but the views fails to process the list. Any help as how to handle this.

    Thanks

  9. line focus March 1, 2012 at 5:23 am #

    How to build group swf file single project thats run ipad any one help me

  10. Rosseyn May 17, 2012 at 4:46 pm #

    Something I ran into while implementing this is with multiple collectionViews, is that after you enter a deep-linked URL to pull up a record, and the requestedId property is set with that Id, if you then navigate to a different collectionView it automatically retrieves the record with that Id, if it exists. This appears to be an easy fix by simply adding a null assignment to requestedId after it is used. e.g.

    this.wineList.fetch({
    success:function () {
    self.wineListView = new WineListView({model:self.wineList});
    $(‘#sidebar’).html(self.wineListView.render().el);
    if (self.requestedId) self.wineDetails(self.requestedId);
    if (self.requestedId) self.requestedId = null; //<<—-
    }
    });

  11. Vikram June 7, 2012 at 3:33 pm #

    Thanks Christophe,

    Your tutorials really helped me getting started on Backbone.js…I am beginning to understand it better now!! ~Vikram

  12. Simon June 28, 2012 at 6:04 pm #

    Hi,
    first of all, thank you very much for this brilliant tutorial!
    i downloaded it from github and found out, that the route for “search/{query}” is missing. so entering something in the url like “search/block” has no effect. of course also the annotated java-method is not called.
    as i am new to javascript and so on, maybe you can give me a little hint, how this issue can be solved?
    it would be greatly appreciated :-) !

  13. GarciaWebDev August 14, 2012 at 3:45 pm #

    First, thank you very much for this tutorial. It helped me to start with Backbone fast and easy. One thing that bothers me is that you access the AppRouter instance “app” from inside of the “class”. I believe a “class” (using quotes bc we know there are no strict classes in JS) shouldn’t know about it’s instance. For example, inside a view method you do:

    “`app.navigate(“wines/new”, true);“`

    I believe this should be decoupled someway. What do you think about that? Can you please give your opinion and, if you agree, share some idea on how to change that?
    Thanks a lot. Best regards.

  14. Fred Thomas August 19, 2012 at 10:21 am #

    Jumped ship so soon? Stick to Flash, sir.

  15. pplegend August 29, 2012 at 4:33 pm #

    when i add new wine, i can not add image? There is not input for it,right?

  16. Seven September 11, 2012 at 4:35 am #

    Hi CHRISTOPHE, thanks for the posts, they really help a lot:) Just wondering how this url of “../api/wines” work when using PHP RESTful api? I got a 404 error while trying to fetch data? Still didn’t get how to run the index.php initially, could you pls help to explain a bit more on this? Thanks a lot!

  17. Esau November 21, 2012 at 9:07 am #

    This is my first introduction to using php. I have extensive experience with Javascript(backbone, jquery, json, ..etc).. I have installed WAMP and am trying to run this app, but somehow it just dont work. I copied the ‘tutorial’ folder into the www folder, but then when i go to localhost/ I only get a list of files and directories on this project. Are there some configurations that must be done first?

    • Esau November 21, 2012 at 9:10 am #

      localhost/folder, i meant..I am guessing the thing is looking for index, which is located in the api folder..when I try to make an explicit call to it, that is localhost/folder/api/index.php ..I get this error: The server encountered an internal error or misconfiguration and was unable to complete your request.

  18. Zhichu November 27, 2012 at 1:29 pm #

    Thanks a lot for this excellent tutorial. Saved me tons of time when learning backbone! The deeplink part is something I didn’t think of before, and your approach is very clean and nice.

  19. Magic Mesh December 16, 2012 at 12:21 pm #

    Sweet blog! I found it while searching on Yahoo News.

    Do you have any suggestions on how to get listed in Yahoo News?
    I’ve been trying for a while but I never seem to get there! Many thanks

  20. Mike Jones December 18, 2012 at 7:10 pm #

    I appreciate your involvement with helping ppl understand backbone, but I wish you made this a little more REAL WORLD oriented. For example:

    1. Why not include a Vineyard select list. Every one has a Backbone.One relationship to Vineyards?

    2. Why not create a check box group of Wine Stores, every wine has a Backbone.Many relationship with wine stores.

    Again, your effort in providing insight into Backbone is appreciative, but, it is a shame it could not be real world oriented.

  21. Emili January 7, 2013 at 9:19 am #

    Hi!

    I really liked your tutorial, thank you very much.
    I was wondering if you could help me with Bakcbone syncing models to a DB.
    I know that in that app the models did not sync to the database. I tried with .save() or .sync() but none of the worked. Could you help me with that?

    http://stackoverflow.com/questions/14154646/syncing-backbone-model-with-database-by-a-slim-framework-api

    Thank you!!

  22. Adrian January 13, 2013 at 1:45 pm #

    There is still a huge bug in your App!

    1. Open the app: http://coenraets.org/backbone-cellar/part3/
    2. Click on any wine
    3. Press the back button

    –> Error: It will append your wine-list a second time so that every list-item exists twice!

    You should definitely correct that! But apart from that cool tutorial! =)

  23. Yome January 13, 2013 at 2:45 pm #

    Hi Nice post, the clear one found for starting using node.js and express.
    I’ll prefer using Ember.js and your tutorial give me direction
    Thanks

  24. Rahul March 21, 2013 at 10:55 pm #

    Small correction if I’m not mistaken – Under Problem 3, the newWine function’s last line needs to be changed to $(‘#content’).html(app.wineView.render().el)

    It exists in the overall code, but it might confuse someone following step by step :)

  25. Christian July 3, 2013 at 3:02 pm #

    Great tutorial. I’m quite new to JS and Backbone and really enjoyed this as an intro. I’m really impressed with how you’ve used Slim along with Backbone to populate your templates. A few things I can’t figure out though:
    I’m not clear on the syntax of your dynamic elements in index.html, your tags look like ASP so I’m wondering how this works
    I’m trying to understand how the index.html, index.php and the main.js are communicating with each other, namely regarding the save button upon adding a new wine. I’m looking for the connection which pulls in the addWine function. I see how you’re creating the new url upon adding the wine, however, I can’t seem to connection between index.html and index.php which facilitates POSTing the data to the database.
    Thanks in advance :)

  26. Janka July 16, 2013 at 6:18 am #

    Hi.
    I have download your code backbone-cellar-master.zip
    and found a bug creating new wine:

    1) click “add wine”
    2) type the Name (for example: aaa)
    3) click Save

    the app should write: You must enter a grape variety
    but it don’t write

    4) click “add wine” again
    ok, now the app write: You must enter a grape variety

    if you fix the bug,
    can you send me an email?
    I’d like to better understand the bug

  27. E Tarif July 25, 2013 at 10:01 am #

    Your tutorials really helped me getting started on Backbone.js

  28. Gayass Daher November 24, 2013 at 3:39 pm #

    it is really a good tutorial to start learning the single page applications and backbone framework. thank you very much.

  29. Kosta May 10, 2014 at 1:47 pm #

    Hi Christophe!

    Thank you very much for the tutorials! They are awesome!!!

    Guys, I followed the three-part tutorial by Christophe and added Java BackEnd with CXF Web Server.
    The ready Eclipse project may be downloaded here:
    ———————————————————————–
    https://github.com/kostaz/WineCellar

    Please see the git commits and git tags to get the details:
    ———————————————————————–
    $ git log –oneline –decorate
    258755a (HEAD, origin/master, master) Add README
    afc3d02 (tag: tutorial-part3) Fix: Problem 3: Updating the URL while creating a new wine
    8f3ea49 Fix: Problem 2: Updating the URL after a wine is created
    6de14c9 Fix: Problem 1: Linking to a specific wine
    459e488 (tag: tutorial-part2) Add changes for tutorial part 2
    46dbbb1 dos2unix: convert files from dos to unix
    24db7b7 (tag: tutorial-part1) Add picture files and use one of them
    9a5286f Web interface works ok with dummy names (read only)
    f79a2fa Fix typo bug
    d0fea1a Fix: give this to each and return in render
    e666fd9 Added Java server side – not all works
    9e6c38e Initial commit – JAX-RS tutorial part1 without server

    Thanks,
    — Kosta

  30. Www.Habitatnaturel.Fr May 25, 2014 at 1:44 am #

    Hello, Neat post. There’s an issue together with your website in web explorer, would check this?
    IE still is the market leader and a huge component to folks will omit your fantastic writing because of this problem.

  31. Manishankar May 29, 2014 at 8:16 am #

    {
    “messages”:[],
    “data”:{
    “data”:[{“name”:”manishankar”,”location”:”vizianagaram”,”id”:1},{“name”:”nagendra”,”location”:”vskp”,”id”:2},{“name”:”bharathsai”,”location”:”nizamabadh”,”id”:3},{“name”:”venkat”,”location”:”guntur”,”id”:4},{“name”:”srikanth”,”location”:”guntur”,”id”:5}]]},
    “code”:”SUCCESS”}

    i need like this can you please tell me how to get response for that message and where to write logic

  32. Bisode Webrtc Demo July 13, 2014 at 9:54 pm #

    Different allocation schemes for radio resourtce (RR) management have been defined in orxer to multiplex several MSs
    onn the same physical channel. I am not a patient person, and have no attention span, sso waiting for a browser to open is frustrating.
    Below is a simmple script that captures the HTML from
    a specified URL and stores it in a variable called “data”:.

  33. mohsen February 22, 2012 at 2:30 am #

    I pushed my code with perl backend to github https://github.com/lotux/backbone-directory

  34. kishan February 29, 2012 at 3:12 pm #

    Routing for same URL is not working. For example, in your example. I have click on New Wine will load input screen to input all details. Say, if i type in some details and without clicking save/delete i ll click again New Wine button, in this scenario routing is not happening. http://www.coenraets.org/backbone-cellar/part3/#wines/new from here i am calling New Wine again should route again with new page.Even i have the same scenarios, everytime i create New wine i should load same page but refreshed. Please help me.

Trackbacks/Pingbacks

  1. Javascript Resources[updated] « Kooljoy.com Blog - December 31, 2011

    […] Backbone.js Wine Cellar Tutorial […]

  2. Backbone.js Wine Cellar Tutorial — Part 2: CRUD - January 9, 2012

    […] grab the URL in the address bar and paste it in another browser window: it doesn’t work. In Part 3, we will add complete support for deep […]

  3. Sample Application with Angular.js - February 3, 2012

    […] I blogged a three-part Backbone.js tutorial (part 1, part 2, part 3), a number of people asked me to try Angular.js. So I decided to take it for a test drive. I […]

  4. Sample Mobile App with Backbone.js and PhoneGap - February 7, 2012

    […] recently blogged a tutorial (part 1, part 2, part 3, and postface) that takes you through the process of building a CRUD application using HTML and the […]

  5. Backbone.js Lessons Learned and Improved Sample App | Christophe Coenraets - November 17, 2012

    […] few weeks ago, I posted a three-part Backbone.js tutorial (part 1, part 2, part 3). Since then, I spent more time building a real-life application with Backbone. I ran into a number […]

Leave a Reply

css.php