Search

Backbone.js: Multiple Model

This is a CC 0 piece authored by Xavier Antoviaque, who is an excellent writer and JavaScript dev who works at farsides.com. You can check out more of his code and content at his Github: https://github.com/antoviaque
Introduction
Backbone.js
Backbone.js allows to implement the whole MVC pattern on the client, leaving the server to do what he knows best: exposing a set of well-defined REST interfaces, which the client queries when he needs to fetch or update some information. No need to split the HTML rendering between the server templates and the client-side javascript.
It's not only cleaner, it's also an excellent architecture to make responsive applications. Less information needs to be exchanged with the server - the formatting (views and controllers) being on the client, you only need to exchange the data being manipulated on the REST interfaces.
No full page reload - the server sends a static HTML file upon the first request, then the JS client handles the interaction with the user, only remodeling the portions of the DOM that changes between pages. And, better still, Backbone.js takes care of a large part of the work required to synchronize data with the server. Sweet!
Backbone-relational
However, when I recently started to learn about Backbone, I realized it doesn't help to handle relationships between models. Most non-trivial applications need this - forum threads each have a series of comments, billing invoices have several items to charge for...
If you're reading this, you've probably found out about backbone-relational after reading a few threads. But the documentation is sparse, and it's hard to see how to use it practically. How do you structure your views to represent the relations between models? How do you update or push relational models to their corresponding REST APIs?
Scope of the tutorial
This tutorial attempts to answer that - showing you, step by step, how to structure a sample application (a simplistic forum), using Backbone-relational to expose inter-dependent models from a set of REST APIs.
Note that this not meant to be a complete tutorial on Backbone.js, it is more a practical HOWTO on how to proceed with nested relationships once you've understood the basics. You may want to read aBackbone.js tutorial to learn about Backbone.js' models, views, routers and collections. Maybe also have a quick look at the TODO sample application to get a feel of how code based on Backbone looks like - it is simpler as it doesn't use relational model and doesn't attempt to synchronize with REST services.
Another great source of information is the source code of Backbone.js itself, which is short, readable and extensively annotated. Whenever in doubt, don't hesitate to dig in.
Warning: I'm new to Backbone, so don't expect a definitive resource. I merely attempted to provide the tutorial that I wish I had found when I first tried Backbone-relational. If you notice mistakes, or know better techniques than what is explained here, please leave a comment or make a push request on the project page. Thanks!

The Application
To demonstrate how Backbone-relational allows you to define relationships between models, and how to sync the data with the server in this case, we're going to look at a simple forum application. It should allow to create forum threads (topics) and list the threads that have been created, as well as post messages in each thread.


Fig 1 - Threads list

