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
$ 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