soma.js

Scalable javascript framework

What is soma.js?

soma.js is a framework created to build scalable and maintainable javascript applications.

The success to build large-scale applications will rely on smaller single-purposed parts of a larger system. soma.js provides tools to create a loosely-coupled architecture broken down into smaller pieces.

The different components of an application can be modules, models, views, plugins or widgets. soma.js will not dictate its own structure, the framework can be used as a model-view-controller framework, can be used to create independent modules, sandboxed widgets or anything that will fit a specific need.

For a system to grow and be maintainable, its parts must have a very limited knowledge of their surrounding. The components must not be tied to each other (Law of Demeter), in order to be re-usable, interchangeable and allow the system to scale up.

soma.js is a set of tools and design patterns to build a long term architecture that are decoupled and easily testable. The tools provided by the framework are dependency injection, observer pattern, mediator pattern, facade pattern, command pattern, OOP utilities and a DOM manipulation template engine as an optional plugin.

Download

Browser

soma.js minified and gzip is 4.5 KB.

download soma.js

You can also download a zip file of the git repository, or use the package installer Bower:

bower install soma.js

Finally, add the script to your page:

<script type="text/javascript" src="soma.js"></script>

Node.js

$ npm install soma.js
var soma = require("soma.js");

Quick start

Here is an example of what a soma.js application could look like. In this case, following the MVC pattern.

The application contains a model that holds data, a template that manipulate a DOM element and an application instance that hook them up. The application instance must extend a framework function (soma.Application), while the other entities (model and template) are plain javascript function free of framework code.

The template function receives a template instance, a DOM element and the model, using dependency injection.

<div class="app">
  <h1>{{content}}</h1>
</div>

<script>

  // function model containing data (text to display)
  var Model = function() {
    this.data = "Hello world!"
  };

  // function template that will update the DOM
  var Template = function(template, scope, element, model) {
    scope.content = model.data;
    template.render();
  };

  // application function
  var QuickStartApplication = soma.Application.extend({
    init: function() {
      // model mapping rule so it can be injected in the template
      this.injector.mapClass("model", Model, true);
      // create a template for a specific DOM element
      this.createTemplate(Template, document.querySelector('.app'));
    }
  });

  // create application
  var app = new QuickStartApplication();

</script>

try it yourself

Application instance

The first step to build a soma.js application is to create an application instance. This is the only moment a framework function has to be extended, all the other entities can be re-usable plain javascript function, and framework agnostic.

The application instance will execute two functions so you can setup the architecture needed: the init and the start functions.

Here are different syntax to create an application instance.

var MyApplication = soma.Application.extend({
  init: function() {
    console.log('init');
  },
  start: function() {
    console.log('start');
  }
});

var app = new MyApplication();
var MyApplication = soma.Application.extend();

MyApplication.prototype.init = function() {
  console.log('init');
};

MyApplication.prototype.start = function() {
  console.log('start');
};

var app = new MyApplication();
var app = new soma.Application();

The constructor of an application can be overridden to implement your own. The "parent" constructor will have to be called so the framework can bootstrap. Click here to learn more about chaining constructor.

var MyApplication = soma.Application.extend({
  constructor: function(param) {
    this.param = param;
    soma.Application.call(this);
  },
  init: function() {
    console.log('init with param:', this.param);
  },
  start: function() {
    console.log('start');
  }
});

var app = new MyApplication("my application parameter");

Core elements

The framework core elements to help you build your own structure are the following:

They are available in the application or in other entities using dependency injection.

Here is how to get their references in the application instance.

var MyApplication = soma.Application.extend({
  init: function() {
    this.injector; // dependency injection pattern
    this.dispatcher; // observer pattern
    this.commands; // command pattern
    this.mediators; // mediator pattern
    this.instance; // facade pattern
  }
});

Here are two different syntax to get their references in other entities. The injector will either send the core elements in the constructor or set the corresponding properties.

var Entity = function(injector, dispatcher, commands, mediators, instance) {

};
var Entity = function() {
  this.injector = null;
  this.dispatcher = null;
  this.commands = null;
  this.mediators = null;
  this.instance = null;
};

Note: in case of injected variables (versus constructor parameters), it is necessary to set properties to a value (such as null), undefined properties are ignored by the injector.

