-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Backbone, The Primer
A common sentiment from developers coming to Backbone is that they don't know where to start with it. Unlike full-featured frameworks with prescribed workflows, Backbone is a lightweight library with few opinions. At its worst, some would say that Backbone has TOO few opinions. At its best though, Backbone is a flexible component library designed to provide a baseline solution for common application design patterns.
The core of Backbone provides a comprehensive RESTful service package. This primer assumes that you have a basic understanding of REST resources. This primer will summarize the basic interactions of Backbone components with a RESTful API, and will demonstrate some basic techniques for rendering data into views.
- What's In The Box?
- RESTful Resources
- Using Models
- Using Collections
- Backbone CRUD
- Using Events
- Using Views
- A Simple REST Application
The first thing to familiarize with are the basic components provided by Backbone. We'll focus on the following foundation components that make up Backbone applications:
Models store application data, and sync with REST services. A model may predefine its default attributes, and will emit events when any of its managed data attributes change.
Collections manage a set of models, and sync with REST services. A collection provides basic search methods for querying its managed models, and emits events when any models are added, removed, sorted and/or changed.
A view connects a model to its visual representation in the HTML Document Object Model (or, the "DOM"). Views render their associated model's data into the DOM, and capture user input from the DOM to send back into the model.
Backbone has two JavaScript library dependencies: jQuery and UnderscoreJS. Chances are good that you're familiar with jQuery. If you don't know Underscore, review its documentation. Underscore provides common functional programming for working with data structures. When setting up Backbone, you get the full capabilities of Underscore as part of the package. jQuery is not a hard dependency and can be replaced.
Backbone does include additional useful features (such as Backbone.Router), however we'll just be focusing on these components for this primer.
For this primer, let's assume the following RESTful Muppets data service is available:
GET /muppets/ ...... Reads all Muppets.
POST /muppets/ ...... Creates a new Muppet.
GET /muppets/:id ... Reads a Muppet.
PUT /muppets/:id ... Updates a Muppet.
DEL /muppets/:id ... Destroys a Muppet.
GET /muppets/
Gets a list of all Muppets within the application. Returns an array of all Muppet models (with some additional meta data):
{
"total": 2,
"page": 1,
"perPage": 10,
"muppets": [
{
"id": 1,
"name": "Kermit",
"occupation": "being green"
},
{
"id": 2,
"name": "Gonzo",
"occupation": "plumber"
}
]
}
POST /muppets/
Creates a new Muppet model based on the posted data. Returns the newly created model:
{
"id": 3,
"name": "Animal",
"occupation": "drummer"
}
GET /muppets/:id
PUT /muppets/:id
DEL /muppets/:id
Gets, modifies, and/or deletes a specific user model. All actions return the requested/modified model:
{
"id": 1,
"name": "Kermit",
"occupation": "being green"
}
First, let's build a single model that manages data for Kermit. We know that Kermit's REST endpoint is "/muppets/1"
(ie: /muppets/:id
, and Kermit's id is 1
). Configured as a Backbone model, Kermit looks like this:
var KermitModel = Backbone.Model.extend({
url: '/muppets/1',
defaults: {
id: null,
name: null,
occupation: null
}
});
Kermit's model does two things:
- It defines a RESTful URL for his model to sync with, and...
- It defines default attributes for his model. Default attributes are useful for representing API data composition within your front-end code. Also, these defaults guarantee that your model is always fully formed, even before loading its data from the server.
However, what IS that KermitModel
object? When you extend a Backbone component, you always get back a constructor function. This is really important. So important, in fact, that we should repeat that rule:
When you extend a Backbone component, you always get back a constructor function.
So, because extending a Backbone component gives us a constructor function, that means we need to create an instance of the component before using it. Let's build an instance of the KermitModel
:
var kermit = new KermitModel();
kermit.fetch().then(function() {
kermit.get('name'); // >> "Kermit"
kermit.get('occupation'); // >> "being green"
kermit.set('occupation', 'muppet ringleader');
kermit.save();
});
After creating a new instance of our Kermit model, we call fetch
to have it load data from its REST endpoint. Calling fetch
returns a promise object, onto which we can chain success and error callbacks. In the above example, we perform some basic actions on our model after loading it. Commonly used Model methods include:
-
fetch
: fetches the model's data from its REST service using aGET
request. -
get
: gets a named attribute from the model. -
set
: sets values for named model attributes (without persisting). -
save
: sets attributes, and then persists the model data using aPUT
request. -
destroy
: decommissions the model, and persists this using aDELETE
request.
Collections handle the loading and management of a set of models. We first define a Model class to be used for the set of models, and then attach that Model class to a managing collection along with an endpoint URL:
var MuppetModel = Backbone.Model.extend({
defaults: {
id: null,
name: null,
occupation: null
}
});
var MuppetsCollection = Backbone.Collection.extend({
url: '/muppets',
model: MuppetModel
});
In the above example, MuppetsCollection
will load data from the "/muppets"
list endpoint. It will then construct the loaded data into a set of MuppetModel
instances.
To load our collection of Muppet models, we build a collection instance and then call fetch
:
var muppets = new MuppetCollection();
muppets.fetch().then(function() {
console.log(muppets.length); // >> length: 1
});
Easy, right? However, there's a problem here: our collection only created a single model. We were supposed to get back a list of two items. Let's review again what the GET /muppets/
service returned...
{
"total": 2,
"page": 1,
"perPage": 10,
"muppets": [
{
"id": 1,
"name": "Kermit",
"occupation": "being green"
},
{
"id": 2,
"name": "Gonzo",
"occupation": "plumber"
}
]
}
We can see that this list data does indeed contain two records, however our collection only fetched one model instance. Why? The reason is because Collections are derived from Arrays, while Models are derived from Objects. In this case, our root data structure is an Object (not an array), so our collection tried to parse the returned data directly into a model.
What we really want is for our collection to populate the set from the "muppets"
array property of the returned data object. To address this, we simply add a parse
method onto our collection:
var MuppetCollection = Backbone.Collection.extend({
url: '/muppets',
model: MuppetModel,
parse: function(data) {
return data.muppets;
}
});
A Collection's parse
method receives raw data loaded from our API, and may return a subset of that data to be loaded into the collection. In Backbone, both Models and Collections support the definition of a parse
method. Using parse
is very useful for reconciling differences between your API design and your front-end application architecture (which often times won't map one-to-one, and that's okay).
With the parse
method in place, the following now happens upon fetching the collection:
var muppets = new MuppetCollection();
muppets.fetch().then(function() {
console.log(muppets.length); // >> length: 2
});
muppets.get(1); // >> Returns the "Kermit" model, by id reference
muppets.get(2); // >> Returns the "Gonzo" model, by id reference
muppets.at(0); // >> Returns the "Kermit" model, by index
muppets.findWhere({name: 'Gonzo'}); // >> returns the "Gonzo" model
Success! The returned list of Muppets were parsed as expected into a collection of MuppetModel
instances, and the Collection provided some basic methods for querying them. Commonly used Collection methods include:
-
fetch
: fetches the collection's data using aGET
request. -
create
: adds a new model into the collection, and persists it viaPOST
. -
add
: adds a new model into the collection without persisting. -
remove
: removes a model from the collection without persisting. -
get
: gets a model from the collection byid
reference. -
at
: gets a model from the collection by index. -
find
: finds all records matching specific search criteria.
Create, Read, Update, and Destroy are the four major data interactions that an application must manage. Backbone Models and Collections work closely together to delegate these roles. In fact, the relationship of Models and Collections (not so coincidentally) mirrors the design of a RESTful API. To review:
var MuppetModel = Backbone.Model.extend({
defaults: {
id: null,
name: null,
occupation: null
}
});
var MuppetCollection = Backbone.Collection.extend({
url: '/muppets',
model: MuppetModel
});
Note above that the Model class does NOT define a url
endpoint to sync with. This is because models within a collection will automatically construct their url
reference as "<collection.url>/<model.id>"
. This means that after fetching the collection, our Kermit model (with an id of 1
) will automatically be configured to sync with a url of "/muppets/1"
.
Use Collection.create
to POST
new data to a list endpoint. The API should return complete data for the new database record, including its assigned id
. The new model is created immediately within the front-end collection, unless specifically told to wait for a response from the server.
muppetsList.create({name: 'Piggy', occupation: 'fashionista'});
Use Collection.fetch
or Model.fetch
to load data via GET
. You generally shouldn't need to call fetch
on models within a collection, given that their data will be fetched with the set.
kermit.fetch();
muppetsList.fetch();
Use Model.save
to PUT
updated data for a model. The model's complete set of data attributes are sent to the API.
kermit.save('occupation', 'being awesome');
Some API configurations may also support the PATCH
method to perform partial model updates (where only modified data attributes are sent to the API). This can be achieved in Backbone by calling Model.save
and passing a {patch: true}
option.
kermit.save('occupation', 'being awesome', {patch: true});
Use Model.destroy
to DELETE
a model instance. The model will remove itself from any parent collection, and issue a DELETE
request to the API.
kermit.destroy();
Backbone also provides a robust Events framework, based upon the observer pattern. A major benefit of Backbone Events is their support for context passing, or, specifying what this
refers to when an event handler is triggered:
target.on(event, handler, context)
target.off(event, handler, context)
target.trigger(event)
Other key features of the Backbone Events framework include the inversion-of-control event binders:
this.listenTo(target, event, handler)
this.stopListening()
These reversed event binders allow a listening object to track its own event bindings, thus allowing the listening object to quickly release itself from all connections. As a general rule of memory management, objects with a shorter lifespan should listen to objects with a longer lifespan, and handle cleaning up their own event references when deprecated.
You may bind event handlers onto any Model or Collection (or any other Backbone component for that matter). Remember that you can pass in a handler context to define what this
references when the handler is called:
kermit.on('change', function() {
// do stuff...
}, this);
Commonly tracked Model events include:
-
"change"
: triggered when the value of any model attribute changes. -
"change:<attribute>"
: triggered when the value of the named attribute changes. -
sync
: called when the model completes a data exchange with the API.
Commonly tracked Collection events include:
-
"add"
: triggered when a model is added to the collection. -
"remove"
: triggered when a model is removed from the collection. -
"reset"
: triggered when the collection is purged with a hard reset. -
"sync"
: triggered when the collection completes a data exchange with the API. -
<model event>
: the events of all models are proxied by their parent collection.
Review Backbone's catalog of built-in events for all available event triggers.
A View manages the visual layer placed in front of a model or collection.
All Backbone views are attached to a container element, or an HTML document element into which all nested display and behavior is allocated. A common approach is to bind major views onto predefined elements within the HTML Document Object Model (henceforth referred to as the "DOM"). For example:
<ul id="muppets-list"></ul>
<script>
var MuppetsListView = Backbone.View.extend({
el: '#muppets-list'
});
</script>
In the above example, a Backbone view class is configured to reference "#muppets-list"
as its target el
, or element. This element reference is a selector string that gets resolved into a DOM element reference.
Another common workflow is to have Backbone views create their own container elements. To do this, simply provide a tagName
and an optional className
for the created element:
var MuppetsListItemView = Backbone.View.extend({
tagName: 'li',
className: 'muppet'
});
These two container element patterns (selecting and creating) are commonly used together. For example, a collection may attach itself to a selected DOM element, and then create elements for each item in the collection.
Once a view class is defined, we'll next need to instance it:
var MuppetsListView = Backbone.View.extend({
el: '#muppets-list'
});
// Create a new view instance:
var muppetsList = new MuppetsListView();
// Append content into the view's container element:
muppetsList.$el.append('<li>Hello World</li>');
When a view is instanced, Backbone will configure an $el
property for us––this is a jQuery object wrapping the view's attached container element. This reference provides a convenient way to work with the container element using the jQuery API.
Backbone also encourages efficient DOM practices using jQuery. Rather than performing large and expensive operations across the entire HTML document, Backbone views provide a $
method that performs jQuery operations locally within the view's container element:
// Find all "li" tags locally within the view's container:
muppetsList.$('li');
Under the hood, using view.$('...')
is synonymous with calling view.$el.find('...')
. These localized queries greatly cut down on superfluous DOM operations.
A view is responsible for binding its document element to a model or a collection instance, provided to the view as a constructor option. For example:
var myModel = new MyModel();
var myView = new MyView({model: myModel});
// The provided model is attached directly onto the view:
console.log(myView.model === myModel); // << true
Attach a model to a view by providing a {model: ...}
constructor option:
var KermitModel = Backbone.Model.extend({
url: '/muppets/1',
defaults: { . . . }
});
var MuppetsListItemView = Backbone.View.extend({
tagName: 'li',
className: 'muppet',
initialize: function() {
console.log(this.model); // << KermitModel!!
}
});
// Create Model and View instances:
var kermitModel = new KermitModel();
var kermitView = new MuppetsListItemView({model: kermitModel});
Attach a collection to a view by providing a {collection: ...}
constructor option:
var MuppetsModel = Backbone.Model.extend({ . . . });
var MuppetsCollection = Backbone.Collection.extend({
model: MuppetsModel,
url: '/muppets'
});
var MuppetsListView = Backbone.View.extend({
el: '#muppets-list',
initialize: function() {
console.log(this.collection); // << MuppetsCollection!!
}
});
// Create Collection and View instances:
var muppetsList = new MuppetsCollection();
var muppetsView = new MuppetsListView({collection: muppetsList});
In the above examples, the provided data sources are attached directly to their view instances, thus allowing the views to reference those sources as this.model
or this.collection
. It will be the view's job to render its data source into its DOM element, and pass user input data from the DOM back into its data source.
Also note, the above examples leverage Backbone's initialize
method. initialize
is called once per object instance at the time the object is created, and is therefore useful for configuring new objects. Any Backbone component may define an initialize
method.
Among the primary responsibility of a view is to render data from its data source into its bound DOM element. Backbone is notoriously unopinionated about this task (for better or worse), and provides no fixtures for translating a data source into display-ready HTML. That's for us to define.
However, Backbone does prescribe a workflow for where and when rendering occurs:
- A view defines a
render
method. This method generates HTML from its data source, and installs that markup into the view's container element. - A view binds event listeners to its model. Any changes to the model should trigger the view to re-render.
A simple implementation:
<div id="kermit-view"></div>
<script>
var KermitModel = Backbone.Model.extend({
url: '/muppets/1',
defaults: {
name: '',
occupation: ''
}
});
var KermitView = Backbone.View.extend({
el: '#kermit-view',
initialize: function() {
this.listenTo(this.model, 'sync change', this.render);
this.model.fetch();
this.render();
},
render: function() {
var html = '<b>Name:</b> ' + this.model.get('name');
html += ', occupation: ' + this.model.get('occupation');
this.$el.html(html);
return this;
}
});
var kermit = new KermitModel();
var kermitView = new KermitView({model: kermit});
</script>
In the above example, a simple render cycle is formed:
- The view's
render
method translates its bound model into display-ready HTML. The rendered HTML is inserted into the view's container element. Arender
method normally returns a reference to the view for method-chaining purposes. - The view's
initialize
method binds event listeners to the model forsync
andchange
events. Either of those model events will trigger the view to re-render. The view then fetches (loads) its model, and renders its initial appearance.
At the core of this workflow is event-driven behavior. View rendering should NOT be a direct result of user interactions or application behaviors. Manually timing render
calls is prone to errors and inconsistencies. Instead, rendering should be a simple union of data and display: when the data changes, the display updates.
To simplify the process of rendering model data into display-ready markup, parsed HTML templates are commonly used. An HTML template looks generally like this:
<p><a href="/muppets/<%= id %>"><%= name %></a></p>
<p>Job: <i><%= occupation %></i></p>
Look familiar? Template rendering on the front-end is very similar to server-side HTML rendering. We just need a JavaScript template utility to parse these template strings.
There are numerous JavaScript template libraries available. In fact, Underscore has a built in template renderer, thus making it omnipresent in all Backbone apps. For this primer, we'll be using the Underscore template renderer.
To implement a front-end template, we first need to define the raw-text markup. Here's a quick and easy trick for hiding raw template text within HTML documents: include the raw text in a <script>
tag with a bogus script type. For example:
<script type="text/template" id="muppet-item-tmpl">
<p><a href="/muppets/<%= id %>"><%= name %></a></p>
<p>Job: <i><%= occupation %></i></p>
</script>
The above <script>
tag defines a bogus type="text/template"
attribute. This isn't a valid script type, so the script tag's contents are ignored by HTML parsers. However––we can still access that ignored script tag within the DOM, extract its raw text content, and parse that text into a template. To create a JavaScript template, we do this:
var tmplText = $('#muppet-item-tmpl').html();
var muppetTmpl = _.template(tmplText);
The Underscore template
method parses our raw text into a reusable template function. This template function may be called repeatedly with different data sources, and will generate a parsed HTML string for each source. For example, let's quickly load and render Kermit:
var muppetTmpl = _.template( $('#muppet-item-tmpl').html() );
var kermit = new KermitModel();
kermit.fetch().then(function() {
var html = muppetTmpl(kermit.toJSON());
});
Resulting HTML string:
<p><a href="/muppets/1">Kermit</a></p>
<p>Job: <i>being green</i></p>
In the above example, a KermitModel
is created and fetched, and then rendered to HTML after its data is loaded. To generate HTML markup, we simply invoke the template function and pass in a data source. The process is pretty straight-forward until we get to that mysterious toJSON
call. What's that?
In order to render a Backbone Model using a generic template, we must first serialize the model into primitive data. Backbone provides a toJSON
method on Models and Collections for precisely this reason; toJSON
will serialize a plain object representation of these proprietary data structures.
Let's revise the earlier rendering example to include a parsed template:
<div id="kermit-view"></div>
<script type="text/template" id="muppet-tmpl">
<p><a href="/muppets/<%= id %>"><%= name %></a></p>
<p>Job: <i><%= occupation %></i></p>
</script>
<script>
var KermitModel = Backbone.Model.extend({
url: '/muppets/1',
defaults: {
name: '',
occupation: ''
}
});
var KermitView = Backbone.View.extend({
el: '#kermit-view',
template: _.template($('#muppet-tmpl').html()),
initialize: function() {
this.listenTo(this.model, 'sync change', this.render);
this.model.fetch();
this.render();
},
render: function() {
var html = this.template(this.model.toJSON());
this.$el.html(html);
return this;
}
});
var kermit = new KermitModel();
var kermitView = new KermitView({model: kermit});
</script>
Using a parsed template greatly simplifies the render
method, especially as the size and complexity of the rendering increases. Also note that our template function is generated once and cached as a member of the view class. Generating template functions is slow, therefore it's best to retain a template function that will be used repeatedly.
Next up, a view must capture user input events--whether that's an element click, text input, or changes in keyboard focus. Backbone Views provide a convenient way of declaring user interface events using an events
object. The events
object defines a mapping of DOM event triggers to handler methods on the view.
<div id="kermit-view">
<label>Name: <input type="text" name="name" class="name"></label>
<button class="save">Save</button>
</div>
<script>
var KermitView = Backbone.View.extend({
el: '#kermit-view',
events: {
'change .name': 'onChangeName',
'click .save': 'onSave'
},
onChangeName: function(evt) {
this.model.set('name', evt.currentTarget.value);
},
onSave: function(evt) {
this.model.save();
}
});
var kermitView = new KermitView({model: new KermitModel()});
</script>
To summarize the structure of the above events
object:
- Events triggers are declared as keys on the
events
object, formatted as"[event_type] [selector]"
. - Event handlers are declared as string values on the
events
object; each handler name cites a method available in the view.
Be mindful that event handler methods should be kept fairly simple, and remain focused on how each DOM event trigger relates to a behavior of the underlying model.
Now its time to put it all together. Let's breakdown a complete RESTful application that performs all CRUD methods with our API.
The first step in setting up any small application is to establish a simple interface for managing the tasks you intend to perform. Here, we've establish a "muppets-app"
container element with a list (<ul>
) for displaying all Muppet items, and a simple input form for defining new muppets.
Down below, a template is defined for rendering individual list items. Note that our list item template includes a "remove" button for clearing the item from the list.
Finally, we'll include our application's JavaScript as an external script. We can assume that all further example code will be included in muppet-app.js
.
<div id="muppets-app">
<ul class="muppets-list"></ul>
<div class="muppet-create">
<b>Add a Muppet</b>
<label>Name: <input id="muppet-name" type="text"></label>
<label>Job: <input id="muppet-job" type="text"></label>
<button class="create">Create Muppet!</button>
</div>
</div>
<script type="text/template" id="muppet-item-tmpl">
<p><a href="/muppets/<%= id %>"><%= name %></a></p>
<p>Job: <i><%= occupation %></i></p>
<button class="remove">x</button>
</script>
<script src="muppet-app.js"></script>
Now in "muppet-app.js"
, the first structures we'll define is the Model class for individual list items, and the Collection class for managing a list of models. The Collection class is configured with the URL of our API endpoint.
// Model class for each Muppet item
var MuppetModel = Backbone.Model.extend({
defaults: {
id: null,
name: null,
occupation: null
}
});
// Collection class for the Muppets list endpoint
var MuppetCollection = Backbone.Collection.extend({
model: MuppetModel,
url: '/muppets',
parse: function(data) {
return data.muppets;
}
});
The first View class that we'll want to define is for individual list items. This class will generate its own <li>
container element, and will render itself with our list item template. That template function is being generated once, and then stored as a member of the class. All instances of this class will utilize that one parsed template function.
This view also configures an event for mapping clicks on the "remove" button to its model's destroy
method (which will remove the model from its parent collection, and then dispatch a DELETE
request from the model to the API).
// View class for displaying each muppet list item
var MuppetsListItemView = Backbone.View.extend({
tagName: 'li',
className: 'muppet',
template: _.template($('#muppet-item-tmpl').html()),
initialize: function() {
this.listenTo(this.model, 'destroy', this.remove)
},
render: function() {
var html = this.template(this.model.toJSON());
this.$el.html(html);
return this;
},
events: {
'click .remove': 'onRemove'
},
onRemove: function() {
this.model.destroy();
}
});
Now we need a view class for rendering out lists of items, and capturing input from the "create" form.
This view binds a listener to its collection that will trigger the view to render whenever the collection finishes syncing with the API. That will force our view to re-render when initial data is loaded, or when items are created or destroyed.
This view renders a list item for each model in its collection. It first finds and empties its list container ("ul.muppets-list"
), and then loops through its collection, building a new list item view for each model in the collection.
Lastly, this view configures an event that maps clicks on the "create" button to collecting form input, and creating a new collection item based on the input data.
// View class for rendering the list of all muppets
var MuppetsListView = Backbone.View.extend({
el: '#muppets-app',
initialize: function() {
this.listenTo(this.collection, 'sync', this.render);
},
render: function() {
var $list = this.$('ul.muppets-list').empty();
this.collection.each(function(model) {
var item = new MuppetsListItemView({model: model});
$list.append(item.render().$el);
}, this);
return this;
},
events: {
'click .create': 'onCreate'
},
onCreate: function() {
var $name = this.$('#muppet-name');
var $job = this.$('#muppet-job');
if ($name.val()) {
this.collection.create({
name: $name.val(),
occupation: $job.val()
});
$name.val('');
$job.val('');
}
}
});
Finally, we need to build instances of our components. We'll construct a collection instance to load data, and then construct a list view instance to display it. When our application components are all configured, all that's left to do is tell the collection to fetch
for data.
// Create a new list collection, a list view, and then fetch list data:
var muppetsList = new MuppetCollection();
var muppetsView = new MuppetsListView({collection: muppetsList});
muppetsList.fetch();
View management is by far the least regulated component of Backbone, and yet is ironically among the most disciplined roles in front-end engineering. While Backbone.View
provides some very useful low-level utility features, it provides few high-level workflow features. As a result, major Backbone extensions including Marionette and LayoutManager have become popular. Also see ContainerView for a minimalist extension of core Backbone.View features.
Thanks for reading. That's Backbone in a nutshell.