Scalable javascript framework
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.
soma.js minified and gzip is 4.5 KB.
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>
$ npm install soma.js
var soma = require("soma.js");
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>
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");
The framework core elements to help you build your own structure are the following:
injector
property)dispatcher
property)commands
property)mediators
property)instance
property)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.
infuse.js is the dependency injection library developed for soma.js, click here to see the infuse.js documentation and tests.
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:
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.
In the following example, two plain javascript functions will be instantiated and injected.
Config
function has a debugMode
constructor parameter that happened to be an injection name (mapping rule associated), the corresponding boolean value will be injected into it.Model
function contains two constructor parameters that also are injection names. A config instance and a boolean value will be injected into the corresponding parameters.mapClass
method used to map functions and one using the mapValue
method used to map objects that are not meant to be instantiated.// 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);
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);
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);
soma-events is the observer library developed for soma.js, click here to see the soma-events documentation and tests.
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.
To dispatch an event, the first step is to create it. There are four ways to create events:
dispatch
method shortcutCreating 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"});
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"}));
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"}));
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);
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.
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();
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');
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');
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'}));
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.
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
}
};
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);
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");
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();
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();
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();
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.
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]);
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");
},
});
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();
soma-template is an optional DOM template engine library implemented in soma.js, click here to see the soma-template documentation and tests.
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);
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);
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();
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.
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();
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);
}
});
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({
});
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);
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.
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>
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);
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();
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();
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.
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();
Demo displaying a string. The application contains a model, a mediator and uses an event to display the string in a DOM Element.
Demo searching in the Twitter API and displaying a list of tweets. The search is performed using a command and a service to get data from a remote location. The result is displayed in the DOM with the soma-template plugin.
Demo managing a list to todo items that can be added, removed, set as completed, and stored in the local storage. The application contains a model holding the data, a view using soma-template and a command to handle the different type of user events.
Same todo app demo with a much lighter code. There's no command and no model API. The view updates the set of data directly using the soma-template function calls capability.
Another version of the todo app light, with routes to filter the completed and active todo items. Director.js has been used as a router.
A complex web app that manage a list of snippets that are saved both in the local storage and on a remote server. The application can sign to the Github API (oauth 2.0) and is composed of a config, commands, models, a service, several views and some utils methods.
Snake game drawn into a canvas. It contains a canvas mediator, several models, layers, and game entities.
Demo displaying three interchangeable views (digital, analog and polar clocks), using the same model and mediators.
TypeScript version of the javascript clock demo. The model and the views are abstracted using interfaces.
CoffeeScript version of the javascript clock demo.
Demo using a custom mobile plugin that dispatches events on a device orientation change (portrait and landscape).
Demo using the recommended router library: Director.js.
Demo creating a server (express), commands, templates and models to get started with soma.js on node.js.
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.
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.
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.