Dependency injection

infuse.js is the dependency injection library developed for soma.js, click here to see the infuse.js documentation and tests.

What is dependency injection?

Dependency injection (DI) and inversion of control (IOC) are two terms often used in programming. Before explaining the benefits of dependency injection and how it is implemented in soma.js, here is a naive and hopefully accessible explanation.

A dependency is the relationships between two objects. This relationship is often characterized by side effects that can be problematic and undesirable at times.

Dependency injection is the process in which you are setting up rules that determine how dependencies are injected into an object automatically, as opposed to have hard-coded dependencies.

An object "A" that needs an object "B" to work has a direct dependency to it. Code where the components are dependent on nearly everything else is a "highly-coupled code". The result is an application that is hard to maintain, with components that are hard to change without impacting on others.

Dependency injection is an antidote for highly-coupled code. The cost required to wire objects together falls to next to zero.

Dependency Injection is based on the following assumptions:

Consider the process of using the dependency injection pattern as centralizing knowledge of all dependencies and their implementations.

A common dependency injection system has a separate object (an injector, often called container), that will instantiate new objects and populate them with what they needs: the dependencies. Dependency injection is a useful pattern to solve nested dependencies, as they can chain and be nested through several layers of the application.

The benefits of dependency injection are:

Using dependency injection in soma.js

Dependency injection involves a way of thinking called the Hollywood principle "don't call us, we'll call you". This is exactly how it works in soma.js. Entities will ask for their dependencies using properties and/or constructor parameters.

Using dependency injection implies creating rules, so the injector knows "what" and "how" to populate the dependencies.

These are called "mapping rules". A rule is simply "mapping" a name (string) to an "object" (string, number, boolean, function, object, and so on). These mapping names are used to inject the corresponding objects in either a javascript constructor or a variable.

A simple example

In the following example, two plain javascript functions will be instantiated and injected.

// simple function that will be injected into the model
// a boolean dependency is injected
var Config = function(debugMode) {

};
// simple function in which a config instance and a boolean is injected
var Model = function(config, debugMode) {

};
// mapping rule: map the string "config" to the function Config
injector.mapClass("config", Config);
// mapping rule: map the string "debugMode" to a boolean
injector.mapValue("debugMode", true);
// instantiate the model using the injector
var model = injector.createInstance(Model);

try it yourself

Singleton injection

By default, every single time the injector will find a dependency that needs to be instantiated (such as the Config function in the previous example), the injector will instantiate a new one. A third parameter can be used when defining the mapClass rule: the "singleton" parameter.

The effect will be that the injector will instantiate the function the first time it is asked, and send that same instance when it is asked again. This is useful to share an instance with several other components.

this.injector.mapClass("config", Config, true);

try it yourself

Where can injection be used?

Injection can be used in any function instantiated by the injector:

In the following example, dispatching an event will instantiate and execute a Command function, which will instantiate a Model function as it needs this dependency, which will instantiate a Config function as it needs this dependency.

var Config = function() {
  console.log("Config instantiated");
};

var Model = function(config) {
  console.log("Model instantiated with a config injected:", config);
};

var Command = function(model) {
  this.execute = function() {
    console.log("Command executed with a model injected:", model);
  }
};

// mapping rule: map the string "config" to the function Config
this.injector.mapClass("config", Config, true);
// mapping rule: map the string "model" to the function Model
this.injector.mapClass("model", Model, true);
// command mapped to an event
this.commands.add("some-event", Command);

try it yourself

Communication

soma-events is the observer library developed for soma.js, click here to see the soma-events documentation and tests.

Native event system

Communicating between entities in an application is an important part. Object orientated programming is all about states of objects and their interaction. Certain objects need to be informed about changes in others objects, some others need to inform the rest of the application and some others will do both.

The Observer pattern is a design pattern used to communicate between objects and reduce the dependencies. One the core elements of soma.js is the "dispatcher", an event system used to dispatch and listen to events throughout the entire application.

For example, a model component that receives data can use the dispatcher to notify the rest of the application that the data has changed. A view component can use the dispatcher to inform other components that a user input has just happened.

