Eventbrite's Backbone.js and Marionette.js guidelines to ensure consistency in JavaScript code.
Backbone and Marionette come with a rich API and also functions provided by underscore (_
) and jquery ($
). Although good and fast to use, these utilities can be hard to navigate or even challenging when building large-scale applications. Many times midway through development, we find that were used the tools incorrectly and have to change course, resulting in Frankenstein code. This guide will attempt to ease some of these problems.
Backbone and Marionette come with rich APIs as well as functions provided by underscore (_
) and jquery ($
). Although good and fast to use, these utilities can be hard to navigate or even challenging when building large-scale applications. Many times, we have found midway through development that we had used the tools incorrectly and must change course, resulting in Frankenstein code. This guide aims to ease some of these problems.
- Backbone.js
- Marionette.js
- Additional plugins
- Common terminology
- File structure
- Ordering
- Statics
- Styling
- Context
- Function
- Hydrating apps
- Marionette.Layout
- Marionette.Views
- Backbone.Model
- Backbone.Collection
- Marionette Artifacts Life Cycle
- Backbone Life Cycle
- Architecting JS Apps at Eventbrite
- Debugging common issues
- Testable Modular JS with Backbone, Jasmine & Sinon
From the Backbone.js docs:
Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface.
Eventbrite still uses v1.0.0 of Backbone. For more, see Getting started with Backbone.js.
NOTE: Backbone.View
is deprecated in favor of using Marionette views.
From the Marionette.js docs:
Marionette simplifies your Backbone application code with robust views and architecture solutions.
Eventbrite still uses v1.8.8 of Marionette.ItemView. For more, see Marionette v1.8.8 docs.
NOTE: Marionette.Application.module
is deprecated in favor of Marionette.Layout
. You will still see it used in certain parts of the product, such as in Listings or My Contacts.
NOTE: Marionette.Controller
is deprecated in favor of Marionette.Layout
. Marionette.Object
is also available. It was taken from a later version of Marionette and stitched in.
We have a couple of plugins/libraries to enhance and simplify our use of Backbone/Marionette:
Backbone.Advice
: Adds functional mixin abilities for Backbone objectsdorsal
: An HTML decorator libraryBackbone.Stickit
: Backbone data binding plugin that binds Model attributes to View elementsBackbone.Validation
: A validation plugin for Backbone that validates both your model as well as form inputBackbone.Wreqr
: Messaging patterns for Backbone applications
- context -
- hydrating -
- bootstrap -
- module -
- component -
- app -
- parameters -
- argument -
- config -
- artifact -
- helpers -
- mixins -
- base bundle -
- bundle -
We structure our Backbone projects like so:
- js/src/require/component/feature_name/
- feature_name.js
- model.js
- model.spec.js
- view.js
- view.spec.js
- sub_feature/
- feature_name.js
- model.js
- model.spec.js
- view.js
- view.spec.js
- router.js
feature\_name.js
contains the code to initialize your module.
Each model, each view, and the router gets its own file mirroring its JavaScript naming. For example, EB.ProjectName.FirstModel
is in eb/feature_name/first_model.js
.
A reference to Marionette
can actually be retrieved from a reference to Backbone
. However, we recommend requiring Marionette
separately, so that if we try to simplify our stack, we don't have to change a considerable amount of code to remove the Backbone
dependency/namespace:
// good
var Marionette = require('marionette');
return Marionette.ItemView.extend({ /* do something here */ });
// bad (access Marionette from Backbone)
var Backbone = require('backbone');
return Backbone.Marionette.ItemView.extend({ /* do something here */ });
Whenever possible, return only one artifact per file:
// good
//view_a.js
var Marionette = require('marionette');
return Marionette.ItemView.extend({ /* do something here */ });
//view_b.js
var Marionette = require('marionette');
return Marionette.ItemView.extend({ /* do something here */ });
// bad (returning multiple artifacts in one file)
var Marionette = require('marionette'),
ViewA = Marionette.ItemView.extend({ /* do something here */ }),
ViewB = Marionette.ItemView.extend({ /* do something here */ });
return {ViewA: ViewA, ViewB: ViewB};
Whenever possible, return the artifact immediately instead of assigning to a variable that just gets returned afterward:
// good
var Marionette = require('marionette');
return Marionette.ItemView.extend({ /* do something here */ });
// bad (assigns the ItemView to a variable unnecessarily)
var Marionette = require('marionette'),
MyItemView;
MyItemView = Marionette.ItemView.extend({ /* do something here */ });
return MyItemView;
- Outside
Marionette.View
definition
dependencies
requirementstatic
functionsconfig
objects
- Ordering for
Marionette.View
:
el
template
tagName
itemView
Composite ViewitemViewContainer
Composite ViewclassName
ui
regions
Layoutevents
triggers
modelEvents
initialize
templateHelpers
onBeforeRender
render
onRender
- all the remaining life cycle methods
- clickHandlers or eventHandlers
- getter methods
When we write views or models/collections, we tend to enclose all of our functions as methods on the artifact. However, sometimes these methods are really just static helpers that don't need context (i.e. not bound to this
). In this case, it's better to extract out the function as a private helper, which also simplifies the API exposed by the artifact:
// good
var Marionette = require('marionette');
function extractAttributes(options) {
var attrs = {};
// do stuff
return attrs;
};
return Marionette.ItemView.extend({
initialize: function(options) {
var attrs = extractAttributes(options);
this.model = new Backbone.Model(attrs);
};
});
// bad (extractAttributes is an additional method on the view unnecessarily)
var Marionette = require('marionette');
return Marionette.ItemView.extend({
initialize: function(options) {
var attrs = this.exractAttributes(options);
this.model = new Backbone.Model(attrs);
},
extracAttributes: function(options) {
var attrs = {};
// do stuff
return attrs;
}
});
Oftentimes an artifact needs some static/constant data that never need to change. Instead of having magic numbers/strings in the code, or having a configuration object attached to each instance, we should store the configuration information in a const object variable:
// good
var $ = require('jquery'),
Marionette = require('marionette'),
config = {
selectorName: 'someDynamicSelector',
isHiddenClass: 'is-hidden',
timeout: 10
};
return Marionette.ItemView.extend({
initialize: function(options) {
$(config.selectorName).add(config.isHiddenClass);
window.setTimeout(this.someCallback, config.timeout);
}
});
// ok (config objects exists as a property for each view instance)
var $ = require('jquery'),
Marionette = require('marionette');
return Marionette.ItemView.extend({
config: {
selectorName: 'someDynamicSelector',
isHiddenClass: 'is-hidden',
timeout: 10
},
initialize: function(options) {
$(this.config.selectorName).addClass(this.config.isHiddenClass);
window.setTimeout(this.someCallback, this.config.timeout);
}
});
// bad (uses magic numbers/strings)
var $ = require('jquery'),
Marionette = require('marionette');
return Marionette.ItemView.extend({
initialize: function(options) {
$('someDynamicSelector').addClass('is-hidden');
window.setTimeout(this.someCallback, 10);
}
});
To simplify searches when trying to find templates, put CSS classes in handlebars templates instead of coupling it with the view logic:
// good
// some_view.handlebars
<div class="g-cell g-cell-12-12"></div>
// some_view.js
var Marionette = require('marionette'),
template = require('hb!./some_view.handlebars');
return Marionette.ItemView({
template: template
});
// bad (CSS classes aren't separated out)
var Marionette = require('marionette');
return Marionette.ItemView({
className: 'g-cell g-cell-12-12'
});
In order to use native JavaScript whenever possible, use Function.prototype.bind
instead of _.bind
and _.bindAll
to bind callback handlers:
// good
return Marionette.ItemView.extend({
initialize: function(options) {
this.listenTo(channel.vent, 'someSignal', this.someMethod.bind(this));
this.listenTo(channel.vent, 'anotherSingle', this.anotherMethod.bind(this));
},
someMethod: function(options) {
/* do something */
},
anotherMethod: function(options) {
/* do something */
}
});
// bad (uses _.bindAll)
return Marionette.ItemView.extend({
initialize: function(options) {
_.bindAll(this, 'someMethod', 'anotherMethod');
this.listenTo(channel.vent, 'someSignal', this.someMethod);
this.listenTo(channel.vent, 'anotherSingle', this.anotherMethod);
},
someMethod: function(options) {
/* do something */
},
anotherMethod: function(options) {
/* do something */
}
});
// bad (uses _.bind)
return Marionette.ItemView.extend({
initialize: function(options) {
this.listenTo(channel.vent, 'someSignal', _.bind(this.someMethod));
this.listenTo(channel.vent, 'anotherSingle', _.bind(this.anotherMethod));
},
someMethod: function(options) {
/* do something */
},
anotherMethod: function(options) {
/* do something */
}
});
Don't store derived/calculated data on the this
context of a view. Doing so makes it fragile and error-prone, because nothing prevents that data from being modified. Furthermore, it complicates quality code review (AKA static analysis) because the reviewer must first investigate the origin of the instance property.
Whenever possible, calculate the data on demand, either in the model or in the view:
// good
return Marionette.ItemView.extend({
getComputedData: function() {
return this.model.getComputedData();
}
});
// ok (the View is doing data calculations that could be done by Model)
return Marionette.ItemView.extend({
getComputedData: function() {
return someDataTransformation(this.options);
}
});
// bad (storing computed data in the View context)
return Marionette.ItemView.extend({
initialize: function(options) {
this.computedData = someTransformation(options);
}
getComputedData: function() {
return this.computedData;
}
});