Fig 2 - Contents of a thread
Models Data Structure
Although simple enough for the scope of this tutorial, the fact that each thread should be able to contain several messages is a good use case for Backbone-relational - one of those fairly common situation where you want to create a one-to-many relationship between two models.
·         Thread object
o    Thread._id: Unique identifier for a forum thread (defined by the database, String)
o    Thread.title: Topic of a forum thread (String)
o    Thread.messages (Array): Message objects
§  Message._id: Unique identifier for a message (defined by the database, String)
§  Message.author: Name of the message author (String)
§  Message.text: Body of a message
Backbone-relation supports other types of relationships, one-to-one and many-to-many - once you have been through this tutorial, using those other types of relationships should be straightforward. Refer to the Backbone-relational documentation for more information.
Getting the sample application running
The application is available via git, if you don't want to bother copy-pasting : )
$ git clone git://github.com/antoviaque/backbone-relational-tutorial.git<span class="comment"></span>
A sample implementation of the web service providing the REST interfaces is provided in the tutorial code. The server side is out of the scope of this tutorial, so I won't say much about it. If you're interested in learning more on this part, there are a lot of resources about Node.js, Restify, MongoDB,Mongoose and Nginx you can refer to.
To get the server-side to work, you need to install Node.js (tested on 12), either from a package manager, or from source:
$ wget http://nodejs.org/dist/v12/node-vtar.gz
$ tar xvzf node-vtar.gz
$ cd node-v12
$ ./configure
$ make
$ sudo make install
Then install MongoDB - on a Debian/Ubuntu:
$ sudo apt-get install mongodb-server
$ sudo /etc/init.d/mongodb start
The node.js dependencies are already included in the node_modules/ directory, so all you should need once Node and mongodb are installed is to run:
$ node ./app.js
It will start a node instance on the port 30
Since node/restify only serves the API, you'll need to serve the client files separately (HTML, JS, CSS & images). A good choice for this is nginx. On a Debian/Ubuntu system:
$ sudo apt-get install nginx
$ sudo cp conf/nginx.conf /etc/nginx/sites-enabled/sample-forum
$ sudo gvim /etc/nginx/sites-enables/sample-forum
Edit the following line to point to the location of your local copy of the tutorial repository:
alias /home/antoviaque/prog/backbone-relational-tutorial/static/;
Be sure to point to the static/ subdirectory, where the index.html file resides.
Then start nginx:
$ sudo /etc/init.d/nginx start
nginx will bind to the port 300 It will then serve:
·         The static/ subdirectory on the root folder (loading index.html when you access http://localhost:3000/ or any URL starting with http://localhost:3000/thread/);
·         Proxy requests made to any URL starting with http://localhost:3000/api to the node server at http://localhost:3001/
Application structure
·         static/index.html: Includes all the base HTML structure we'll find on all pages, along with the templates for each of the views. You'll notice that I use Handlebars as the template engine, rather than Underscore's default one, but you should be able to achieve the same results with any of the javascript templating engines around - just pick your favorite.
·         static/js/forum.js: Defines a $.forum object, which contains all the application objects. Some developers chose to assign them to the window object, or define them as globals. Defining a specific array allows to isolate ours objects on a dedicated namespace.
·         app.js and models.js: Node.js application files - serves the REST API requests and connects to the MongoDB database.
Defining the models
We're defining two models, Thread and Message, a Thread being able to contain multiple Messages.
Message Model
$.forum.Message = Backbone.RelationalModel.extend({
urlRoot: '/api/message',
idAttribute: '_id',
});
The Message model, the most simple. Note that we're extending Backbone.RelationalModel rather than BackboneModel, to take advantage of the extra features Backbone-relational. We're just telling Backbone where to find the corresponding resource on the server (urlRoot), and what attribute the server is using to uniquely identify each message (idAttribute).
REST API - POST /api/message/
The REST interface on the server is at /api/message, and allows to POST a message of the following JSON format:
{
"author""Author Name",
"text""The message",
"thread""thread_id"
}
The thread attribute will be automatically defined by Backbone-relational, and will contain the _id attribute of the Thread model defined below. It allows the webservice to know which thread the message belongs to.
If the request is successful, the server returns the same object, to which it adds the unique _id attribute used by the database to identify it.
For the simple forum we're developing for this tutorial, we only need to define a POST interface. It allows to add new message objects to the database - this will come handy when you'll be visiting a given thread and will want to post a reply to the initial post. We will be GETting the message objects currently in the database from the /api/thread/ interface defined below. If we were implementing a full-fledged application, we would define all the methods: GET to retreive individual message objects, PUT to update an existing message, DELETE, etc.
Thread Model
$.forum.Thread = Backbone.RelationalModel.extend({
urlRoot: <span class="string">'/api/thread'</span>,
idAttribute: <span class="string">'_id'</span>,
relations: [{
type: Backbone.HasMany,
key: <span class="string">'messages'</span>,
relatedModel: <span class="string">'$.forum.Message'</span>,
reverseRelation: {
key: <span class="string">'thread'</span>,
includeInJSON: <span class="string">'_id'</span>,
},
}]
});
The relations option comes from Backbone-relational, and allows to specify how the Thread objects relate to the Message objects:
·         type: Backbone.HasMany: Each Thread can contain references to multiple Messages.
·         key: 'messages': The name of the attribute of Thread objects containing the external references. Thread.messages will contain an array of Messages.
·         relatedModel: '$.forum.Message': The model being referenced.
·         reverseRelation.key = 'thread': The reverse reference to the Thread object, from each of the Message objects contained. For example, if thread.messages = [message], then message.thread will contain a reference to the thread object.
·         reverseRelation.includeInJSON = '_id': Tells Backbone-relational to store the value of one of the Thread attributes in message.thread, rather than a reference to the object itself. Here, if thread._id = '123', then message.thread will contain '123'.
REST API - GET /api/thread/:id
The REST interface on the server is at /api/thread/, and allows to GET a thread object along with all its messages by specifying its id (/api/thread/456), using the following JSON format:
{
"_id""456",
"title""The thread title",
"messages": [
{
"_id""123",
"author""Author Name",
"text""The message"
}, {
"_id""124",
"author""Second Author Name",
"text""The reply to the previous message"
}
]
}
The messages attribute contains a list of all the messages contained in the thread. The messages objects are expanded (it's not just a reference to their ids), to allow to get the whole content of a thread in one request.
Django-relational will automatically assign each of the array elements of the message attribute to a separate Message object, and set the reverse reference (message.thread) to the value of the thread._id attribute (here, "456").
REST API - POST /api/thread/
The interface also allows to POST a new thread to add to the database, by sending the following JSON structure:
{
"title""My new thread",
"messages": []
}
If successful, the server returns the same object, to which it adds the unique '_id' attribute used by the database to identify it.
List of threads
The Thread and Message models, along with their associated APIs server-side, allow us to keep track of all the messages in a given thread, and to post new threads and messages. However, we're still missing one important piece of informations - how do we know which threads are already in the forum? We want to present a list of all the forum threads to the visitors.
This is where collections come in - they allow to query the database for a set of objects (here, all the forum threads), a bit like the way a Thread contains a reference to a set of Messages.
$.forum.ThreadCollection = Backbone.Collection.extend({
url: <span class="string">'/api/thread'</span>,
model: $.forum.Thread,
})
REST API - GET /api/thread/
The collection will GET /api/thread/ for the array of all the forum threads:
[
{
"_id""456",
"title""The thread title",
"messages": [
{
"_id""123",
"author""Author Name",
"text""The message"
}, {
"_id""124",
"author""Second Author Name",
"text""The reply to the previous message"
}
]
}, {
"_id""457",
"title""My new thread",
"messages": []
}
]
The Router
Now that we have our models, we can start looking at doing something with the data they contain. In our simple forum, we serve only two different types of pages:
·         The list of threads, which is also be the homepage, available at http://localhost:3000/ (show_thread_list)
·         The content of a single thread, available at http://localhost:3000/thread/<id_of_the_thread>/ (show_thread)
The URL router, which binds URLs (routes) to views, is:
$.forum.Router = Backbone.Router.extend({
routes: {
"""show_thread_list",
"thread/:_id/""show_thread",
},

show_thread_list: function() {
var thread_collection = new $.forum.ThreadCollection();
var thread_list_view = new $.forum.ThreadListView({el: $('#content'), model: thread_collection });
thread_collection.fetch();
},

show_thread: function(_id) {
var thread = new $.forum.Thread({_id: _id});
var thread_view = new $.forum.ThreadView({el: $('#content'), model: thread});
thread.fetch();
},

});
In both controller methods, the approach is similar (note that you don't need to understand every detail just yet - just keep in mind the way requests are processed):
1.       Instantiate the model (or the collection, since a collection is simply a way to refer to several models simulteanously) corresponding to the current URL/view.
2.       Instantiate a view, which will use the model from 1) to produce its HTML representation. It will insert the result inside the DOM element we provide (el: $('#content')).
3.       Get the model/collection to retreive the data from the server corresponding to its instantiation in 1). Doing so will trigger events in the view we instantiated in 2), and cause the view to refresh itself. Note that steps 1) and 2) don't actually modify the DOM or manipulate any data from or to the server. They just instantiate objects which start to listen to events.
For example, a request to http://localhost:3000/thread/123 will get a Thread({id: '123'}) object instantiated, which will fetch() its data at /api/thread/1 Once the server has returned the JSON data, events will be triggered on the ThreadView object (see below).
Displaying a Single Thread
A single thread can contain multiple messages. We thus need to define two different views - one to represent the Thread, for example its title (ThreadView), one to represent each of the messages it contains (MessageView).
ThreadView
Template
<script type="text/template" id="tpl_thread">
<h2 class="centered">Thread - "{{title}}"</h2>

<div class="message_list"></div>

<h2>New message</div>
<ul>
<li>Author: <input class="new_message_author" type="text" /></li>
<li>Text: <textarea class="new_message_text" /></li>
</ul>
<input type="submit" value="Post message" />
</ul>
<div class="error_message"></div>
</script>
View
$.forum.ThreadView = Backbone.View.extend({
tagName: <span class="string">'div'</span>,

className: <span class="string">'thread_view'</span>,
The HTML representation of the thread will be contained in a <div class="thread_view"></div> container.
initialize: function(){
_.bindAll(this'render''render_message''on_submit');
this.model.bind('change'this.render);
this.model.bind('reset'this.render);
this.model.bind('add:messages'this.render_message);
},
bindAll() comes from Underscore, and allows to make sure that the specified methods are always invoked with this pointing to the current object.
this.model points to the {model: ...} attribute passed when ThreadView is initialized in the router. It contains a reference to the model the view is representing, and allows us to listen to events from the model:
·         When the model changes or is reset, we redraw the view entirely (this.render)
·         When a new message is added to the thread, add it on the DOM. This happens when a new message is added to the thread.messages array, or when a new message is saved with a message.thread set to the same value as the thread._id.
render: function() {
return $(this.el).html(this.template(this.model.toJSON()));
},
Use #tpl_thread as the template. The model's attributes are passed as the current context to Handlebars, which replaces the corresponding variables in the #tpl_thread template and returns the resulting HTML. This HTML is inserted in the {el: ...} element via jQuery (cf the route, el here is $(#content')).
Note that the thread template doesn't attempt to represent the thread's messages. Backbone-relational triggers the 'add:messages' event on the model for each contained message, every time the model is fetch()'ed. When the router calls thread.fetch(), Backbone will first trigger the 'reset' event, which will run the render() method and get the #tpl_thread inserted. Then Backbone-relational will trigger the 'add:messages' event, and call render_message() for each of the messages the thread contains:
render_message: function(message) {
var message_view = new $.forum.MessageView({model: message});
this.$('div.message_list').append($(message_view.render()));
},
message will be an instance of the Message model. We use it to instantiate a MessageView view, like we did in the router for the ThreadView. The main difference is that Backbone-relational has already retreived the messages data, so we don't need to fetch() it. The messages data was contained in the response from the API request to get the thread data (/api/thread/123 - see "REST API - GET /api/thread/:id" above).
events: {
'click input[type=submit]''on_submit',
},

on_submit: function(e) {
var new_message = new $.forum.Message({author:this.$('.new_message_author').val(), text:this.$('.new_message_text').val(), thread: this.model});
new_message.save();
},
});
Here, we give the visitor the ability to post a new message in the current thread. We monitor clicks on the submit button, retreive the data the user entered using jQuery selectors (this.$('.class') is a shortcut for $('.class', this.el)), instantiate a new Message object with the user data, and get Backbone to send it to the API with the save() method. This will send the data to /api/message as a POST request. The message.thread attribute will be set to the value of thread._id, to let the server know which thread to add the message to.
Since we're adding a new message to thread.messages, Backbone-relational will trigger the "messages:add" event, and the new message will get automatically inserted in the DOM via the render_message() method - no extra effort required!
MessageView
Template
<script type="text/template" id="tpl_message">
<div class="message">
<div class="message_author">By: {{author}}</div>
<div class="message_text">{{text}}</div>
</div>
</script>
View
We used a MessageView view above to represent individual messages - we thus need to define it to let Backbone know which template to use:
$.forum.MessageView = Backbone.View.extend({
tagName: 'div',

className: 'message_view',

initialize: function(){
_.bindAll(this'render');
this.model.bind('change'this.render);
},

template: Handlebars.compile($('#tpl_message').html()),

render: function() {
return $(this.el).html(this.template(this.model.toJSON()));
},
});
Displaying a List of All Threads
This is very similar to displaying a single thread - instead of having a thread model containing a list of messages, we have a collection containining a list of all the threads. So we'll need one view for the collection, ThreadListView, and one view for each of the threads, ThreadSummaryView. We don't need to display individual messages, but we'll still use them inside of the ThreadSummaryView to show a message count for the whole thread.
ThreadListView
Template
<script type="text/template" id="tpl_thread_list">
<h2 class="centered">Threads</h2>

<ul class="thread_list"></ul>

<h2>New Thread</h2>
<ul>
<li>Title: <input type="text" class="new_thread_title" /></li>
<li>Author: <input class="new_message_author" type="text" /></li>
<li>Message: <textarea class="new_message_text" /></li>
<li><input type="submit" class="new_thread_submit" value="Create thread" /></li>
</ul>
<div class="error_message"></div>
</script>
View
$.forum.ThreadListView = Backbone.View.extend({
tagName: 'div',

className: 'thread_list_view',

initialize: function(){
_.bindAll(this'render''render_thread_summary''on_submit','on_thread_created''on_error');
this.model.bind('reset'this.render);
this.model.bind('change'this.render);
this.model.bind('add'this.render_thread_summary);
},
Although the view renders a collection and not a single model, we attach the collection object to the this.model attribute. This is because Backbone automatically sets the model attribute of a view when a {model: xxx} option is passed when the view is instantiated. If we passed a {collection: xxx} instead in the router method, we would have to access the attribute at this.options.collection.
template: Handlebars.compile($('#tpl_thread_list').html()),

render: function() {
$(this.el).html(this.template());
this.model.forEach(this.render_thread_summary);
return $(this.el).html();
},
One notable difference in the way collections and relational models behave regarding events is that collections don't trigger the 'add' event when the collection is initially fetch()'ed. We thus have to call the render_thread_summary() manually in the render() method, we can't assume render_thread_summary() will be called after render() every time the collection is rendered.
render_thread_summary: function(thread) {
var thread_summary_view = new $.forum.ThreadSummaryView({model: thread});
this.$('ul.thread_list').prepend($(thread_summary_view.render()));
},

events: {
'click input[type=submit]''on_submit',
},
This is also similar to the ThreadView. We append the HTML from the ThreadSummaryView of each of the threads to the DOM, and monitor clicks on the submit button, on the form allowing a visitor to create a new thread and post a first message in it.
on_submit: function(e) {
var thread = new $.forum.Thread({ title:this.$('.new_thread_title').val() });
thread.save({}, { success: this.on_thread_created, error: this.on_error });
},
Here, however, we'll be saving two distinct objects: first we'll create the new thread, and once the thread has been created on the server, we'll add a message in it. We wait for the thread.save() to complete to get the thread._id from the server's response:
on_thread_created: function(thread, response) {
this.model.add(thread, {at: 0});
var message = new $.forum.Message({ author:this.$('.new_message_author').val(), text:this.$('.new_message_text').val(), thread: thread.get('_id') });
message.save({}, {
success: function() {
$.forum.app.navigate('thread/'+thread.get('_id')+'/', { trigger:true });
},
error: this.on_error,
});
},
The collection doesn't automatically know that the new thread should belong to the collection, we thus need to add it manually. This will trigger the render_thread_summary() method, and add the new thread to the DOM.
on_error: function(model, response) {
var error = $.parseJSON(response.responseText);
this.$('.error_message').html(error.message);
},
});
We also define an error method, to alert the user if something goes wrong. In such a case - for example a validation error - the server should return a JSON string of the following format:
{'error': 'Error message'}show_thread().

No comments:

Post a Comment