The library used to dispatch and listen to events (soma-events) is not a simple publish-subscribe library. In case of an application related to the DOM (in opposition to a node.js application), the events used are real DOM events, and the dispatcher shares a common interface to DOM 3 events. The result is that the soma.js dispatcher can be interchanged with a DOM Element for a complete application decoupling.

The soma-events library provides a custom dispatcher (usable in non-DOM environment), a event wrapper that will create the right events depending on the environment (webkit, internet explorer, non-DOM environment), and some shortcuts for ease-of-use.

Note: the shortcuts are not part of the DOM 3 events specification.

Creating and dispatching events

To dispatch an event, the first step is to create it. There are four ways to create events:

Create and dispatch an event using the dispatcher shortcut

Creating an event and dispatching it with the dispatcher shortcut is the easiest way. Note that the shortcut syntax is not part of the DOM 3 specification if it matters for the application.

dispatcher.dispatch("some-event");
dispatcher.dispatch("another-event", {data:"parameters"});

try it yourself

Create and dispatch an event instantiating the event wrapper

An instance of the event wrapper can be created, and dispatched following the DOM 3 dispatchEvent interface.

dispatcher.dispatchEvent(new soma.Event('some-event'));
dispatcher.dispatchEvent(new soma.Event('some-event', {data:"parameters"}));

try it yourself

Create and dispatch an event instantiating an extended event wrapper

The event wrapper can also be extended to specify custom variables or change the implementation.

var MyEvent = soma.Event.extend({
  constructor: function(type, params, bubbles, cancelable) {
    return soma.Event.call(this, type, params, bubbles, cancelable);
  }
});
dispatcher.dispatchEvent(new MyEvent('some-event'));
dispatcher.dispatchEvent(new MyEvent('some-event', {data:"parameters"}));

try it yourself

Create and dispatch an event using DOM Native methods

In case an entity requires a very strict decoupling, the dispatcher can be switch to a DOM Element (see the soma-event documentation), and a DOM Event can be used rather than using the event wrapper.

The goal of the event wrapper is hiding the complexity of creating cross-browser events, solving cross-browser different implementations and creating objects that represents events for node.js. The following example will only work in browsers that support the "document.createEvent" method.

// swap the dispatcher to a DOM Element (window in this example)
injector.removeMapping("dispatcher");
injector.mapValue("dispatcher", window);
// listen to an event
dispatcher.addEventListener('some-event', function(event) {
  console.log('event received:', event.type);
});
// dispatch an event
var event = document.createEvent("Event");
event.initEvent("some-event");
dispatcher.dispatchEvent(event);

try it yourself

Listening to events

The dispatcher implements the same interface as the DOM Event specification, the methods "addEventListener" and "removeEventListener" can be used to listen and stop listening to events.

A simple example

In the following example, an event is dispatched from the application instance, and listened to both in the application instance and a view entity.

var View = function(dispatcher) {
  dispatcher.addEventListener('some-event', function(event) {
    console.log('event received in a view:', event.type);
  });
}

var MyApplication = soma.Application.extend({
  init: function() {
    this.injector.createInstance(View);
  },
  start: function() {
    // listen to an event
    this.dispatcher.addEventListener('some-event', function(event) {
      console.log('event received:', event.type);
    });
    // dispatch an event
    this.dispatcher.dispatch('some-event');
  }
});

var app = new MyApplication();

try it yourself

Event handlers scope and binding

As with events dispatched from the DOM, the scope of the event handlers (this) might not be the expected scope. To solve this common javascript problem, a "bind" method can be used.

Function.prototype.bind is part of ECMAScript 5 and soma.js has implemented a shim for older browsers. Click here for more information about the bind method.

In the following example, one event is dispatched and two functions are listening to it. In the first function, the "this" is the window. In the second function the "this" is the application instance as the bind function changes the scope of the event handler, making the instance variable accessible.

// instance variable
this.myVariable = "myValue";

// listen to an event without binding
this.dispatcher.addEventListener('some-event', function(event) {
  // "this" is window
  console.log('instance variable not accessible:', this.myVariable);
});

// listen to an event with binding
this.dispatcher.addEventListener('some-event', function(event) {
  // "this" is the application instance
  console.log('instance variable accessible:', this.myVariable);
}.bind(this));

// dispatch an event
this.dispatcher.dispatch('some-event');

try it yourself

