Module.js

A lightweight library for creating modular JavaScript applications

Module.js enables you to build large-scale JavaScript applications that are composed of several decoupled, testable and reusable modules.

It promotes apps to be built using a modular, event-driven architecture. Each module is aggressively self-concerned, having it's own DOM scope, it's own lifecycle management, and it's own messaging/events mechanism.

The project his hosted on GitHub, and is available to use under the MIT Software License. You can report bugs on the GitHub issues page.

Module.js has no hard dependencies; however if you require DOM manipulation then jQuery or Zepto are needed.

Introduction

When creating web apps in JavaScript, it’s essential to have well-organized code; maintaining a spaghetti-coded project will only cause you pain and suffering as entropy prevails.  Module.js aims to help you take a modular approach when building JavaScript web apps, making your code more organized, scalable and maintainable.

It provides a high-level architectural structure, leaving lower-level implementation choices (such as an individual module's usage of views, models, templating etc.) to be decided by the developer. It's goal is to create disparate modules that can be dropped-in to any part of your application. Just provide a module with a DOM scope on instantiation, and subscribe to any events of interest that it emits.

You'll find the module.js API described in full below. For more help, take a look at the tutorials. For further reading on large scale application development, try Large Scale JavaScript by Addy Osmani, and The Future is Modules not Frameworks by John Hann.

Module

To create a module, simply instantiate the Module constructor. If the module is to be concerned with the DOM, then a DOM reference (in the form of a CSS selector or jQuery/Zepto object) should be passed in as the modules scope. This will automatically set the module scope under-the-covers using setScope().

// creating a module
var myModule = new Module();

// setting properties on instantiation
myModule = new Module({
  initialize : function(){},
  scope : '#app-wrapper'
});

The initialize() method is automatically invoked when the module is created, if defined. You can extend the module constructor too:

// extending the Module constructor
var App = Module.extend({
  initialize : function(){},
  start : function(){},
  stop : function(){}
});
// create our app, passing in it's scope
var app = new App({scope : '#app-wrapper'});
app.start();

You'll notice above that unimplemented start and stop methods have been supplied. These are the primary strategies to manage a modules lifecycle.

Properties

scope String

A CSS selector representing the modules DOM scope, for example '#anElement'. The value will be null if scope has not been set.

$scope Object

A jQuery/Zepto object referencing the modules DOM scope. The value will be null if scope has not been set or jQuery/Zepto isn't being used.

events Object

An events hash. It provides declarative callbacks to handle events that occur on a module's properties, allowing the module to mediate event handling across multiple objects.

The context of the callback handlers ( this ) will always be the current module.

A basic event declaration includes the property name of interest, the event it emits, and a handler function.

events:{
  'objectName eventName' : function(){ // handle event here }
}

Suppose a module has a Backbone.Model type object that will emit a 'change' event. We can listen for this event by doing the following:

new Module({
  initialize: function(){
    this.model = new Backbone.Model();
    this.view = new Backbone.View();
    this.bindEvents();
  },
  events: {
    'model change' : function( data ){
      this.view.doSomething();
    }
  }
});

If an object emits multiple events, we can assign multiple handlers too:

events:{
  'model' : {
    'change' : function( data ){},
    'reset' : function( arg ){},
    'destroy' : function( arg1, arg2 ){}
  }
}

Furthermore, we can listen out for similar events across multiple objects by using a '*' wildcard:

events:{
  // handle 'change' event on the model object
  'model change' : function( data ){ // do something },

  // handle 'change' event on any object
  '* change' : function( data ){ // captures same event },

  // handle multiple events on any object
  '*' : {
    'change' : function( data ){},
    'error' : function(){}
  }
}

When using mappings, matching event handlers are automatically bound - bindEvents() does not need to be called. Suppose a module has a map containing chat, email and notification sub-modules. Event handlers can be declared like so:

App = Module.extend({
  initialize: function(){
    this.widgets = this.createMap('widgets');
  },
  start: function(){
    // create each widget, passing in their scope
    this.widgets.add({
      'email' : new EmailModule({scope: this.$('#email')}),
      'chat' : new ChatModule({scope: this.$('#chat')}),
      'notification': new NotificationModule({scope: this.$('#notify')}),
    });
  },
  events: {
    // handles the chat widgets 'newChat' event
    'widgets.chat newChat': function( chatDetails ){
      // do something with the chat details
    },
    // handles any widgets 'error' event by using '*'
    'widgets.* error': function( widget, widgetName, error ){
      // do something with the error
    }
  }
});

Note how when using a '*' wildcard, the emitting object and objectName are injected into the beginning of the event handler function's arguments.

Communication Methods

on ( eventstring, handlerfunction, [context] )

Provides a mechanism to subscribe to events emitted by a module. The method binds a callback function to the module. The callback will be invoked whenever the event is fired.

myModule.on('error', function( error, message ){
  throw error;
})

To supply a context value for this when the callback is invoked, pass the optional third argument.

Related note:  A modules events property can be used to declare event handlers on any of a module's properties which emit events.

off ( [eventstring], [handlerfunction], [context] )

Removes/unbinds a previously-bound callback function from the module. If no context is specified, all of the versions of the callback with different contexts will be removed. If no callback is specified, all callbacks for the event will be removed. If no event is specified, callbacks for all events will be removed.

// Removes just the 'onError' event handler.
myModule.off('error', onError);

// Removes all 'error' event handlers.
myModule.off('error');

// Removes all event handlers on the module.
myModule.off();

Remember to unbind events when you destroy your module - see the Module Lifecycle Management tutorial.

emit ( eventstring, [*argsany] )

Emits an event. Subsequent arguments provided to emit will be passed along to any event handlers.

// emit a 'newMessage' event
var myModule = new Module({
  initialize : function(){
    this.emit('initialized');
  }
});

Events are described as any 'significant change in state'. Modules should emit events when they have any useful information to present for consumption by others.

DOM Scope Methods

$ ( selectorstring )

If jQuery or Zepto is included on the page, the $ method performs DOM queries specifically scoped to the modules DOM reference. This is the equivalent to this.$scope.find().

myModule = new Module({
  initialize : function(){
    // create a view within the module, passing in it's scope
    this.view = new Backbone.View({
      el: this.$('.foo')
    });
  },
  start : function(){
    this.view.render();
  },
  // set any events on the view
  events : {
    'view popUp' : function(){ .. }
  }
});

// runs a scoped query within our module
myModule.$('.foo');

hide ()

Hides the modules DOM element - equivalent to this.$scope.hide(). All other DOM manipulation should be delegated to views.

show ()

Reveals the modules DOM element - equivalent to this.$scope.show(). All other DOM manipulation should be delegated to views.

setScope ( scopestring or jquery/Zepto object )

Sets the module's scope. Accepts a CSS selector or a jQuery/Zepto object referencing a DOM element. Automatically updates the scope and $scope properties when invoked.

Utility Methods

bindEvents ()

Binds event handlers set in events to any matching properties on the module.

Module.extend({
  initialize: function(){
    this.foo = new Model();
    this.bar = new View();
    // bind matching events to the above properties
    this.bindEvents();
  },
  events: {
    'foo testEvent': function(){ // implement },
    'bar someEvent': function(){ // implement }
  }
});

If you are only using Maps to store objects in your modules, matching events are automatically bound and bindEvents does not need to be called.

createMap ( nameString )

Returns a Map object. The provided name is the map's identifier; it's used to automatically bind any matching event declarations in the events property when objects are added to the map.

Module.extend({
  initialize: function(){
    // create a map to store 'subModules'
    this.subModules = this.createMap('subModules');
    this.subModules.add({
      'email' : someObject,
      'chat' : anotherObject
    })
  },
  // our 'subModules' map events are automatically bound
  events: {
    'subModules.*': {
      'error': function(){},
      'update': function(){}
    },
    'subModules.email message': function( msg ){}
  }
});

log ( [*any] )

Logs out supplied arguments to the console, if the console is available.

throwException ( errorError, [messagestring] )

Throws an error and emits an error event.

module.on('error', function(error, message){
  console.log( message );
});
// logs out 'Something went wrong';
module.throwException(new Error('Something went wrong'));

Lifecycle Methods

start ()

Implement this method as the sole entry-point to start the module. When called, the start() method should fully take care of module installation - see the Lifecycle Management tutorial.

stop ()

Implement this method as the sole entry-point to stop the module. On tear-down, remember to destroy internal objects and unbind any events - see the Lifecycle Management tutorial.

'Static' Functions

Module.extend ( propertiesobject, [classPropertiesobject] )

Use to create your own Module 'class' by extending Module. Provide instance properties, as well as optional classProperties to be attached directly to the module's constructor function.

var MyModule = Module.extend({
  initialize: function(){ // implement },
  start: function(){ // implement },
  stop:  function(){ // implement },
  events {
    // implement
  }
});

Module.installEventsTo ( targetObjectobject )

Mixes in an event aggregator onto the target object, providing the object with the same event method signature as described in Module Messaging.

var testObject = {};
Module.installEventsTo( testObject );
testObject.emit('iCanEmitEvents');

Map

add ( nameString, objectObject )

Adds an object to the current map.

var models = myModule.createMap();

// add a single object
models.add('myModel', new Model());

// add multiple objects
models.add({
  'myModel': new Model(),
  'yourModel': new Model(),
  'ourModel': new Model()
});

Multiple objects can be added with the same name, if desired. An array of the objects is then returned upon a get call if more than one object exists in the mapped value.

// add a single object mapped to 'test'
models.add('test', new Model());

models.get('test'); // returns model

// add more objects to the same 'test' mapped value
models.add('test', new Model());
models.add('test', new Model());

models.get('test'); // returns [ model, model, model ]

each ( mixed )

Iterates over each object in the map, invoking a method or applying a callback function depending on the arguments provided.

Method invocation on each object via passing method name and arguments:

// executes 'foo()' on each object in the map
models.each('foo');

// executes 'foo()' on each, passing true into the method
models.each('foo', true )

Iterating over each object by passing in a function callback:

// applies callback on each object in the map
models.each( function( model, name ){
  model.foo();
});

eachTry ( methodNameString, [*any] )

Similar to each, however if the stated method does not exist then no error is thrown. This is useful when we want to try invoke a method on all of our objects in a map knowing that the method may not exist on some objects.

length ()

Returns the number of objects currently in the map.

on ( eventstring, handlerfunction, [context] )

Handle events on all objects within the map. See on() and declaring map events. The mapped object emitting the event and it's name are injected into the handler function.

// add objects to the models map
models.add({
  'myModel': new Model(),
  'yourModel': new Model(),
  'ourModel': new Model()
});

// declare an event handler
models.on( 'change', function( model, modelName, data ){
  this.log('The model ' + modelName + ' has changed.');
});

// logs out 'The model myModel has changed'
models.get('myModel').emit('change');

off ( [eventstring], [handlerfunction], [context] )

Remove event handlers on all objects within the map. See off()

remove ( [objectNameString] )

Removes one or all objects from a map.

models.add({
  'myModel': new Model(),
  'yourModel': new Model(),
  'ourModel': new Model()
});
models.length(); // 3

// remove 'myModel' object
models.remove('myModel');
models.length(); // 2

// remove all objects in the map
models.remove();
models.length(); // 0

Tutorials

Creating a Module

Coming soon.

Using Module Scope

Coming soon.

Using Maps

Coming soon.

Hooking-Up Events

Coming soon.

Creating a Modular Application

Coming soon.

Module Lifecycle Management - Cleaning Up After Yourself

Coming soon.