soma

Scalable javascript framework

What is soma?

soma 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 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 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 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.

Requirements

Soma requires two other libraries:

npm install @soundstep/infuse
npm install signals

Download

Install using NPM

npm install @soundstep/soma
const soma = require('@soundstep/soma');

Browser

soma minified and gzip is 2.2 KB.

download soma

Add the script to your page:

Quick start

Here is an example of what a soma 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.

try it yourself

The same application with functions, if you prefer this syntax.

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

// command to update the DOM
const UpdateDOM = function(model) {
	this.execute = () => {
		document.querySelector('.app').innerHTML = model.data;
		console.log('DOM updated!');
	};
}

// application
const 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.commands.add('update-dom', UpdateDOM);
	},
	start: function() {
		this.emitter.dispatch('update-dom');
	}
});

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

try it yourself

Application instance

The first step to build a soma 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.

class MyApplication extends soma.Application {
	init() {
		console.log('init');
	}
}

const app = new MyApplication();
const MyApplication = soma.Application.extend({
	init: function() {
		console.log('init');
	}
});

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

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

const app = new MyApplication();

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.

class MyApplication extends soma.Application {
	constructor(param) {
		this.param = param;
		super.call(this);
	},
	init: function() {
		console.log('init with param:', this.param);
	}
});

const app = new MyApplication('my application parameter');
const MyApplication = soma.Application.extend({
	constructor: function(param) {
		this.param = param;
		soma.Application.call(this);
	},
	init: function() {
		console.log('init with param:', this.param);
	}
});

const 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.