A quick note about removing listeners. The bind method creates a new function, removing a listener using the bind method will not work as the event handler will be a new function (see some-event documentation).

Here is the solution to properly remove listeners using the bind method.

// event handler
var eventHandler = function(event) {
  // will not be triggered as the listener is properly removed
}
// bound event handler reference
var boundEventHandler = eventHandler.bind(this);
// listen to an event
this.dispatcher.addEventListener('some-event', boundEventHandler);
// stop listening to an event
this.dispatcher.removeEventListener('some-event', boundEventHandler);
// dispatch an event
this.dispatcher.dispatch('some-event');

try it yourself

Send parameters

Parameters can be sent when dispatching events, so the events handlers can receive information. The parameters are store on the event.params property of an event.

dispatcher.dispatch('some-event', {data:'some data'});
dispatcher.dispatchEvent(new soma.Event('some-event', {data:'some data'}));

try it yourself

Command

A command is behavioral design pattern to help reducing dependencies. An object is used to encapsulate the information needed to call a method, including the object owning the method, the object itself and any parameters.

Concretely, a command is a function instantiated by the framework, created for any re-usable action that could be performed in various places of the application. A command is meant to be created, used and discarded straight away. A command should never be used to hold data, and should be executable at any time without causing errors in the system.

Create commands

A command in soma.js is nothing more than a simple function that contains an execute method that will receive the event instance used to trigger it. Dependency injection can be used to retrieve useful objects.

var Command = function(myModel, myView) {
  this.execute = function(event) {
    // do something
  }
};

Manage commands

Add a command

Commands in soma.js are triggered using events. This makes possible for the application actors to, not only execute commands, but also listen to events used to trigger the commands.

The commands core element is used to managed the commands. It is a good practice, unless you are building self-contained modules, to add all the commands in the same place, such as the application instance. This would give a good overview for a potential new developer to get an idea of what is possible to do in the application using the commands.

this.commands.add("some-event", Command);

Remove a command

The commands core element contains several useful methods to manage the commands, such as has, get and getAll. To remove a command, the remove method can be used.

this.commands.remove("some-event");

A simple command example

The following example contains a Navigation model, its role is to display a screen using an ID. A Command is created to call the navigation method. The command is mapped to a "show-screen" event, which is used to send the ID of a screen. This setup makes possible, for any actor of the application, to dispatch an event to show a screen.

var Navigation = function() {};
Navigation.prototype.showScreen = function(id) {
  console.log('Display screen:', id);
}

var Command = function(navigation) {
  this.execute = function(event) {
    console.log("Command executed with parameter:", event.params);
    navigation.showScreen(event.params);
  }
};

var MyApplication = soma.Application.extend({
  init: function() {
    this.injector.mapClass("navigation", Navigation, true);
    this.commands.add("show-screen", Command);
  },
  start: function() {
    this.dispatcher.dispatch("show-screen", "home");
  }
});

var app = new MyApplication();

try it yourself

Flow control

Being able to control the flow of an application is important. Some components might decide to execute a command, but some other components might have the need to monitor or even interrupt the application flow. A good example could be a "logout" command in an editor application. A menu component can decide to execute this command, but the editor should be able to intercept and interrupt it in case the user didn't save his current file.

As the commands are using events to be triggered, the DOM Event provides the necessary tools to monitor (addEventListener) and interrupt a command (event.preventDefault).

In the following example, a command is executed and an external Monitor entity is able to monitor this command by listening to the event.

var Monitor = function(dispatcher) {
  dispatcher.addEventListener('some-event', function(event) {
    console.log("Command monitored");
  });
};

var Command = function() {
  this.execute = function(event) {
    console.log("Command executed");
  }
};

var MyApplication = soma.Application.extend({
  init: function() {
    this.commands.add("some-event", Command);
    this.injector.createInstance(Monitor);
  },
  start: function() {
    this.dispatcher.dispatch("some-event");
  }
});

var app = new MyApplication();

try it yourself

In the following example, the start method dispatch an event to logout a user. The EditorModel is able to monitor the logout command and interrupt it.

The initiator of the command can decide when dispatching the event, if another component should be able to interrupt it. A DOM Event has a cancelable property that should be set to true when dispatched so another component can interrupt it using the event.preventDefaut() method.

