Guidelines used by Eventbrite to provide consistency and prevent errors in JavaScript code written for Backbone.js and Marionette.js.
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.js
- Marionette.js
- Additional plugins
- Common terminology
- File structure
- Statics
- Styling
- Context
- Function
- Hydrating apps 0. Static or on Bootstrap 0. dynamic
- Marionette.Layout 0. Regions
- Marionette.Views
- Backbone.Model 0. Handling errors
- Backbone.Collection 0. Handling errors
- Marionette Artifacts Life Cycle
- Backbone Life Cycle
- Architecting JS Apps at Eventbrite 0. app.js 0. Templates 0. File structure 0. File name conventions 0. Eb Flux 0. Stores 0. Views 0. Actions
- debugging common issues
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 -
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 simply 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;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 */
}
});Storing derived/calculated data on the this context of a view can be fragile and error prone because nothing prevents that data from being modified. Furthermore, it makes quality code review (aka static analysis) more challenging as the reviewer needs to first investigate where the instance property originates.
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;
}
});