Skip to content

Latest commit

 

History

History
 
 

backbone

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Eventbrite Backbone & Marionette Coding Style Guide

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.

Table of Contents

  1. Backbone.js
  2. Marionette.js
  3. Additional plugins
  4. Common terminology
  5. File structure
  6. Ordering
  7. Statics
  8. Styling
  9. Context
  10. Function
  11. Hydrating apps
  12. Marionette.Layout
  13. Marionette.Views
  14. Backbone.Model
  15. Backbone.Collection
  16. Marionette Artifacts Life Cycle
  17. Backbone Life Cycle
  18. Architecting JS Apps at Eventbrite
  19. Debugging common issues
  20. Testable Modular JS with Backbone, Jasmine & Sinon

Backbone.js

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.

⬆ back to top

Marionette.js

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.

⬆ back to top

Additional plugins

We have a couple of plugins/libraries to enhance and simplify our use of Backbone/Marionette:

  • Backbone.Advice: Adds functional mixin abilities for Backbone objects
  • dorsal: An HTML decorator library
  • Backbone.Stickit: Backbone data binding plugin that binds Model attributes to View elements
  • Backbone.Validation: A validation plugin for Backbone that validates both your model as well as form input
  • Backbone.Wreqr: Messaging patterns for Backbone applications

⬆ back to top

Common terminology

  • context -
  • hydrating -
  • bootstrap -
  • module -
  • component -
  • app -
  • parameters -
  • argument -
  • config -
  • artifact -
  • helpers -
  • mixins -
  • base bundle -
  • bundle -

⬆ back to top

Folder Structure

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.

File structure

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 */ });

⬆ back to top

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;

⬆ back to top

Ordering

  • Outside Marionette.View definition
  1. dependencies requirement
  2. static functions
  3. config objects
  • Ordering for Marionette.View:
  1. el
  2. template
  3. tagName
  4. itemView Composite View
  5. itemViewContainer Composite View
  6. className
  7. ui
  8. regions Layout
  9. events
  10. triggers
  11. modelEvents
  12. initialize
  13. templateHelpers
  14. onBeforeRender
  15. render
  16. onRender
  17. all the remaining life cycle methods
  18. clickHandlers or eventHandlers
  19. getter methods

⬆ back to top

Statics

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);
	}
});

⬆ back to top

Styling

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'
});

⬆ back to top

Context

Binding

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 */
	}
});

⬆ back to top

Data

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;
	}
});

⬆ back to top