var EditorModel = function(dispatcher) {
  dispatcher.addEventListener('logout', function(event) {
    console.log("Logout event received in EditorModel");
    if (!this.fileSaved) {
      console.log('Interupt logout command');
      event.preventDefault();
    }
  });
};

var LogoutCommand = function() {
  this.execute = function(event) {
    console.log("Logout user");
  }
};

var EditorApplication = soma.Application.extend({
  init: function() {
    this.commands.add("logout", LogoutCommand);
    this.injector.createInstance(EditorModel);
  },
  start: function() {
    // dispatch a cancelable event
    // dispatch(eventType, parameters, bubbles, cancelable)
    this.dispatcher.dispatch("logout", null, false, true);
  }
});

var app = new EditorApplication();

try it yourself

Mediator

A mediator is a behavioral design pattern that is used to reduce dependencies. Concretely, a mediator is an object that represents another object, the communication between objects can be encapsulated within the mediators rather than the objects it represents, reducing coupling.

Create mediators

The core element mediators can be used to create mediators. It contains a single create method as the mediators and objects represented are not stored by the framework.

The create method requires two parameters. The first parameter is a function instantiated by the framework, the mediator itself. The second parameter is either a single object or a list of objects (array, node list, and so on).

Mediators are also useful to represent several targets with the same function (DRY). The create method will instantiate as many mediators needed and return them.

A target parameter will be injected to get a reference of the object represented.

function MyMediator = function(target) {
  // target is the object represented
};
// create mediator with a single object
var mediators = mediators.create(MyMediator, object);
// create mediator with a list of objects
var mediators = mediators.create(MyMediator, [object1, object2, object3]);

Simple mediator example

In the following example, a mediator that represents a DOM Element is created, paradoxically called "view" on purpose (see next example).

The mediator updates its target, a DOM Element, when it receives a specific event.

var TitleView = function(target, dispatcher) {
  // listen to the event "render-title" to update the target (DOM Element)
  dispatcher.addEventListener('render title', function(event) {
    target.innerHTML = event.params;
  });
};

var Application = soma.Application.extend({
  init: function() {
    // create a mediator that represents a DOM Element
    this.mediators.create(TitleView, document.getElementById('title'));
  },
  start: function() {
    // dispatch an event to trigger an action in the mediator
    this.dispatcher.dispatch('render title', "This is a title");
  },
});

try it yourself

Decoupling communication

The previous example can be taken even further to create a highly re-usable object. The goal is to create a view that is only a DOM Element wrapper and stripped off any framework and communication code. The view can provide a set of methods to update its content, and the communication with the framework can be moved to another mediator.

Responsibilities will be completely separated as the view will only take care of displaying content, and the mediator will be responsible of providing the view the right content, at the right moment. The result are components that are highly decoupled and more maintainable.

The following example also show how to use a single view function and a single mediator function, to handle multiple DOM Elements.

<h1 data-id='first'></h1>
<h1 data-id='second'></h1>
<h1 data-id='third'></h1>

// model containing title strings
var TitleModel = function() {
  this.data = {
    first: 'First title',
    second: 'Second title',
    third: 'Third title'
  };
};
TitleModel.prototype.getTitle = function(id) {
  return this.data[id];
};

// view representing a DOM element
var TitleView = function(target) {
  this.element = target;
};
TitleView.prototype.setTitle = function(value) {
  this.element.innerHTML = value;
};

// mediator representing a view
var TitleMediator = function(target, dispatcher, model) {
  dispatcher.addEventListener('render title', function(event) {
    var id = target.element.getAttribute('data-id');
    var title = model.getTitle(id);
    target.setTitle(title);
  });
};

var Application = soma.Application.extend({
  init: function() {
    // map model to inject in mediators
    this.injector.mapClass('model', TitleModel, true);
    // create views (mediators representing a DOM element)
    var views = this.mediators.create(TitleView, document.querySelectorAll('h1'));
    // create mediators (mediators representing a view)
    var mediators = this.mediators.create(TitleMediator, views);
  },
  start: function() {
    // dispatch an event to render the titles
    this.dispatcher.dispatch('render title', "This is a title");
  }
});

var app = new Application();

try it yourself

Template

