##Why Modules? Writing your JavaScript in a modular fashion provides many benefits over writing in the traditional spaghetti style of sprawling global functions. Benefits include:
Hanging all your JavaScript code off of the window
object`, pollutes the global scope and increases the chance of naming collisions within your own application and also with third-party libraries. It makes your code more error-prone and susceptible to interference from external sources. Writing your code with a module pattern hides your code from the global scope and only exposes what you want to.
Modules make it easy to obey the Single Responsibility Principle. Your modules should do one thing and one thing well. Larger components should be built up from a combination of small tightly-focused modules.
Having small modules with a singular purpose make them much easier to unit test than large sprawling functions with complex intertwined responsibilities. A benefit of having your module unit tested is that it makes it trivial to refactor and improve your code over time without fear of breaking existing functionality.
Small modules are much easier maintain as by their nature they are responsible for a specific area of functionality. This makes it easier to track down bugs and also to add new features over time.
Abstracting your code into tightly-focused modules increase the chances of reusing these modules throughout the app. This results in code which is more tested and less prone to bugs. It also allows for more rapid development as you are essentially building on top of a library of tried-and-tested modules.
Below is an example module format. It's based off of the Module Pattern.
;(function(IC, $, _){
//Enables stricter syntax JavaScript parsing
'use strict'
//Set up a namespace for your module
IC.namespace('Util.YourModule');
//Implement your module
IC.Util.YourModule = {
...
};
})(window.IC || {}, jQuery, _);
While ideally it would be nice to take advantage of module patterns such as AMD or CommonJS, allowing you to specify your dependencies, it is too premature to do so. This could be a good next step once our codebase is has moved to a more modular structure.
Additional information: Why the leading semicolon? What is JavaScript strict mode?
By hanging lots of functions on the window
object, it pollutes the global scope. Defining namespaces for your modules are a good way to prevent this. Our namespace strategy for the Intercom web app consists of only creating a single global variable IC
, an acronym for Intercom. This was selected as it doesn't collide with the global namespace of the Intercom widget, Intercom
.
Creating a namespace for your module is as simple as calling:
IC.namespace('App.View.ModuleName');
We are using the namespace
function inspired by the YUI mixin which makes it trivial to create namespaces and avoid overwriting already existing namespaces. How it works is, if a particular path in the namespace already exists its properties are copied in, otherwise an empty object literal is created.
The window.IC
namespace is created and bootstrapped in assets/javascripts/bootstrap.js
with the namespace
.
###IC.App Functionality specific to the app. Views and business logic.
###IC.Util
Library of reusable non-view utility functions. e.g. Backbone Extend
and Event
objects, dates, string manipulation etc.
##Directory Structure While not an exact one-to-one mapping, module namespaces should try to mirror the structure of the app/assets/javascripts directory. Examples
IC.Util.Events
=> app/assets/javascripts/util/events.js file.IC.App.View.Base
=> app/assets/javascripts/views/base.js file.IC.App.View.Selector
=> app/assets/javascripts/views/selector.js file.
##Components TODO Talk about coding guidelines. For example:
-
only use
js_
prefixed classes. No CSS styling classes, e.g.js_component
. -
State classes should be prefixed with
js_is_
e.g.js_is_active
. -
Don't add variables to the
prototype
of the component unless it is a constant value that is to be shared amongst all instances of the component. If you want instance variables, create them within theintialize()
function. -
Trigger events off of the component's
this
context e.g.this.trigger('selected.all')
-
In the
initialize()
function, only call_.extend(this, options)
after initializing all the component's default values. This guarantees that the passed-in options always take precedence over the default ones. -
Event name strings are namespaced and dot-delimited, e.g.
eventname.eventgroup
. The right-most token of the event is the most generalised, referring to an event group. The left-most token referring to the specific event being triggered. Take the following examples:selected.item
deselected.item
clicked.item
-
js_
HTML classes should not be used by CSS for styling. Instead provide the ability to customise the CSS state classes using the configuration options. For example:
//Component implementation
IC.App.View.Button = IC.App.View.Base.extend({
initialize: function(options) {
//Empty custom class by default
this.button_css_active_class = '';
_.extend(this, options);
},
activate: function(){
//Add the JS specific class as well as the custom class for styling purposes
this.$el
.addClass('js_is_active ' + this.button_css_active_class);
}
});
//Initialising the component with the custom class
var component = IC.App.View.Button.extend({
button_css_active_class: 'is-selected'
});