Scalable javascript framework
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.
Soma requires two other libraries:
npm install @soundstep/infuse
npm install signals
npm install @soundstep/soma
const soma = require('@soundstep/soma');
soma minified and gzip is 2.2 KB.
Add the script to your page:
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.
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();
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');
The framework core elements to help you build your own structure are the following:
injector
property)emitter
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.
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.
infuse is the dependency injection library developed for soma, click here to see the infuse 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, 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. 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
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();
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.
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();
signals is the pub-sub library developed for soma.
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 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();
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.
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();
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();
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();
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);
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]);
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 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 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.
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();
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();
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();
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(object, MyMediator);
// create mediator with a list of objects
var mediators = mediators.create([object1, object2, object3], MyMediator);
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();
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();
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.
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();
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();
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);
}
});
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({
});
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 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.
A function to instantiate.
modules.create(function);
An object containing a module property referencing a function to instantiate.
modules.create({
module: function
});
An EcmaScript Module (esm) containing a Module class to instantiate
export class Module() {
}
modules.create({
Module: function
});
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();
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();
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();
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();
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();
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.
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();
Example displaying a string. The application contains a model, a mediator and uses an event to display the string in a DOM Element.
Example 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.
Example 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.
Snake game drawn into a canvas. It contains a canvas mediator, several models, layers, and game entities.
Example 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.
Example of a module that dispatches events on a device orientation change (portrait and landscape).
Example using the recommended router library: Director.js.
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.