soma-template is an optional DOM template engine library implemented in soma.js, click here to see the soma-template documentation and tests.

Load the soma-template plugin

Simply load the template engine just after loading the soma.js framework. No further actions are needed to use the soma-template library.

<script type="text/javascript" src="soma.js"></script>
<script type="text/javascript" src="soma-template.js"></script>

Load the template plugin with commonJS.

var soma = require('soma.js');
var template = require('soma-template');
soma.plugins.add(template.Plugin);

Create a template

The framework provides a method createTemplate that will instantiate a mediator and receive a template, scope and element parameters so that the DOM can be manipulated in an easy way.

var Template = function(template, scope, element) {

};

this.createTemplate(Template, domElement);

Simple template example

In the following example a template is created using a template function and a reference to a DOM Element. The template will ask the injector a model that contains the data to be displayed.

<div class="app">
  <h1>{{content.title}}</h1>
  <div data-repeat="item in content.items">{{$index}} - {{item}}</div>
</div>

// function model containing data (text to display)
var Model = function() {
  this.data = {
    title: "This is a title",
    items: [
      "First item",
      "Second item",
      "Third item"
    ]
  }
};

// function template that will update the DOM
var Template = function(template, scope, element, model) {
  scope.content = model.data;
  template.render();
};

// application function
var Application = soma.Application.extend({
  init: function() {
    // model mapping rule so it can be injected in the template
    this.injector.mapClass("model", Model, true);
    // create a template for a specific DOM element
    this.createTemplate(Template, document.querySelector('.app'));
  }
});

// create application
var app = new Application();

try it yourself

OOP

Javascript is an object oriented language, but do not have built-in classes, private members, inheritance, interfaces and other common features found in other languages.

There are a lot of debates whether you should emulate classes, but the fact is, some inheritance capabilities are sometimes needed not to duplicate code (DRY principle).

Inheritance is very useful in many cases but must not be overused, composition over inheritance is usually a better design in the long term. The solution is using the right pattern for the right problem.

soma.js provides some useful utility functions to emulate inheritance.

Inheritance

In the following example, a function Person is created, along with a say method. A second function Man is created and inherits from its parent.

The Man constructor calls its parent constructor sending a parameter (name) so the assignment is not broken. The Man.prototype.say overrides its parent say method so it can display a more specific message.

// create "super class"
var Person = function(name) {
  this.name = name;
};
Person.prototype.say = function() {
  console.log("I'm a Person, my name is:", this.name);
}

// create "child class" that will inherit from its parent
var Man = function(name) {
  Person.call(this, name); // call super constructor
}
Man.prototype.say = function() {
  // Person.prototype.say.call(this); // call super say method
  console.log("I'm a Man, my name is:", this.name);
}

// apply inheritance
soma.inherit(Person, Man.prototype);

// create Person
var person = new Person("Doe");
person.say();

// create Man
var john = new Man("John Doe");
john.say();

try it yourself

Here is a different syntax using an object to create the Man function. The result is exactly the same as the previous example.

// create "child class" that will inherit from its parent
// apply inheritance using an object that will become the prototype
var Man = soma.inherit(Person, {
  constructor: function(name) {
    Person.call(this, name); // call super constructor
  },
  say: function() {
    // Person.prototype.say.call(this); // call super say method
  console.log("I'm a Man, my name is:", this.name);
  }
});

try it yourself

Extend method shortcut

Here is small snippet to attach a method and easily apply inheritance on a function.

// create shortcut extend method
Person.extend = function (obj) {
  return soma.inherit(Person, obj);
};
// create "child class" using the extend shortcut
var Man = Person.extend({

});

try it yourself

Call super constructor and methods

The way used to call a parent constructor and a method up in the prototypal chain in the previous example is the native javascript way of doing it.

// call parent constructor
ParentFunction.call(this, param);
// call parent prototype method
ParentFunction.prototype.myMethod.call(this, param);

soma.js offers a slightly different way of writing calls to the parents. Note that this syntax is not pure javascript, this is a soma.js enhancement, use it only if the application benefit of not writing the parent function name explicitly.

// call parent constructor
CurrentFunction.parent.constructor.call(this, param);
// call parent prototype method
CurrentFunction.parent.myMethod.call(this, param);

try it yourself

Plugins

