Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

Eventbrite Backbone & Marionette Coding Style Guide

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.

Table of Contents

  1. Backbone.js
  2. Marionette.js
  3. Additional plugins
  4. Common terminology
  5. File structure
  6. Statics
  7. Styling
  8. Context
  9. Function
  10. Hydrating apps 0. Static or on Bootstrap 0. dynamic
  11. Marionette.Layout 0. Regions
  12. Marionette.Views
  13. Backbone.Model 0. Handling errors
  14. Backbone.Collection 0. Handling errors
  15. Marionette Artifacts Life Cycle
  16. Backbone Life Cycle
  17. 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
  18. debugging common issues

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

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

⬆ 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

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

⬆ back to top