Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Guides
Table of Contents
Introduction
Why Marionette?
Getting Started
Installing Marionette
2.1
2.2
Tutorial
2.3
2.4
2.5
2.6
Summary
2.7
Views
Lists of Data
3.1
3.2
3.3
Built-in Triggers
3.3.1
Templates
Creating an Application
5.1
5.2
Implementing Routing
Attaching Controllers
Sharing common view Behavior
Example Behaviors
6
6.1
7
7.1
10
11
FAQ
12
Cookbook
13
Appendix
14
Marionette Guides
Tutorial
Todo app
Routing
Full router example
14.1
14.1.1
14.2
14.2.1
Marionette Guides
Marionette Guides
Have you ever wanted to be a puppet master? Now, through the power of JavaScript, you
can pull the strings and make your applications dance! This book will take you through the
journey from Punch and Judy to Master of Puppets.
This book is a comprehensive guide to all aspects of the Backbone and Marionette
frameworks for JavaScript. From the basics of installing and setting up your first JavaScript
app to developing a complex full-featured application that can integrate with multiple web
services, navigate your user through your systems from any arbitrary entry point, and take
full advantage of the latest JavaScript and HTML5 features.
What is Marionette?
Marionette is a flexible web app framework that helps you build your apps quickly and
integrated into any of your web service backends easily.
Marionette is built on the popular Backbone.js framework - with a major upgrade of the View
layer of the MVC/MVVM application design pattern. In essence, Marionette gives you a set
of simple, yet powerful, components for building your web applications that handle the,
sometimes messy, business of rendering your data and handling user and server input on
your application and data.
Where Do I Start?
Firstly, check out our installation instructions and our getting started guide. If you have a
particular topic that you're interested in, our contents page should guide you in the right
direction.
For those just looking for a quick reference, the [Marionette reference][reference] should be
your next port of call.
Contribute
Do you see something that can be improved? Perhaps you know something about
Marionette that will be a great help. It might just be a typo but we welcome any and all
contributions to making this book the definitive source for working with Marionette.
Introduction
Marionette Guides
Summary
Introduction
Why Marionette?
Getting Started
Installing Marionette
Installing Marionette Inspector
Tutorial
Your first view
Adding new items
Laying out your page
Summary
Views
Lists of Data
Structuring your App
Events and Triggers
Built-in Triggers
Templates
Creating an Application
Integrating with Routing
Multiple Application Starters
Implementing Routing
Attaching Controllers
Sharing common view Behavior
Example Behaviors
Handling Data Latency
Introduction
Marionette Guides
Introduction
Marionette Guides
Why Marionette?
Building large web applications using Backbone.js can be hard. Backbone is a great tool, but
it's designed to be minimalist and useful in a wide variety of situations. As a result, you get
less guidance and support from the tool as you scale up than you do from more opinionated
frameworks like Angular and Ember. When a Backbone application grows, maintaining it
requires adding structure, either through a custom set of conventions and components, or
based on somebody elses framework. There are a lot of different Backbone frameworks out
there, but we think you'll enjoy using Marionette.js.
Decisions, Decisions
Developing with Backbone is an exercise in decision making. Backbone provides you with a
minimalist set of Models and Collections that essentially serve as light wrappers around
JavaScript objects synced over Ajax. It provides you lightweight Views that associate an
object with a DOM node and some data. It provides a router that associates URLs with
function, and it provides helpers for managing events between all of these options. That
leaves Backbone developers with many questions to answer.
How do you render Views? - By default, Backbone's render method does nothing. To
use it, you need to fill in your own rendering function. That could use a templating
system like Underscore templates or Handlebars, jQuery manipulation of the DOM, or
simple string inserts with .innerHTML() . You could use the same method for every
View, or mix it up and use different methods for different Views.
How do you manage relationships between objects? - By default Backbone
provides a way to manage sets of Models as a Collection, but it doesn't have any builtin utilities for handling nested Models or Collections. And if you want to nest your Views
you're completely on your own. You can have a View manage it's child Views, have a
single object that manages all Views, or let each View manage itself.
How do your Views communicate between each other? - Views will often need to
communicate with each other. If for instance one View needs to change the contents of
another area of the page, it could do so directly through jQuery, could get a direct
reference to a View managing that area and call a function on it, change a Model that
another View listens to, adjust a URL that a router listens to, or fire an event that
another View could respond to. Apps can use some combination of all of these
methods.
How do you avoid repeating yourself? - If you're not careful, Backbone can involve a
lot of boilerplate. Taking the naive approach, you could end up writing rendering code,
View management code and event management code over and over again in every
Why Marionette?
Marionette Guides
View. If you try to get around that using inheritance, you can end up with brittle designs
that require you to make calls down to a Views prototype when you want View specific
code. Avoiding that type of repetition and the maintenance overhead it brings is a
challenge.
How do you manage a View's life-cycle?? - What code is responsible for rendering a
View? Does it render itself on creation? Or is it the responsibility of the object creating
it? Does it get attached to the DOM immediately on render? Or is that a separate step?
When the View is removed from the DOM or deleted, how do you handle any cleanup
that is needed?
How do you structure your application? - How do you get your app started? Do you
have a central object that starts everything, or is it more distributed? If you do centralize,
do you use the router to start things, or provide some other object for managing your
code?
How do you prevent memory leaks? - If your application is a Single Page Application
or it contains long lasting interactive sections, another issue that you may need to deal
with is memory leaks. It can be easy to create "zombie Views" in Backbone if you're not
attentive to the need to unregister events attached to a View after you're done with it.
That's just a small sample of the type of decision making that you have to make for a
Backbone project. Those questions signify flexibility, but they also represent mental
overhead. If you're like me, you see these common problems and think that you can get
better results relying on a shared solution that leverages the experience of the community.
Why Marionette?
Marionette Guides
with utilities to manage child views, you can easily create deeply nested View structures
with Marionette while minimizing complexity.
A central event bus with semantic events to simplify communication between
Views - Marionette includes Backbone.Wreqr or Backbone Radio as an event bus to
allow communication between Views without explicitly coupling them.
Helpers to help you write DRY Code - In addition to centralizing the rendering and
view management code, Marionette provides hooks to allow you to abstract away
details of the DOM and events in your View code, and a mechanism to pull common ui
operations out into separate reusable objects
Helpers to avoid "Zombie Views" and memory leaks - Marionette's lifecycle includes
an explicit destroy phase that cleans up many common sources of memory leaks, and
provides a hook for you to take care of the rest
A central Application object to initialize your application - Using Marionette, you're
able to specify a set of initializers that run any code that needs to be executed before
your application starts, providing a clear structure and starting point to your app.
That's not the complete feature set, but it is the essential sales pitch. The important thing to
understand is that Marionette provides a framework for building Backbone apps that builds
on established practices from the Backbone community. If you're building a Backbone
application and want to focus on the problems that are specific to your application,
Marionette is a great way to move past common issues and focus on what's unique to you.
This explanation is adapted from a blog post by a Marionette community member. You can
find the original post here.
Why Marionette?
Marionette Guides
Install Marionette
Like all JavaScript libraries, there are a number of ways to install Marionette, depending on
your workflow. If you're new to JavaScript development, follow our simple guide for installing
Marionette.
Tutorial
Once you've installed Marionette and set up your environment, you'll want to build your first
app! Our tutorial will take you through all the major components in Backbone and Marionette
and build a fully functional application.
Getting Started
10
Marionette Guides
Installing Marionette
As with all JavaScript libraries, there are a number of ways to get started with a Marionette
application. In this section we'll cover the most common ways.
Whether you bundle jQuery really depends on whether you have external dependencies on
jQuery. For example, if you're using Bootstrap's JavaScript as a <script> tag, you probably
don't want to bundle jQuery and you'll want to depend on the global window.$ variable.
Bundled jQuery
Bundling jQuery lets you require jquery inside your application and keeps all your
dependencies under Webpack's management. To bundle jQuery, simply npm install --save
jquery . We'll move on to configuring our Webpack application.
Configuring Webpack
Configuring Webpack with jQuery bundled in your application is relatively straightforward.
Let's assume that your application lives in a directory called app and the main entry point
(or driver) is called driver.js . In addition, you want to send the resulting file to a directory
called static/js .
Create a file called webpack.config.js with the following:
Installing Marionette
11
Marionette Guides
Global jQuery
If you're referencing the window.$ variable created by including jQuery in a <script> tag,
download jQuery and place it in your static JS folder.
Configuring Webpack
Configuring Webpack for a global jQuery variable is only slightly more complicated than a
bundled jQuery. Let's assume that your application lives in a directory called app and the
main entry point (or driver) is called driver.js . In addition, you want to send the resulting
file to a directory called static/js .
Create a file called webpack.config.js with the following:
Installing Marionette
12
Marionette Guides
Note the new externals key that tells Webpack to inject the global window.$ variable
whenever Backbone or Marionette reference require('jquery') in their imports.
Installing Marionette
13
Marionette Guides
<script src="static/js/bundle.js"></script>
If you're using any other imported JavaScript (e.g. jQuery), make sure they're loaded before
our bundle.js file so anything that depends on them will be able to see the globals they
expose.
Installing Marionette
14
Marionette Guides
To setup Browserify and Marionette, we must install a few dependencies and then install
Marionette itself.
1. Install NPM following the advice from the NPM blog
2. Create a directory for your JavaScript application
3. Inside that directory, run npm init , giving your application names
4. Install Browserify: sudo npm install -g browserify
5. Install Underscore: npm install --save underscore@1.7.0
6. Install Backbone: npm install --save backbone
7. Install Marionette: npm install --save backbone.marionette
8. Install the Underscore plugin for browserify: npm install --save node-underscorify
9. Install jquery npm install --save jquery
10. Download jQuery and place it in a static folder
Now we have our basics in place, we can setup a standard setup.js file that will ensure
Marionette can be accessed:
window._ = require('underscore'); // Backbone can't see it otherwise
var Backbone = require('backbone');
Backbone.$ = window.$; // Use the jQuery from the script tag
Backbone.Marionette = require('backbone.marionette');
When building our applications, we'll commonly use a driver.js file that will start by
importing this setup.js file: require('./setup.js') .
where static_folder is the directory to the static directory that your web server provides.
Installing Marionette
15
Marionette Guides
It's important to load jQuery first, so your setup.js file sees it.
Installing Marionette
16
Marionette Guides
17
Marionette Guides
Things to know
Before we start the tutorial, we'll set up a basic directory structure to keep everything in
order. We'll create a separate folder each for collections, models, routers, templates, and
views. The folder structure is:
|-- index.html
|-- app/
|-- driver.js
|-- collections/
|-- models/
|-- routers/
|-- templates/
|-- views/
Whenever we reference a file, we will typically refer to the filename and the type of file
corresponding to a directory in the above structure.
Our index.html file will be:
Tutorial
18
Marionette Guides
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="app-hook"></div>
<script src="static/js/app.js"></script>
</body>
</html>
If you haven't already, go through the Installing Marionette section for instructions in how to
setup and install a Marionette application.
You can optionally use a CSS framework such as Bootstrap to improve how your application
looks. This tutorial will stick to a simpler to understand structure and HTML.
This tutorial will regularly ask you to restructure code from previous chapters. We will detail
every change, so if you can't see a file, assume that it remains unchanged.
Experimentation
Throughout this tutorial you should feel free to experiment with the code samples to see if
you can understand what's happening. Try different options, templates, classes, or even
write some custom logic.
Tutorial
19
Marionette Guides
Hello, world
We're going to start by building a very simple view that simply displays some text to the
world.
Running it all
Your first view
20
Marionette Guides
After compiling the file, navigate to index.html in your browser to see it render on your
screen. You've just built your first JavaScript application with Marionette!
The major point of note is that we're wrapping our object in a Backbone.Model instance.
Backbone models cleanly integrate with templates and make it easy to access their data.
We'll see in a few chapters how powerful models can be for our views.
Next we'll modify our templates/layout.html file to take advantage of this:
<ul>
<% _.each(items, function(item) { %>
<li><%- item.text %> — <%- item.assignee %></li>
<% }) %>
</ul>
We're using the built-in Underscore template engine to render our item list. You should have
a list of two items detailing simple todo items. We now have a list of items being rendered on
the page but it all feels a little clunky. What happens if we want to add or remove items in
this list?
21
Marionette Guides
The first thing we've done is add another view and attached it to our CollectionView using
the childView attribute. We also changed out this.model for this.collection in the
initialize method of our newly minted CollectionView and removed the wrapping items
key.
The CollectionView is a view that goes through this.collection and renders an instance
of its childView attribute for each item that it finds. We've removed the template attribute
as CollectionView has no template of its own.
In templates/todoitem.html we simply have:
<%- text %> — <%- assignee %>
22
Marionette Guides
That's it! Marionette knows how to build up the surrounding ul and li tags and inserts
the iteration for us. Even better, if we wanted to add/remove items in the collection,
CollectionView sees that and automatically re-renders itself for us.
In the next chapter, we're going to add new items to this collection so we can keep track of
our job list.
23
Marionette Guides
A CompositeView is a CollectionView with its own template. We then define where the
childView items are to be attached to the template using childViewContainer and pass in
24
Marionette Guides
<ul></ul>
<form>
<label for="id_text">Todo Text</label>
<input type="text" name="text" id="id_text" />
<label for="id_assignee">Assign to</label>
<input type="text" name="assignee" id="id_assignee" />
<button id="btn-add">Add Item</button>
</form>
We don't need to change the individual item template at all. Now when we refresh the page,
we'll have a form where we can enter data, click "Add Item" and... nothing happens.
Marionette doesn't know what elements to watch and what not to watch, so we have to tell it.
25
Marionette Guides
26
Marionette Guides
We've added quite a bit of code here, so lets take a few minutes to break it down:
1. We've added a "ui hash" to our view. We can attach these to any view to create cached
jQuery selectors to elements in our view's template.
2. In the triggers hash, we can reference those ui keys and, when a jQuery event occurs,
we can listen for it and fire a trigger.
3. The collectionEvents hash allows us to listen to changes occurring on the attached
this.collection attribute. The value must exist as a method on this view.
4. This trigger is then converted to an onEventName method and called. This method need
not exist and is very powerful. We'll cover it in more depth later in the book.
5. We can also reference the ui hash inside our view and treat it just like a jQuery selector
object.
6. The method referenced in collectionEvents is called when the event is triggered.
Now, whenever we click on the "Add Item" button, a new job will be added to our todo list
and the form will be cleared. We've taken an opportunity to introduce model and collection
driven events as well. For a full list of events, see the Backbone documentation.
Validating input
A job shouldn't be added to the list unless it has some text and has been assigned to
someone. You'll notice that we don't really enforce that here but we really should. There are
a couple of ways we could go about this: we could use the ui hash and validate the jQuery
content, or we could use Backbone's Model validation to do it for us.
Apart from looking nicer, the advantage of doing data validation in the Model is that we can
share that model class between views and not have to rewrite all our validation logic every
time.
We'll create a new file called models/todo.js which contains:
27
Marionette Guides
module.exports = ToDo;
If we have no errors, our validate method must return nothing ( undefined ). When we do
have errors, we can return an object describing the errors. The Marionette view will then see
this validation error and let us handle it.
Back in our driver.js file we can do:
var Backbone = require('backbone');
var Marionette = require('backbone.marionette');
var ToDoModel = require('./models/todo');
28
Marionette Guides
template: require('./templates/todolist.html'),
childView: ToDo,
childViewContainer: 'ul',
ui: {
assignee: '#id_assignee',
form: 'form',
text: '#id_text'
},
triggers: {
'submit @ui.form': 'add:todo:item'
},
collectionEvents: {
add: 'itemAdded'
},
onAddTodoItem: function() {
this.model.set({
assignee: this.ui.assignee.val(),
text: this.ui.text.val()
}, {validate: true});
var items = this.model.pick('assignee', 'text');
this.collection.add(items);
},
itemAdded: function() {
this.model.set({
assignee: '',
text: ''
});
this.ui.assignee.val('');
this.ui.text.val('');
}
});
var todo = new TodoList({
collection: new Backbone.Collection([
{assignee: 'Scott', text: 'Write a book about Marionette'},
{assignee: 'Andrew', text: 'Do some coding'}
]),
model: new ToDoModel()
});
todo.render();
29
Marionette Guides
With these changes, we can now refuse to add an item unless it passes validation. We could
also display error messages if validation fails by binding the invalid event in modelEvents .
You can see how we've set our fields to blank fields on the model and also in the ui hash. If
we start having more and more fields, you can see how this would quickly become
unmanageable. Surely we can just clear the form when the model is cleared?
By adding in the model field values to our form, we'll be able to render the data directly from
our model fields. It also means that, when the model fields are blank, these will contain no
data. We just need to wire up our view to handle this:
var Backbone = require('backbone');
var Marionette = require('backbone.marionette');
var ToDoModel = require('./models/todo');
30
Marionette Guides
form: 'form',
text: '#id_text'
},
triggers: {
'submit @ui.form': 'add:todo:item'
},
collectionEvents: {
add: 'itemAdded'
},
modelEvents: {
change: 'render'
},
onAddTodoItem: function() {
this.model.set({
assignee: this.ui.assignee.val(),
text: this.ui.text.val()
}, {validate: true});
var items = this.model.pick('assignee', 'text');
this.collection.add(items);
},
itemAdded: function() {
this.model.set({
assignee: '',
text: ''
});
}
});
var todo = new TodoList({
collection: new Backbone.Collection([
{assignee: 'Scott', text: 'Write a book about Marionette'},
{assignee: 'Andrew', text: 'Do some coding'}
]),
model: new ToDoModel()
});
todo.render();
With these simple changes, the form will now re-render itself as an empty form whenever the
user clicks the "Add Item" button. However, there's one final thing to note - the render
method redraws the entire list as well. You can probably imagine this will start to get really
slow as the list grows in size. Ideally, we just want to re-render the form itself and handle the
list separately. We'll go into this in our next chapter.
31
Marionette Guides
32
Marionette Guides
Applications
Before we do that though, we'll take a slight diversion into the Application in Marionette.
The Application is an object that lets us manage our ToDo list and its interaction with the
surrounding page.
Creating an Application
We'll be using the layout described in the introduction, so let's move the bulk of our view
code into views/layout.js and rejig it a little:
var Backbone = require('backbone');
var Marionette = require('backbone.marionette');
var ToDoModel = require('../models/todo');
33
Marionette Guides
form: 'form',
text: '#id_text'
},
triggers: {
'submit @ui.form': 'add:todo:item'
},
collectionEvents: {
add: 'itemAdded'
},
modelEvents: {
change: 'render'
},
onAddTodoItem: function() {
this.model.set({
assignee: this.ui.assignee.val(),
text: this.ui.text.val()
}, {validate: true});
var items = this.model.pick('assignee', 'text');
this.collection.add(items);
},
itemAdded: function() {
this.model.set({
assignee: '',
text: ''
});
}
});
module.exports = TodoList;
We now need a way to render this view when our application loads. Marionette gives us the
Application class for just this case. An Application sits between your pre-rendered page
34
Marionette Guides
With an Application object, we now have an obvious starting point for our application. We've
passed our initial data in from the driver.js file instead of the individual view file itself.
Now, if our index.html file gets generated by a web server e.g. Django, Rails, PHP; we can
generate a different list for each user, attach it to an object inside index.html and reference
it from our JavaScript application.
Layouts
With that little diversion out the way, we can now start breaking up our application's layout so
we have different views for different purposes. The first thing we need to do is add an extra
layout. First, we're going to break up our existing views/layout.js into views/list.js and
views/form.js which look like:
35
Marionette Guides
// views/list.js
var Marionette = require('backbone.marionette');
var ToDo = Marionette.LayoutView.extend({
tagName: 'li',
template: require('../templates/todoitem.html')
});
module.exports = TodoList;
// views/form.js
var Marionette = require('backbone.marionette');
var FormView = Marionette.LayoutView.extend({
tagName: 'form',
template: require('../templates/form.html'),
triggers: {
submit: 'add:todo:item'
},
modelEvents: {
change: 'render'
},
ui: {
assignee: '#id_assignee',
text: '#id_text'
}
});
module.exports = FormView;
You can see straight away how much simpler these views are. They only deal with their own
data management and are completely unaware of each other. We've also turned the list
back into a CollectionView because we no longer need to attach a model or a form
template. We can safely remove the todolist.html template. The todoitem.html template
is unchanged but we have a new form.html template:
36
Marionette Guides
We no longer need the wrapping form because the LayoutView will generate that for us.
Our views/layout.js file now handles the management of the two separate views:
37
Marionette Guides
The major changes here are that some of the form rendering logic is pushed into the form
view itself, while logic that links the form to the list is kept in this LayoutView . We also have
an onShow handler that renders the views into the jQuery selectors referenced by the
Laying out your page
38
Marionette Guides
regions hash. Finally, a LayoutView can see events occurring on its children by
As a template, the layout has been relegated to just an overarching frame that delegates
most of its rendering responsibilities to its subordinate views.
39
Marionette Guides
Wrapping up
After reading this tutorial, you should now be able to start building your own Marionette
apps.
Summary
40
Marionette Guides
Marionette Views
To structure the data on your page, Marionette requires you to split up the logical structure
into different views. Each view takes a template which it can render (transform into HTML)
and display. Your view will then watch this section of your application for user input,
providing the hooks you need to react to user actions.
Views
41
Marionette Guides
Let's start by building a simple view with the following template which we'll call
mytemplate.html :
Now that we have the template, we'll create a view to draw it:
var Marionette = require('backbone.marionette');
var MyView = Marionette.LayoutView.extend({
template: require('mytemplate.html')
});
view = new MyView();
view.render();
This is among the simplest views we could build - it simply renders the HTML displayed and
(with some extra code) will attach it to our page. We have a button on display, let's do
something when it gets clicked:
var Marionette = require('backbone.marionette');
var MyView = Marionette.LayoutView.extend({
template: require('mytemplate.html'),
events: {
'click .mybutton': 'alertBox'
},
alertBox: function() {
alert('Button Clicked');
}
});
view = new MyView();
view.render();
Now, whenever we click our button, we'll get an alert box. This is handled through the
events object. Put simply, the events object maps a combination of a DOM event (e.g.
click, keyup) with a jQuery selector ( .mtbutton ) to a method to call on the view ( alertBox ).
We can do something a little more complex like so:
Views
42
Marionette Guides
Now, whenever we modify the input, the contents of the div tag will change to reflect it.
You might find yourself asking why we'll go to all these lengths to do something we could do
in 2 lines of jQuery. We're just using jQuery anyway, aren't we?
Declaring your UI
Before we move on, we'll have a quick look at how to make our jQuery references a little
cleaner. Marionette views can contain an object called ui that lets us name jQuery
selectors. Let's look at a short example of how this works:
Views
43
Marionette Guides
By using the ui object we can make the code a little easier to read and a lot less brittle changing the underlying template only requires us to update the ui object with the new
selectors. Another advantage to using ui references over raw jQuery selectors is that they
get stored as references - we can look up the same references over and over and jQuery will
only search the DOM once.
Views
44
Marionette Guides
Because this view is so simple, we'll just completely redraw the template whenever the
underlying data changes.
The view where our user enters data with the template input.html :
<input class="myinput" />
<button class="mybutton">Click Me</button>
At the top-level, all we need to do is attach the same model to both views and they can then
both change and listen to it.
Views
45
Marionette Guides
Binding to Models
The above is an example of binding views to models. This is a key aspect of building
Marionette applications, especially those with dynamic data.
To bind a view to a model, simply pass it in when you create a new instance of the view:
var MyView = require('./myview');
var view = new MyView({
model: new Backbone.Model()
});
Once we have a model bound to our view, we can access it from this.model and listen to
events on the model. The official Backbone documentation contains the full list of events,
and what they apply to.
46
Marionette Guides
Now, whenever that event is fired by the model, we can listen to the save event and act on it
from whichever views are bound to the model. While this works, this ad-hoc method for
triggering model events only works for one-off sections of your application. If you want to
standardize this behavior across your app's models then we would recommend you provide
a custom method or extend the model's save() method (and/or any others).
Views
47
Marionette Guides
Lists of Data
A key component of views is being able to handle lists (or collections) of data. The
CollectionView and CompositeView are specifically designed to deal with these lists by
Lists of Data
48
Marionette Guides
<ul>
<li><a href="/items/1">Some Text</a></li>
<li><a href="/items/2">Some other text</a></li>
</ul>
The CollectionView will recognize the item has been added and inject the item into the
HTML template at the right location.
Events on Collections
Just like we can bind modelEvents we can also bind collectionEvents to our views. The full
list of events can be found in the Backbone documentation. This includes events for add
and remove (which is what Marionette listens to internally). Let's see an example:
var List = Marionette.CollectionView.extend({
tagName: 'ul',
childView: Item,
collectionEvents: {
add: 'itemAdded'
},
itemAdded: function(collection) {
alert('New item added');
}
});
Now, whenever an item gets added, no matter how, an alert box will be displayed for each
one.
Rendering Tables
You'll notice the CollectionView doesn't define its own template. This makes it unsuitable
for more complex listed layouts such as tables. To solve this problem, we have the
CompositeView - a view that lets us assign a template to be rendered.
Lists of Data
49
Marionette Guides
The CompositeView takes two extra required attributes: the familiar template and
childViewContainer - a jQuery selector to the element in our template to attach the
childView elements. Using the CompositeView is identical to the CollectionView :
Lists of Data
50
Marionette Guides
Adding and removing items works exactly as you'd expect - the new views are injected at
the correct locations in the template.
Binding Models
The CompositeView has another advantage over CollectionView - it can take and additional
model argument and render based on the contents of the model. Let's say we wanted to
know how many people were in the final list above, we'll modify our table.html :
<thead>
<tr>
<th>Name</th>
<th>Nationality</th>
<th>Gender</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<th>Total</th>
<td colspan="2"><%- total %></td>
</tr>
</tfoot>
This total needs to come from a model which we'll pass when creating the table:
var Table = require('./table');
var collection = new Backbone.Collection([
{name: 'John Smith', gender: 'male', nationality: 'UK', url: '/items/1'},
{name: 'Jane Doe', gender: 'female', nationality: 'USA', url: '/items/4'}
]);
var model = new Backbone.Model({
total: 30
});
var view = new Table({
collection: collection,
model: model
});
view.render();
And that's it, our table now has access to the total field in the model!
Events
Lists of Data
51
Marionette Guides
Our CompositeView can now listen to both collectionEvents and modelEvents at the same
time. One good use for this is a report table with a summary that is calculated and fetched
separately:
var Table = Marionette.CompositeView.extend({
tagName: 'table',
template: require('table.html'),
childView: Row,
childViewContainer: 'tbody',
modelEvents: {
sync: 'render'
},
collectionEvents: {
update: 'checkStatus'
},
checkStatus: function(collection) {
collection.each(function(model) {
// Do something
});
}
});
With this example, when our model is fetched from the server, we'll re-render the table.
When our collection is modified, we run another handler that, in this case, does some form
of checking/modification for the collection.
Lists of Data
52
Marionette Guides
We've created two empty elements with hooks that we want to attach our regions to. Next,
we'll define our view:
var Summary = require('./summary');
var Table = require('./table');
The LayoutView takes a regions object that maps our keys to jQuery selector strings in the
template. The next step is to tell Marionette what views to display when it is itself displayed:
53
Marionette Guides
We've added our onShow handler to our layout. When we show the MyLayout view in a
parent region, the show trigger is fired, upon which it will then create and show our
summary and table. Each of these views will be rendered and their show triggers will be
fired. If these contain LayoutView s, we could keep listening to this trigger and show more
views down the tree.
Layouts as mediators
The LayoutView is a great medium for transferring messages between one view and
another, particularly if they're on the same level. For example, we could contain a form
which, whenever the user saves it, adds a new record to a collection elsewhere in the
application. To make this easier, we'll use triggers on the form. Triggers are covered more indepth in another chapter so we'll stick to the parts that are relevant for our layouts.
We'll gloss over the templates and stick to just the views, keeping everything in a single file
for simplicity:
54
Marionette Guides
Now that we have our skeleton, we need to figure out how to get the 'save:model' trigger
fired on the FormView to be recognized from its parent Layout . Luckily, Marionette gives us
just a tool for that - the childview: trigger prefix. Every time a trigger is fired on a view, any
parent views can see it as well. Taking just our Layout , we can do the following:
55
Marionette Guides
The layout view knows about all its children, as it should, but the individual views don't know
about each other. This means each view can continue operating independently and manage
its own state - when that state needs to be shared, we need only start looking up the view
hierarchy to see when, and how, that state gets shared between views.
56
Marionette Guides
Using Events
Marionette provides an events object that allows us to listen to activity in our view's
template and respond to it with a specific method. For example, we can listen to data entry
or a button press and determine the method to call. For example:
var MyView = Marionette.LayoutView.extend({
template: require('./events.html'),
ui: {
title: '.title',
save: '.save'
},
events: {
'keyup @ui.title': 'setTitle',
'blur @ui.title': 'setTitle',
'click @ui.save': 'saveForm'
},
setTitle: function(domEvent) {
this.model.set({
title: this.ui.title.val()
});
},
saveForm: function(domEvent) {
this.model.save();
}
});
57
Marionette Guides
This is a simple example of setting a model field and saving the data to the server. It should
be clear what methods are being called when we perform actions on the page. The events
object must be bound at initialization and will cause an error if any of the referenced
methods don't exist on the view.
Triggers
What happens if we want to provide more generic behavior? We could want to build a base
view for our application that gets extended by other views, providing events that can be
listened to when we want to extend the behavior of the view. Forcing us to define all the
methods when creating the base class would be overkill, especially in a language like
JavaScript that aims for a certain amount of brevity and clarity.
Luckily Marionette gives us the triggers framework. To use a trigger, we simply define the
name of the trigger to be fired and either provide listeners or, for simplicity, a special method
name that will be called. We'll rewrite our above example to demonstrate:
var MyView = Marionette.LayoutView.extend({
template: require('./events.html'),
ui: {
title: '.title',
save: '.save'
},
triggers: {
'keyup @ui.title': 'set:title',
'blur @ui.title': 'set:title',
'click @ui.save': 'save:form'
},
onSetTitle: function(domEvent) {
this.model.set({
title: this.ui.title.val()
});
},
onSaveForm: function(domEvent) {
this.model.save();
}
});
As you can see, the changes were minor - we changes the events object to triggers and
specified some trigger names. Triggers are typically named using the : as word
separators. This special syntax is recognized by Marionette to work out which methods to
58
Marionette Guides
call. As you can see, set:title will call onSetTitle when triggered and save:form will
call onSaveForm when triggered.
This style is used throughout Marionette to allow developers to bind hooks before and after
common actions occur. Some of the most common examples are the render and show
hooks: before:render , render , before:show , show .
Built-in Triggers
We can listen to these standard triggers just like any other:
59
Marionette Guides
The next section will cover the built in triggers in more detail.
60
Marionette Guides
Built-in Triggers
Marionette views include a set of built-in triggers that are fired throughout the View creations
and destruction lifecycle. These triggers are designed to give you more fine-grained control
over your view, for example being able to modify your template before it gets attached to
your page, or even before the template gets rendered.
It's also possible to write your own custom triggers and handlers to provide hooks for
specific actions that aren't provided by Marionette.
Most triggers have before:action and action hooks that fire before and after action has
occurred. A common example could be before:render and render which fire before and
after the template is rendered.
Short-hand
All triggers that get fired can also be handled by defining methods in a specific style on our
views: the onTriggerFired format e.g. onBeforeRender . To know what name our handler
method requires, we simply take the trigger name ( before:render in this case), prefix on
and uppercase the first letter and every first letter following a : in the name. So render
becomes onRender , before:render becomes onBeforeRender , attach becomes
onAttach , etc.
Built-in Triggers
61
Marionette Guides
For simplicity, this reference will use the trigger:fired format, which you should now be
able to convert to the method name using the formula described above.
trigger (or all), and what each trigger means to you. Where appropriate, we'll provide an
example of common use of the trigger.
If you just want to know what triggers a view can fire, skip to the bottom of this reference to
find the list of views and the triggers they can each fire.
Example
On Views
CollectionView
CompositeView
Built-in Triggers
62
Marionette Guides
This trigger fires once our view gets attached to the actual DOM i.e. once the view is
completely rendered and viewable in the browser window. This is usually used to perform
actions that require the view to be completely rendered and visible - for instance displaying a
modal or datepicker widget. We also use this as a safe point where we know that the view is
finished and active for the user.
Example
This example uses Bootstrap's Modal JavaScript which requires the HTML to be attached to
the DOM to work.
var ModalLayout = Marionette.Layout.extend({
ui: {
wrapper: '.modal-wrapper'
},
/** Use Bootstrap's modal JavaScript. This assumes that the JS files are
* loaded and will work.
*/
onAttach: function() {
this.ui.wrapper.modal('show');
}
});
On Views
View
ItemView
LayoutView
CollectionView
CompositeView
Example
In this example, we'll use the before:destroy trigger to close a modal before it gets
removed from the DOM. We're using something similar to Bootstrap's modal JavaScript.
Built-in Triggers
63
Marionette Guides
As an aside, this wouldn't work in Bootstrap as the modal method returns immediately,
causing the view to be destroyed before the modal can be hidden.
On Views
View
ItemView
LayoutView
CollectionView
CompositeView
dom:refresh
Only fired when a view is re-rendered. This does not get fired on the first render, only on
subsequent rendering.
Example
This example demonstrates when the dom:refresh trigger is fired.
Built-in Triggers
64
Marionette Guides
On Views
View
ItemView
LayoutView
CollectionView
CompositeView
Example
A common example is to check for data and add/remove classes on the view's element
every time it gets rendered. This example checks to see if a table row was selected for a
form element and attaches the appropriate class.
Built-in Triggers
65
Marionette Guides
On Views
ItemView
LayoutView
CollectionView
CompositeView
render:collection and
before:render:collection
Fired after the collection has been rendered and attached to the view. This will only fire if the
view's collection is not empty. In other words, if the view attached to emptyView is displayed,
this trigger won't be fired.
On Views
CollectionView
CompositeView
Built-in Triggers
66
Marionette Guides
Example
On Views
CompositeView
Example
On Views
CollectionView
CompositeView
Example
On Views
CollectionView
CompositeView
Built-in Triggers
67
Marionette Guides
Example
Our example is a Marionette pattern that is commonly used to build up our view hierarchy by
chaining show handlers.
var MyLayout = Marionette.LayoutView.extend({
regions: {
todoList: '.todo-hook'
},
onShow: function() {
console.log('regionManager has show this view');
this.showChildView('todoList', new CompositeView({
collection: this.collection
}); // This will trigger CompositeView's 'before:show' and 'show' handlers
}
})
// Assuming regionManager has been defined with a region 'layout'
regionManager.get('layout').show(new MyLayout({collection: todoList}));
On views
View
ItemView
LayoutView
CollectionView
CompositeView
Built-in Triggers
68
Marionette Guides
Built-in Triggers
69
Marionette Guides
This diagram displays the full lifecycle. Our before:add:child and add:child only get
called on CollectionView and CompositeView before the view lifecycle of each child is
executed.
Our before:show and show triggers get fired whenever the view is shown as part of a usual
lifecycle - from region.show(view) , view.showChildView(view) , or when a new child is added
to a CollectionView with view.collection.add(model) .
View
ItemView
LayoutView
add:child
CollectionView
CompositeView
attach
destroy
dom:refresh
render
render:collection
render:template
reorder
remove:child
show
Built-in Triggers
70
Marionette Guides
A simple template
With a LayoutView we can render a template quite easily:
var MyLayout = Marionette.LayoutView.extend({
template: require('mylayouttemplate.html')
});
When we show or render our view, we can see the template that we've just created. This
isn't very interesting, however, so let's take it a step further using some model data:
var mymodel = new Backbone.Model({
name: 'Scott'
});
var layout = new MyLayout({model: mymodel});
layout.render();
Our page is now a little more interesting - we can change it based on the model data we
pass into our view. With just this knowledge we can, and will, build plenty of complex web
applications.
Templates
71
Marionette Guides
Template syntax
This syntax comes from Underscore's template engine; an extremely simple template engine
that does most of what we need. We'll quickly go through its syntax here.
Output data
There are two basic ways to output data in an underscore template: escaped and
unescaped.
Escaped data
The default syntax we'll use is the familiar <%- %> syntax from above. This will cause
Underscore to escape HTML strings to make them safe. This should be your default goto,
especially for user-entered data:
<h1><%- heading %></h1>
<p><%- user_data %></p>
The escaping has protected us from potential cross-site scripting (XSS) attacks.
Unescaped data
On occasion, we need to output data exactly as it is entered into our model. For example,
we may have pre-escaped our output, or we explicitly want to output HTML that the browser
will render (for example, rendered Markdown).
In this case, we can tell Underscore to output our data as-is with <%= %> .
Templates
72
Marionette Guides
With the same model as above, the raw output will be:
<h1>Some text</h1>
<script>alert("test")</script>
The following template can't render and will raise an error instead:
<h1><%- heading %></h1>
<%- user_data %>
Templates
73
Marionette Guides
We get access to the _ namespace in our templates too, making it easier to iterate:
<ul>
<% _.each(data, function(item) { %>
<li><%- item %></li>
<% }) %>
</ul>
Templates
74
Marionette Guides
<ul>
<li>string 1</li>
<li>string 2</li>
</ul>
Template Helpers
As we can see, Underscore's template language is pretty basic and gives us very little to
work with. For example, setting variables or custom logic isn't well supported, and
attempting to access an undefined variable causes the entire template to not render. In
addition, if we want to start introducing some complex logic, the template itself will quickly
become unwieldy. We can mitigate some of this complexity using views and layouts but,
when we simply want to validate data or format it, we turn to the templateHelpers attribute
on our view.
The templateHelpers attribute lets us assign an object - or function returning and object whose keys will be available in the template. Let's build a simple template helper that
outputs some information about a basket.
Our desired template is:
<p>
Your basket total is <%- toCurrency(total) %> and contains
<%- count(items) %> items.
</p>
Templates
75
Marionette Guides
<p>
Your basket total is 35.00 and contains 2 items.
</p>
Now, our template can see the keys count and toCurrency and can call these functions
directly. Another key advantage of this method is reusability - it's simple to just import a
function call and attach it to our templateHelpers as such:
var currencyFormatter = require('./helpers/currency');
var arrayHelpers = require('./helpers/array');
var BasketView = Marionette.LayoutView.extend({
template: require('./basket.html'),
templateHelpers: {
count: arrayHelpers.count,
toCurrency: currencyFormatter
}
});
Returning a function
The templateHelpers attribute can also take a function and call it for you. This is especially
useful in the case where we have potentially undefined variables on our model:
Templates
76
Marionette Guides
Templates
77
Marionette Guides
Creating an Application
The Marionette.Application base class is used as the root of your application. The
Application is commonly used to:
1. Transfer any data from the surrounding page into your application.
2. Initialize your regions and views.
3. Start your router.
This section explores the different use cases and how to combine them effectively.
This simple pattern will work for many types of application. It also works well for index pages
with data injected from a server-side template.
Creating an Application
78
Marionette Guides
<!DOCTYPE html>
<html>
<head></head>
<body>
<div id="body-hook"></div>
<script>
/** This section was generated by a server template engine e.g. Django
or Ruby on Rails
*/
window.initialData = [
{
id: 1,
url: '/item/1',
strapline: 'Make your applications dance',
title: 'Marionette'
},
{
id: 3,
url: '/item/3',
strapline: '',
title: 'Backbone.js'
}
];
/** End server generated section */
</script>
<script src="/static/js/app.js"></script>
</body>
</html>
Now we'll modify our driver.js file to look for the initial data from the server:
var Marionette = require('backbone.marionette');
var Layout = require('./views/layout');
Creating an Application
79
Marionette Guides
our test runner. For this reason, it's preferable to keep our driver.js file as simple as
possible - we want to minimize the amount of setup code we need to duplicate.
Creating an Application
80
Marionette Guides
This is an ideal way to pass page data into router, as well as the rest of our application.
Now, when our Router responds to the URL from HTML Push State, we'll also send in any
data specific to that page to be immediately rendered.
Examples
81
Marionette Guides
We'll just take a few minutes to look at potential example pages that a server could generate
for given URLs. Assuming the rest of the page remains the same, we'll just be looking at the
<script> tag that generates the initialData key.
/
window.initialData = {
guides: [
{
name: 'Marionette',
url: '/guides/marionette'
},
{
name: 'Ember',
url: '/guides/ember'
}
],
authors: [
{
name: 'Scott Walton',
email: 'scott@example.com'
},
{
name: 'Joanne Daudier',
email: 'joanne@example.com'
}
]
}
The Router will trigger the handler bound to the '' route and inject two lists of guides and
authors into the application to be rendered (or not).
/guides/marionette
82
Marionette Guides
window.initialData = [
{
name: 'The Marionette Guide',
authors: [
{
name: 'Scott Walton',
email: 'scott@example.com'
},
{
name: 'Joanne Daudier',
email: 'joanne@example.com'
}
],
published: '2015'
},
{
name: 'Marionette: A Gentle Introduction',
authors: [
{
name: 'David Sulc',
email: 'david@example.com'
}
]
}
];
This data will trigger the handler bound to the 'guides/:guide' route and inject a list of
Marionette guides and their authors to be rendered. This is obviously useful when we have a
short list that's easy for the server to include with the page being loaded. When the
application has loaded, we'll only need to request the data as and when we need it. On the
initial application load, no matter what page we start from, we'll always get some data that
we can render immediately.
This pattern lets us make the best use of HTML5's Push State to get the best of both worlds.
Even better, this pattern only requires us to change the template that our server generates our JavaScript files are safely cached and we don't need to keep duplicating the same basic
functionality!
83
Marionette Guides
Dynamic Initializer
The key to achieving this is by attaching initializers on the application. To do this, we can use
the addInitializer method like so:
var Marionette = require('backbone.marionette');
var initializerFunction = require(window.initializer);
var application = new Marionette.Application();
application.addInitializer(initializerFunction);
app.start({initialData: window.initialData});
The value of initializer is then set by the template created by the server.
Browserify Integration
If you use Browserify, you'll notice that this doesn't work at all. The reason for this is
because Browserify tracks the strings in your require() calls to determine the modules to
import and doesn't support dynamic imports.
To get round this, we can use a includes.js file to map all the locations to keys:
84
Marionette Guides
module.exports = {
root: require('./initializers/root'),
guides: require('./initializers/guides')
};
Now Browserify can follow the imports from includes.js and will automatically import all
the modules.
Purpose
Why go to all this trouble? There are plenty of valid reasons an application may use multiple
starters. It can reduce the amount of JS code transferred to the client in one go - the server
will implement some of the logic itself. It also allows us to use Marionette for certain screens;
a major benefit in particularly large legacy applications that would take a long time to
completely port over to a JS app which may not be worth the cost.
85
Marionette Guides
Implementing Routing
In this chapter, we'll take a brief look at the history of JavaScript web apps so we can
understand the need for routing. We'll then examine how Marionette aims to solve the issues
we uncover.
History
One of the main benefits to building a JavaScript application is showing/hiding views based
on user input without lengthy calls to the server. For example, when choosing an item from a
list, we can just render that item based on information stored in a Collection without
making any lengthy calls to the server to fetch data we have in our browser already. Over
time this method has been successfully used in many applications, reducing server load and
increasing the responsiveness of our web applications.
However, a major downside of this method has historically been the inability to get back to
that page. You'll undoubtedly remember the frustration of accidentally clicking back,
refreshing a page, or closing your browser only to get sent to the home page and have to
find your way back to the screen you were working on, or that interesting article you were
reading.
Solutions
Over the years different methods were tried, including having the server try to remember
your position in an application, mostly with poor results, bugs, complex code, inability to
bookmark or share a post, and many other issues you have likely encountered. What we
really wanted was to just use the trusted browser URL in our applications.
Twitter popularized the use of the fragment - the portion of the URL after # - to store
information about the current location. This part of the URL isn't read by the server but is
understood by web browsers to jump to certain sections of a page. It is also the part of the
window.location object that we can change without causing the browser to change the
whole page from underneath us. When we display some information we may want to retrieve
later, we could simply do: window.location.hashCode = 'newLocation'; and the fragment
would be updated. When we want to retrieve the data on page load, we can read the value
of window.location.hashCode and map it to the code we want to execute to retrieve and
render the information referenced by the fragment.
Implementing Routing
86
Marionette Guides
Routing in Marionette
The Marionette AppRouter is an attempt to abstract over this behavior and provide a URL
routing engine that mirrors those found in the more mature server-side web frameworks
such as those provided by Django, Flask, and Ruby on Rails.
There are two aspects to routing in Marionette:
1. Set the fragment for state to restore later.
2. Render the view matching the fragment on page load.
need to remember is that this is all navigate does! We must not call navigate if we want
to run code from our router, we'll see how to do this in the Router and Controller section
below.
The methods referenced in appRoutes must exist on an attached controller. We can attach
this controller in a number of ways which we'll explore shortly.
Implementing Routing
87
Marionette Guides
The other two routes have variables in their URLs ( entry and comment ) which we pass
into the mapped methods. Some examples:
http://example.com#blog/3 will call blogEntry('3')
http://example.com#blog/5/comments/32 will call blogComment('5', '32')
http://example.com#blog/my-title/comments/2015-02-11 will call blogComment('my-title',
'2015-02-11')
Attaching a Controller
Once you've created your AppRouter you'll need to attach a controller to it before any
routes will be activated. To attach a controller, simply extend Marionette.Object with the
method names matching the values of appRoutes and attach an instance to our
AppRouter.controller attribute:
Implementing Routing
88
Marionette Guides
A simple app
Now we have all the building blocks in place, we can look at a simple app that uses a single
region and manages which view is being rendered using routes. As is common in many
applications, the Controller is going to be the core of our app, initializing our layouts and
handling rendering.
We'll just show the Router and View here. For the full app, visit the appendix.
Our Router
Our router and controller will be stored in router.js and looks like:
var LayoutView = require('./blog');
Here we setup our top-level view in the controller, which simply renders by triggering events
on our view. The view, described below, then renders the correct view based on the layout.
Implementing Routing
89
Marionette Guides
Our View
In our blog.js file, we'll just outline the top-level layout. If you want to see the full
application, check out the appendix:
var LayoutView = Marionette.LayoutView.extend({
template: require('./templates/layout'),
regions: {
main: '.app-hook'
},
onShowBlogList: function() {
this.showChildView('main', new BlogListView());
},
onShowBlogEntry: function(entry) {
var model = this.collection.get(entry);
this.showChildView('main', new BlogEntryView({model: model}));
},
/** Called when `BlogEntryView` triggers `show:blog:list` */
onChildviewShowBlogList: function() {
this.triggerMethod('show:blog:list');
Backbone.history.navigate('blog/');
},
/** Called when `BlogListView` triggers `show:blog:entry` */
onChildviewShowBlogEntry: function(entry) {
this.triggerMethod('show:blog:entry', entry);
Backbone.history.navigate('blog/' + entry);
}
});
module.exports = LayoutView;
The key is that the layout can listen to its children, renders its main region and calls
Backbone.history.navigate to let us know that a URL change occurred. Our router then, at
page load, attempts to match a fragment and triggers the main layout to render the correct
view.
Implementing Routing
90
Marionette Guides
Backbone.history.start();
Implementing Routing
91
Marionette Guides
Attaching controllers
There are a number of ways to attach a controller to your AppRouter , depending on how
you use it in your application. This chapter covers the different strategies and why you might
want to use each one.
Simple objects
The easiest way to attach a controller is to simple attach an object to the controller key
when extending AppRouter as such:
var Router = Marionette.AppRouter.extend({
appRoutes: {
'blog/': 'blogList',
'blog/:entry': 'blogEntry',
'blog/:entry/comments/:comment': 'blogComment'
},
controller: {
blogList: function() {
// ...
},
blogEntry: function(entry) {
// ...
},
blogComment: function(entry, comment) {
// ...
}
}
});
module.exports = Router;
This method works for simpler controllers that don't have much internal state. We just treat
this like any other JavaScript object.
Attaching Controllers
92
Marionette Guides
We can now have the Object listen to events that occur in the application - for example, we
might handle major layout changes like route-changing logic in our controller by listening to
events on views. This will reduce the amount of code needed to change route inside the
application versus setting up the views on page load.
During initialization
We can also attach a controller during initialize to pass options through the router:
Attaching Controllers
93
Marionette Guides
This pattern lets us initialize a router inside our application and pass any options directly
through to the controller without having to expose it to the rest of the application.
No Controller
If you're familiar with Backbone, you'll see that the AppRouter is very similar to Backbone's
built-in Router class. As you can probably guess, the AppRouter is simply an extension of
Backbone.Router so, intuitively, supports the standard Backbone Router behavior.
What this means for our simpler applications is that we can merge the router and controller
into a single AppRouter class by setting URLs on the routes key instead of appRoutes .
Any methods declared on routes must exist on our router, just as in standard Backbone:
Attaching Controllers
94
Marionette Guides
With the AppRouter , we can mix the two styles in a single routing class.
Attaching Controllers
95
Marionette Guides
This will work until you need to override anything that the base view provides i.e. the ui or
triggers hashes. If the form is also a modal, we then have to choose the View to extend,
What is a Behavior?
At its most basic level, a Behavior is an object attached to a view that is able to listen, and
respond, to events/triggers on the view. It has access to the view object, allowing it to
perform modifications if it needs to. Let's take a look at a simple form Behavior:
96
Marionette Guides
Now, with a slightly different configuration, we've achieved the same effect in a way that can
be shared across all views that contain a .save-button element. The behaviors object is
used to map Behaviors to Views with extra options. The object key is completely arbitrary,
here we've decided to call it form to reflect its use. We then tell the view which class to use
with the behaviorClass key. Next we'll examine ways of adding extra information to our
behavior.
97
Marionette Guides
This behavior can now, when a new item is added to our collection, highlight the table row of
the newly added model, like so:
98
Marionette Guides
Now we have a form that can be a modal! Another common case is to create a basic View
that does what you need, then add a modal version of it like so:
var NoteView = Marionette.LayoutView.extend({
template: require('./template/note.html')
});
var ModalNoteView = NoteView.extend({
// May be needed depending on the modal implementation
template: require('./template/modalnote.html'),
behaviors: {
modal: {
behaviorClass: ModalBehavior
}
}
});
99
Marionette Guides
Now we're able to both render a Note and display it in a Modal with minimal added effort.
100
Marionette Guides
101
Marionette Guides
102
Marionette Guides
Data Manipulation
Forms
The form behavior is one of the most common behaviors for interactive applications. The
form here will be a simple behavior that you can build on and customize for your specific
case:
var FormBehavior = Marionette.Behavior.extend({
defaults: {
formSelector: 'form',
submitSelector: '.submit-form'
},
ui: {
return {
form: this.getOption('formSelector'),
submit: this.getOption('submitSelector')
};
},
events: {
'submit @ui.form': 'saveForm',
'click @ui.submit': 'saveForm'
},
saveForm: function() {
this.view.triggerMethod('before:form:save', this.view.model);
this.view.model.save();
this.view.triggerMethod('form:save', this.view.model);
}
})
We've provided before:form:save and form:save handlers that the view can listen for to
provide extra hooks. Some possible ways to extend this behavior could be to add field
selectors that could be used to automatically bind the field data to the model before saving it.
Example Behaviors
103
Marionette Guides
Bootstrap
A number of Bootstrap's JavaScript widgets involve repetitive rendering tasks that can be
handled using behaviors.
Modal
The Bootstrap CSS framework provides a modal dialog that we can use for confirmation
boxes, extra information, or even pop-up forms. This behavior wraps a view's region in a
modal that can be rendered anywhere, as well as providing the handlers to tear it back down
again.
var ModalBehavior = Marionette.Behavior.extend({
defaults: {
modalClasses: '',
modalOptions: null
},
ui: {
close: '.close-modal'
},
events: {
'hidden.bs.modal': 'triggerFinish',
},
triggers: {
'click @ui.close': 'close:modal'
},
onRender: function() {
this.view.$el.addClass('modal ' + this.getOption('modalClasses'));
},
onAttach: function() {
this.view.$el.modal(this.getOption('modalOptions') || {});
},
onCloseModal: function() {
this.view.$el.modal('hide');
},
triggerFinish: function() {
this.view.triggerMethod('destroy:modal');
}
});
Example Behaviors
104
Marionette Guides
We provide a destroy:modal handler that a parent view can listen to so it knows when it's
safe to empty the region. For example:
var ModalView = Marionette.LayoutView.extend({
behaviors: {
modal: {
behaviorClass: ModalBehavior
}
},
template: require('./templates/view_with_modal.html')
});
Tooltip
The tooltip behavior is a lot easier than the modal, as we'll see:
var TooltipBehavior = Marionette.Behavior.extend({
defaults: {
tooltipSelector: '.has-tooltip'
},
ui: function() {
return {
tooltip: this.getOption('tooltipSelector');
}
},
onRender: function() {
this.ui.tooltip.tooltip();
}
})
Example Behaviors
105
Marionette Guides
Example Behaviors
106
Marionette Guides
107
Marionette Guides
Models
We'll start by looking at the Backbone.Model and how to use it. Let's create a simple note
with a timestamp, content, and title.
var Note = Backbone.Model.extend({
defaults: {
timestamp: '',
content: '',
title: ''
}
});
var note = new Note({
timestamp: '2015-09-02 11:00:00',
content: "I'm writing a book!",
title: 'Doing something'
});
108
Marionette Guides
console.log(note.get('content'));
// Doing something
If we have some new data to put in this note, we can update it like so:
note.set('content', 'New content');
Besides brevity, there's a very good reason we'd want to update multiple fields at once, as
we'll get to later.
There's no reason we have to stick to the fields defined by defaults (we don't even need to
define them), so we can add some ad-hoc data:
note.set('reminder', '2015-09-04 11:01:00');
Server synchronization
This is all well and good but it still takes us no closer to pushing and pulling data to a server.
First, we'll need to define a pretend server. Let's give it the URL http://example.com/note/1
which, when we GET it, returns:
{
"id": 1,
"title": "My note",
"content": "Some content saved online",
"timestamp": "2015-09-02 11:01:02",
"reminder": null
}
109
Marionette Guides
The Model.fetch method knows how to construct a URL from its urlRoot and id
properties - namely appending id to urlRoot . Like most web calls in JavaScript, fetch is
asynchronous - execution will continue before the web request completes.
If we wanted to perform an action on the data once the fetch method returns, we can attach
a success callback, as in jQuery:
note.fetch({
success: function(response, model) {
// Do something
}
});
When we're done modifying our data and want to save it, we'll call Model.save() and
Backbone will save the data back to the server:
note.set('title', 'New title');
note.save();
Again, like fetch, we can use the success callback to execute based on the result of the call:
note.save(
{
title: 'New title'
},
{
success: function(response, model) {
// Do something
}
);
110
Marionette Guides
This dependency on the success and error callbacks doesn't help us when our model is
attached to multiple views - what happens if one view updates the model but another one
needs to be changed? Do both views need to know about each other? Let's find out.
Events
Models use events to signal that something has happened that another object may be
interested in. For example, a model can signal that fields have changed, it has successfully
saved its data (or failed), or that it has fetched a new set of data from the server. These
events can then be listened to by views, or any other object that knows about the model.
Using this, our models can affect multiple parts of an application without needing to be
explicitly told about them. Let's look at some examples:
note.set('title', 'New title');
// Fires the 'change' and 'change:title' events
note.set('content', 'New content');
// Fires another 'change' event and 'change:content' event
note.set({
content: 'Newer content',
title: 'Newer title'
});
// Fires only one 'change' event, 'change:content', and 'change:title'
note.save();
// Fires the 'request' event, then either 'sync' or 'error' depending on the
// server response
note.fetch();
// Like save, fires `request`, then `sync` or `error` depending on the response
A full, up-to-date, list of events can always be found on the Backbone documentation, with a
description of when each event fires. As we can see in the example, change is fired every
time we successfully set a field - if we want to only fire a single change event, we must
pass an object into set with all the fields we want to update.
Listening to Events
For these events to be useful, we need to attach listeners that act when the event is fired.
When the title is updated, let's listen for it like so:
111
Marionette Guides
We can also listen to events from our views by using the modelEvents attribute. When
defining our view:
var NoteView = Marionette.LayoutView.extend({
modelEvents: {
'change:title': 'updateTitle'
},
updateTitle: function(model, value, options) {
console.log('title for NoteView is now ' + value);
}
});
This will trigger the updateTitle method on our NoteView whenever title changes.
Custom Events
When you start building your apps, you'll notice that Backbone doesn't always give you the
events you need. As a common example, there's no way to distinguish between a
successful save and a successful data pull - sync covers both cases.
Luckily, we can fire custom events on models, and Marionette views are capable of binding
to them. Let's see an example of this now:
112
Marionette Guides
Now, when save succeeds, our saveComplete method gets called and Note saved makes
it to the log. Whilst useful, this example has a flaw in that only NoteView will trigger the
saved event, and so it's not much better than just executing the code in success directly.
This could be acceptable if our save is only called in this view - other views can still happily
listen to the saved event, even though they don't fire it.
Collections
Managing a single model is good, and we can do a lot of interesting things with just this
knowledge. When it comes to building applications, we will normally operate on collections
of data to render lists, draw charts, and otherwise aggregate data.
The Backbone.Collection class is used to model and act on multiple models at the same
time. Let's take our note example and see how we could build up a list of notes that we'd like
to draw later:
113
Marionette Guides
This will store the list of notes. After creating our collection, we can add new notes using the
add method, like so:
Since we only have a single type in our list, let's set this constraint in the definition:
var NoteCollection = Backbone.Collection.extend({
model: Note
});
var noteList = new NoteCollection([
{title: 'Note1', content: 'Content1'},
{title: 'Note2', content: 'Content2'}
]);
noteList.add({title: 'Note3', content: 'Content3'});
Now Backbone will convert our raw JavaScript objects into Note objects, both on creation
and when adding new objects. It will also enforce this:
var NotANote = Backbone.Model.extend();
noteList.add(new NotANote({something: 'Some Value'}));
// ERROR - This is not a Note object
Synchronizing data
Collections are used to pull lists of data from our server and build an abstraction we can
operate on. Assuming that our note-taking app has a server endpoint /note/ that returns a
JSON list in the form:
114
Marionette Guides
[
{
"content": "Note Content",
"title": "Note title",
"reminder": null,
"timestamp": "2015-09-01 12:01:00",
"id": 1
},
{
"content": "Another note with some content",
"title": "Note title2",
"reminder": "2015-09-02 09:00:00",
"timestamp": "2015-09-01 12:51:00",
"id": 2
},
{
"content": "Yet another note",
"title": "Note title 3",
"reminder": null,
"timestamp": "2015-02-01 12:01:00",
"id": 3
}
]
Now let's define our collection that references the URL endpoint:
var NoteCollection = Backbone.Collection.extend({
url: '/note/',
model: Note
});
var noteList = new NoteCollection();
noteList.fetch();
Like in Model , we use the fetch method to pull our data from the server and insert it into
our collection.
115
Marionette Guides
116
Marionette Guides
117
Marionette Guides
FAQ
FAQ
118
Marionette Guides
Cookbook
It'd be great if we can add all the cookbook recipes with each recipe as a subchapter.
Cookbook
119
Marionette Guides
Appendix
Appendix
120
Marionette Guides
Driver file
driver.js :
Views
Everything in this section is contained in the views/ directory.
layout.js :
Todo app
121
Marionette Guides
form.js :
Todo app
122
Marionette Guides
// views/form.js
var Marionette = require('backbone.marionette');
module.exports = FormView;
list.js :
// views/list.js
var Marionette = require('backbone.marionette');
var ToDo = Marionette.LayoutView.extend({
tagName: 'li',
template: require('../templates/todoitem.html')
});
module.exports = TodoList;
Templates
Everything here is stored in the templates directory.
Todo app
123
Marionette Guides
layout.html :
<div class="list"></div>
<div class="form"></div>
form.html :
todoitem.html :
Models
Everything here is under the models directory:
todo.js
Todo app
124
Marionette Guides
module.exports = ToDo;
Todo app
125
Marionette Guides
Index file
index.html :
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="blog-hook"></div>
<script src="static/js/app.js"></script>
</body>
</html>
As with all the appendix items here, this is a simplistic HTML file. Feel free to utilize styling
libraries such as Bootstrap to jazz it up.
Driver file
driver.js :
126
Marionette Guides
var initialData = {
posts: [
{
author: 'Scott',
title: 'Why Marionette is amazing',
content: '...',
id: 42,
comments: [
{
author: 'Steve',
content: '...',
id: 56
}
]
},
{
author: 'Andrew',
title: 'How to use Routers',
content: '...',
id: 17
}
]
};
127
Marionette Guides
initialize: function() {
/** The region manager gives us a consistent UI and event triggers across
our different layouts.
*/
this.options.regionManager = new Marionette.RegionManager({
regions: {
main: '#blog-hook'
}
});
var initialData = this.getOption('initialData');
var layout = new LayoutView({
collection: new BlogList(initialData.posts)
});
this.getOption('regionManager').get('main').show(layout);
/** We want easy access to our root view later */
this.options.layout = layout;
},
/** List all blog entrys with a summary */
blogList: function() {
var layout = this.getOption('layout');
layout.triggerMethod('show:blog:list');
},
/** List a named entry with its comments underneath */
blogEntry: function(entry) {
var layout = this.getOption('layout');
layout.triggerMethod('show:blog:entry', entry);
}
})
var Router = Marionette.AppRouter.extend({
appRoutes: {
'blog/': 'blogList',
'blog/:entry': 'blogEntry'
},
/** Initialize our controller with the options passed into the application,
such as the initial posts list.
*/
initialize: function() {
this.controller = new Controller({
initialData: this.getOption('initialData');
});
}
});
module.exports = Router;
128
Marionette Guides
module.exports = Backbone.Model.extend({
/** Let us inject 0 comments in from the data set
*/
defaults: function() {
return {
comments: []
}
}
});
collections/blog.js :
models/comment.js :
module.exports = Backbone.Model.extend();
collections/comment.js :
module.exports = Backbone.Collection.extend({
model: Comment
});
Views
views/layout.js :
129
Marionette Guides
views/list.js :
130
Marionette Guides
views/blog.js :
131
Marionette Guides
Templates
templates/blog/layout.html :
<h1>Marionette Blog</h1>
<div class="layout-hook"></div>
templates/blog/item.html :
132
Marionette Guides
<a href="/blog/<%- id %>"> <!-- This won't actually link anything -->
<div class="title"><%- title %></div>
by
<span class="author"><%- author %></span>
</a>
templates/blog/blog.html :
templates/blog/comment.html :
133