soma.js provide an interface to create plugins to add new features to the framework.

The soma-template plugin is a self-contained library with no dependency, but can also be added to soma.js as a plugin. soma-template is adding DOM manipulation capabilities and a custom method to create template directly in the framework (instance.createTemplate).

A plugin is a function that can receive framework core elements and custom parameters. The plugin can be instantiated using the method createPlugin, the first parameter is the plugin function to be instantiated and the others parameters will be sent the plugin constructor.

Create a plugin

Plugins can be either dependent or independent to the framework, it is only a matter of choice. The following example show a simple structure to create a plugin.

<script>
// plugin
var Plugin = function(instance, dispatcher, injector, customParameter) {
  // framework core elements can be injected as well custom parameters
  console.log('Plugin created with', customParameter);
};

var Application = soma.Application.extend({
  init: function() {
    // create a plugin
    var plugin = this.createPlugin(Plugin, 'custom parameter');
  }
});

var app = new Application();
</script>

try it yourself

Mouse plugin

The following example creates a MousePlugin and an injection mapping rule so it can be used anywhere in the application. The MousePlugin provide a getPosition method to retrieve the mouse coordinates, as well as a dispose function to "destroy" the plugin.

Providing a dispose method is a good practice to properly destroy an object. Listeners can be removed, internal objects can be destroyed, so the object can be garbage collected.

// plugin to retrieve the mouse coordinates
var MousePlugin = function() {
  // object holding mouse position
  var position = {
    x: 0,
    y: 0
  };
  // event handler storing the mouse position
  var handler = function(event) {
    position.x = event.clientX;
    position.y = event.clientY;
  }
  //add event
  window.addEventListener('mousemove', handler);
  // returns a getPosition method to retrieve the mouse position
  // returns a dispose method to remove the listener
  return {
    getPosition: function() {
      return position;
    },
    dispose: function() {
      window.removeEventListener('mousemove', handler);
    }
  }
};

// create the plugin
var mouse = this.createPlugin(MousePlugin);
// create a mapping rule
this.injector.mapValue('mouse', mouse);

try it yourself

Ready plugin (auto-registration)

The following plugin (ReadyPlugin) alters the prototype of the soma.js application and add a ready method to it. The ready method is used to add callbacks for the application to know when the DOM has been fully loaded.

An event could have been dispatched but for the sake of the demonstration. the application prototype has been altered to show a direct use of a plugin. The ready function is used both in the application instance and in the view.

The plugin is also not created by the user application instance. The plugin is "auto-registered" and will be made available by the framework automatically. This makes the creation of a plugin possible without further actions, by executing its code, or by loading a javascript file containing the plugin.

// plugin to add callbacks when the dom is ready
var ReadyPlugin = function(instance) {
  // ready function to add callbacks when the DOM is loaded (https://github.com/ded/domready)
  var ready=function(){function l(b){for(k=1;b=a.shift();)b()}var b,a=[],c=!1,d=document,e=d.documentElement,f=e.doScroll,g="DOMContentLoaded",h="addEventListener",i="onreadystatechange",j="readyState",k=/^loade|c/.test(d[j]);return d[h]&&d[h](g,b=function(){d.removeEventListener(g,b,c),l()},c),f&&d.attachEvent(i,b=function(){/^c/.test(d[j])&&(d.detachEvent(i,b),l())}),f?function(b){self!=top?k?b():a.push(b):function(){try{e.doScroll("left")}catch(a){return setTimeout(function(){ready(b)},50)}b()}()}:function(b){k?b():a.push(b)}}();
  // add the ready function to the prototype of the soma.js application for a direct use
  instance.constructor.prototype.ready = ready;
};

// auto-registration, the plugin will be instantiated automatically by soma.js
if (soma.plugins && soma.plugins.add) {
  soma.plugins.add(ReadyPlugin);
}

// view
var View = function(target, instance) {
  instance.ready(function() {
    target.innerHTML = 'DOM is loaded';
  });
};

var Application = soma.Application.extend({
  init: function() {
    // use the plugin
    this.ready(function() {
      console.log('DOM READY');
    });
  },
  start: function() {
    // create a view
    this.mediators.create(View, document.querySelector('.report'));
  }
});

var app = new Application();

try it yourself