class MyApplication extends soma.Application {
	init() {
		this.injector; // dependency injection pattern
		this.emitter; // 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.

const Entity = function(injector, emitter, commands, mediators, instance) {

};
const Entity = function() {
	this.injector = null;
	this.emitter = 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 is the dependency injection library developed for soma, click here to see the infuse 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, 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

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. 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
class Config {
	constructor(debugMode) {
		console.log('debugMode injected in Config:', debugMode);
	}
}

// simple function in which a config instance and a boolean is injected
class Model {
	constructor(debugMode, config) {
		console.log('debugMode injected in Model:', debugMode);
		console.log('config injected in Model:', config);
	}
}

// function model containing data (text to display)
class MyApplication extends soma.Application {
	init() {
		// mapping rule: map the string 'config' to the function Config
		this.injector.mapClass('config', Config);
		// mapping rule: map the string 'debugMode' to a boolean
		this.injector.mapValue('debugMode', true);
		// instantiate the model using the injector
		this.start();
	}
	start() {
		const model = this.injector.createInstance(Model);
	}
};

var app = new MyApplication();

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.

class Config {
	constructor() {
		console.log('Config instantiated');
	}
}

class Model {
	constructor(config) {
		console.log('Model instantiated with a config injected:', config);
	}
}

class Command {
	constructor(model) {
		this.model = model;
	}
	execute() {
		console.log('Command executed with a model injected:', this.model);
	}
}

class MyApplication extends soma.Application {
	init() {
		// 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);
		// start
		this.start();
	}
	start() {
		// send a command
		this.emitter.dispatch('some-event');
	}
}

const app = new MyApplication();

try it yourself

Communication

signals is the pub-sub library developed for soma.

Event system (signals)

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 is the "emitter", 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.

Soma is using an underlying library for its event system called signals, which is a "pub-sub" library (publish-subscribe). Signals have been created for specific reasons, and provide a powerful alternative to issues that can occur with standard DOM 3 Event, more information there.

Signals are very powerful, here are a few thing you can do:

Find out more in the Signals examples

Creating and dispatching events

Create and dispatch an event using the dispatcher

Creating an event and dispatching it with the dispatcher is very simple.

class MyApplication extends soma.Application {
	init() {
		this.start();
	}
	start() {
		// listen to an event
		this.emitter.addListener('some-event', this.handler);
		// dispatch an event
		this.emitter.dispatch('some-event', ['my parameter']);
		// cleanup
		this.emitter.removeListener('some-event', this.handler);
	}
	handler(param) {
		console.log('event received with:', param);
	}
}

const app = new MyApplication();

try it yourself

Listening to events

The "emitter" implements the same interface as the signals, the methods "addListener" and "removeListener" can be used to listen and stop listening to events.

The signals used by the "emitter" can be retrieved using the "getSignal" method.

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.

cclass View {
	constructor(emitter) {
		const handler = () => console.log('event received in a view');
		emitter.addListener('some-event', handler);
	}
}

class MyApplication extends soma.Application {
	init() {
		this.injector.createInstance(View);
		this.start();
	}
	start() {
		// listen to an event
		this.emitter.addListener('some-event', this.handler);
		// dispatch an event
		this.emitter.dispatch('some-event');
	}
	handler() {
		console.log('event received in the app');
	}
}

const app = new MyApplication();

try it yourself

Event handlers scope and binding

As with events dispatched from the DOM and handler callback function in general, the scope of the functions (this) might not be the expected scope. To solve this common javascript problem, the "scope" of the signals can be used (preferred way), or the javascript "bind" method.

Function.prototype.bind is part of ECMAScript 5 and soma 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.

class MyApplication extends soma.Application {
	init() {
		// instance variable
		this.myVariable = "myValue";
		// listen to an event without binding
		this.emitter.addListener('some-event', function() {
			try {
				// "this" is the window, or global with nodejs, or undefined with classes
				console.log('instance variable not accessible:', this.myVariable);
			} catch(err) {
				console.log(err.message);
			}
		});
		// listen to an event using the signals scope
		this.emitter.addListener('some-event', function() {
			// "this" is the application instance
			console.log('instance variable accessible:', this.myVariable);
		}, this);
		// listen to an event using bind method
		this.emitter.addListener('some-event', function() {
			// "this" is the application instance
			console.log('instance variable accessible:', this.myVariable);
		}.bind(this));
		// start
		this.start();
	}
	start() {
		this.emitter.dispatch('some-event');
	}
}

const app = new MyApplication();

try it yourself

Removing listeners

Listeners can be removed using the emitter, the signal instance of an event, or the binding of a listener.

class MyApplication extends soma.Application {
	init() {

		// retrieve a binding
		const binding = this.emitter.addListener('some-event', this.handler, this);

		// retrieve a signal
		const signal = this.emitter.getSignal('some-event');

		// dispatch the first event
		this.emitter.dispatch('some-event');

		// Remove the listener using the emitter
		// the same scope is needed to properly remove the listener
		this.emitter.removeListener('some-event', this.handler, this);

		// remove the listener using the binding
		binding.detach();

		// remove the listener using the signal
		// the same scope is needed to properly remove the listener
		signal.remove(this.handler, this);

		// dispatch another event
		this.emitter.dispatch('some-event'); // will not be received
	}
	handler() {
		console.log('received event');
	}
}

const app = new MyApplication();

try it yourself

An important note about removing listeners.

The bind method creates a new function, and so is the scope of a signal, removing a listener using the bind method will not work as the event handler will be a new function.

The preferred way is to use the "scope" (called context for a signal) instead of using the javascript "bind" method, the third parameter of the "addListener" and "removeListener" method.

emitter.addListener('event', handler, this);
emitter.removeListener('event', handler, this);

If you still wish to use the javascript "bind" method, a "bound" function should be used to properly remove the listener.

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

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.

emitter.dispatch('some-event', ['param1', {data:'another'}, true, 12]);

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

class Navigation {
	showScreen(screenId) {
		console.log('Display screen:', screenId);
	}
}

class Command {
	constructor(navigation) {
		this.navigation = navigation;
	}
	execute(screenId) {
		console.log('Command executed with parameter:', screenId);
		this.navigation.showScreen(screenId);
	}
}

class MyApplication extends soma.Application {
	init() {
		this.injector.mapClass('navigation', Navigation, true);
		this.commands.add('show-screen', Command);
		this.start();
	}
	start() {
		this.emitter.dispatch('show-screen', ['home']);
	}
}

const 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 (addListener) 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.

class Monitor {
	constructor(emitter) {
		emitter.addListener('some-event', () => {
			console.log('Command monitored');
		});
	}
}

class Command {
	execute() {
		console.log('Command executed');
	}
}

class MyApplication extends soma.Application {
	init() {
		this.injector.createInstance(Monitor);
		this.commands.add('some-event', Command);
		this.start();
	}
	start() {
		this.emitter.dispatch('some-event');
	}
}

const 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.

class EditorModel {
	constructor(emitter) {
		const fileSaved = false;
		emitter.addListener('logout', function() {
			const signal = emitter.getSignal('logout');
			console.log('Logout event received in EditorModel');
			if (!fileSaved) {
				console.log('Interupt logout command');
				signal.halt();
			}
		});
	}
}

class LogoutCommand {
	execute() {
		console.log('Logout user');
	}
}

class EditorApplication extends soma.Application {
	init() {
		this.injector.createInstance(EditorModel);
		this.commands.add('logout', LogoutCommand);
		this.start();
	}
	start() {
		this.emitter.dispatch('logout');
	}
}

const 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(object, MyMediator);
// create mediator with a list of objects
var mediators = mediators.create([object1, object2, object3], MyMediator);

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.

class TitleView {
	constructor(target, emitter) {
		// listen to the event 'render-title' to update the target (DOM Element)
		emitter.addListener('render-title', function(title) {
			target.innerHTML = title;
			console.log('Title rendered');
		});
	}
}

class Application extends soma.Application {
	init() {
		// create a mediator that represents a DOM Element
		this.mediators.create(document.getElementById('title'), TitleView);
		this.start();
	}
	start() {
		// dispatch an event to trigger an action in the mediator
		this.emitter.dispatch('render-title', ['This is a title']);
	}
}

const app = new Application();

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.

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

// view representing a DOM element
class TitleView {
	constructor(target) {
		this.element = target;
	}
	setTitle(value) {
		this.element.innerHTML = value;
	}
}

// mediator representing a view
class TitleMediator {
	constructor(target, emitter, model) {
		emitter.addListener('render-title', function() {
			const id = target.element.getAttribute('data-id');
			const title = model.getTitle(id);
			target.setTitle(title);
		});
	}
}

class Application extends soma.Application {
	init() {
		// map model to inject in mediators
		this.injector.mapClass('model', TitleModel, true);
		// convert a NodeList to an array of DOM elements
		const list = Array.from(document.querySelectorAll('h2'));
		// create views (mediators representing DOM elements)
		const views = this.mediators.create(list, TitleView);
		// create mediators (mediators representing a view)
		const mediators = this.mediators.create(views, TitleMediator);
		this.start();
	}
	start() {
		// dispatch an event to render the titles
		this.emitter.dispatch('render-title');
	}
}

const app = new Application();

try it yourself

OOP

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

At the time of writing, javascript has now classes and soma works just with classes.

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.

For version of javascript that does not support classes, soma provides some useful utility functions to emulate inheritance.

Inheritance with classes

In the following example, a class Person is created, along with a say method. A second class 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.say method overrides its parent say method so it can display a more specific message.

class Person {
	constructor(name) {
		this.name = name;
	}
	say() {
		console.log("I'm a Person, my name is:", this.name);
	}
}

class Man extends Person {
	constructor(name) {
		super(name);
	}
	say() {
		console.log("I'm a Man, my name is:", this.name);
	}
}

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

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

try it yourself

Inheritance without classes

Here is the same example but with functions and prototypes.

// 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.utils.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
const Man = soma.utils.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.utils.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 offers a slightly different way of writing calls to the parents. Note that this syntax is not pure javascript, this is a soma 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);

Modules

Modules are meant to be separate entities, following a specific signature, so they can be added effortlessly in a soma application. The can be seen as "plugins".

The modules can be developed using the soma capabilities (injection, mediators, and so on), or be completely independent of soma (vanilla javascript).

A module has two requirements:

Here are some examples.

The function or class requires an static "id" property.

function MyModule() {}
MyModule.id = 'my-module';
class MyModule{}
MyModule.id = 'my-module';

Soma will try to inject mapping values if it finds any, such as the core elements or custom ones. The core element is accessible using the "modules" property, or injecting the "modules" mapping name.

Note: Module instances are never stored within the framework.

Here is the modules core instance signature:

modules.create(module:function, arguments:array, register:boolean);
modules.has(id:string);
modules.get(id:string);
modules.remove(id:string);
modules.dispose();

A simple module

const Module = function(/* can inject values, such as emitter, mediators and so on */) {
	console.log('Module created');
};
Module.id = 'moduleId';
const module = modules.create(Module);
const container = {
	module: function(/* can inject values, such as emitter, mediators and so on */) {
		console.log('Module created');
	};
};
container.module.id = 'moduleId';
const module = modules.create(container);

As a best practice, a Module should contain an optional dispose function that will be called on removal. The Module can also contain an optional init method that will be called on creation.

function Module() {}
Module.prototype.init = function() {};
Module.prototype.dispose = function() {};

A full example with a class:

class Module {
	static id = 'my-module';
	constructor() {
		console.log('Module created');
	}
	init() {
		console.log('Module initializing...');
	}
	dispose() {
		console.log('Module disposed');
	}
}

class MyApplication extends soma.Application {
	init() {
		// create module
		const module = this.modules.create(Module);
		// get module
		console.log('get:', this.modules.get('my-module'));
		// has module
		console.log('has:', this.modules.has('my-module'));
		// destroy module using an id
		this.modules.remove('my-module');
	}
}

const app = new MyApplication();

try it yourself

A module with injections

class Module {
	static id = 'my module';
	constructor(emitter, instance, someData) {
		// module can receive injections
		console.log('Module created.');
		console.log('Arguments received', arguments);
	}
}

class MyApplication extends soma.Application {
	init() {
		this.injector.mapValue('someData', { data:1 });
		const module = this.modules.create(Module);
	}
}

const app = new MyApplication();

try it yourself

A module with parameters

class Module {
	static id = 'my module';
	constructor(param1, param2) {
		// module can receive injections
		console.log('Module created.');
		console.log('Arguments received', arguments);
	}
}

class MyApplication extends soma.Application {
	init() {
		const module = this.modules.create(Module, ['a param', {data:1}]);
	}
}

const app = new MyApplication();

try it yourself

A module with injections and parameters

class Module {
	static id = 'my module';
	constructor(param1, param2) {
		this.emitter = null;
		this.commands = null;
		// module can receive injections
		console.log('Module created.');
		console.log('Arguments received', arguments);
	}
	init() {
		console.log('Emitter injected', this.emitter);
		console.log('Commands injected', this.commands);
	}
}

class MyApplication extends soma.Application {
	init() {
		const module = this.modules.create(Module, ['a param', {data:1}]);
	}
}

const app = new MyApplication();

try it yourself

Router

soma 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

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

class Navigation {
	constructor(router, emitter) {
		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]);
		// });
		router.init('/home');
		function dispatchRoute(id) {
			console.log('> dispatching route id:', id);
			emitter.dispatch('show-view', [id]);
		}
	}
}

class View {
	constructor(target, emitter) {
		emitter.addListener('show-view', function(params) {
			var isCurrentView = target.className.indexOf(params) !== -1;
			target.style.display = isCurrentView ? 'block' : 'none';
			if (isCurrentView) {
				console.log('  showing the view:', params);
			}
		});
	}
}

class Application extends soma.Application {
	init() {
		// 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([].slice.call(document.querySelectorAll('.view')), View);
		this.start();
	}
	start() {
		// instantiate Navigation to start the app
		this.injector.createInstance(Navigation);
	}
}

var app = new Application();

Try it yourself

Examples

Browser support

soma 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.