Orientation plugin

The role of the following plugin (OrientationPlugin), is to detect an orientation change on a mobile device, either portait or landscape. The orientation can be retrieve at any moment from the plugin, but the plugin will also dispatch an event when the device orientation changes. This makes possible for another entities to simply listen to this events to update its state.

// plugin to retrieve the mouse coordinates
var OrientationPlugin = function(dispatcher) {
  // hold current orientation
  var orientation = detectDeviceOrientation();
  // add listener to detect orientation change
  window.addEventListener('orientationchange', handler);
  // store the new orientation and dispatch an event
  function handler(event) {
    orientation = detectDeviceOrientation();
    dispatcher.dispatch('orientation', orientation);
  }
  // return the orientation, portait or landscape
  function detectDeviceOrientation(){
    switch(window.orientation) {
      case 90:
      case -90:
        return 'landscape';
      break;
      case 0:
      case 180:
      default:
        return 'portait';
    }
  }
  // return plugin API
  // getOrientation returns either landscape or portait
  // dispose removes the listener
  return {
    getOrientation: function() {
      return orientation;
    },
    dispose: function() {
      window.removeEventListener('orientationchange', handler);
    }
  }
};

// view
var View = function(target, dispatcher, orientation) {
  // display the orientation when the view is created
  updateOrientation(orientation.getOrientation());
  // listen to the event dispatched by the plugin
  dispatcher.addEventListener('orientation', function(event) {
    // display the orientation when a change happened
    updateOrientation(event.params);
  })
  // display the orientation in the DOM Element
  function updateOrientation(value) {
    target.innerHTML = 'Current orientation: ' + value;
  }
};

var Application = soma.Application.extend({
  init: function() {
    // create the plugin
    var plugin = this.createPlugin(OrientationPlugin);
    // create a mapping rule
    this.injector.mapValue('orientation', plugin);
  },
  start: function() {
    // create a view
    this.mediators.create(View, document.querySelector('.report'));
  }
});

var app = new Application();

Test on a mobile device

Try it yourself

Router

soma.js does not provide a built-in router to take care of path and url. However, Director is a great library that is focused on routing.

The library is very featured, fully tested, does not have any dependency and can be seamlessly integrated in the framework. Director also support the HTML 5 history API.

Get Director

Click here to see the todo app that integrates Director.

Here is a simple example to implement Director in a soma.js application.

var Navigation = function(router, dispatcher) {

  // setup routes and dispatch views ids

  router.init('/home');

  router.on('/home', function() {
    dispatchRoute('home');
  });

  router.on('/page1', function() {
    dispatchRoute('page1');
  });

  router.on('/page2', function() {
    dispatchRoute('page2');
  });

  // in this demo, all routes could have been handled with this single regex route
  // router.on(/.*/, function() {
  //   dispatchRoute(router.getRoute()[0]);
  // });

  function dispatchRoute(id) {
    dispatcher.dispatch('show-view', id);
  }

};

var View = function(target, dispatcher) {
  dispatcher.addEventListener('show-view', function(event) {
    var isCurrentView = target.className.indexOf(event.params) !== -1;
    target.style.display = isCurrentView ? 'block' : 'none';
  });
}

var Application = soma.Application.extend({
  init: function() {
    // create the Director router and make it available through the framework
    this.injector.mapValue('router', new Router());
    // create mediators for the views (DOM Element)
    this.mediators.create(View, document.querySelectorAll('.view'))
  },
  start: function() {
    // instantiate Navigation to start the app
    this.injector.createInstance(Navigation);
  }
});

var app = new Application();

Try it yourself

Demos

Tools

soma.js is very lightweight and some tools might be needed to performed some actions, such as handling routes in a one-page application, making DOM manipulations or making ajax requests.

Developers are free to use the libraries they like but here are some recommended libraries that are very efficient and well-tested.

Router

Ajax requests

DOM manipulation

DOM selector

Tests

soma.js, as well its internal libraries (infuse.js, soma-events and soma-template), are all fully tested. There is no such a thing as a bug-free library, if something does not work as intended, Github is the best place to report it.

Browser support

soma.js and its internal libraries (infuse.js, soma-events and soma-template) support all browsers and should work anywhere, with the exception of Internet Explorer 6.