domino.js

A JavaScript cascading controller for fast interactive Web interfaces prototyping

This project is maintained by Alexis Jacomy from Linkfluence.

domino.js

Current version: v1.3.3

domino.js is a JavaScript cascading controller for fast interactive Web interfaces prototyping, developped by Alexis Jacomy at Linkfluence. It is released under the MIT License.

How to use it:

To use it, clone the repository:

git clone git@github.com:jacomyal/domino.js.git

The latest minified version is available here:

https://raw.github.com/jacomyal/domino.js/master/build/domino.min.js

You can also minify your own version with Grunt:

Contributing:

You can contribute by submitting issues tickets and proposing pull requests. The changelog is accessible here.


Navigation:


Introduction (↑)

domino.js is a JavaScript library to manage interactions in dashboards. It has been especially designed for iterative processes, to obtain quickly maintainable proofs of concepts.

The concept is pretty simple:

  1. First, you define your properties (that describe your data as well as all the minor counts/flags that define the state of your interface), and associate input and output events to each of them.
  2. Then, you instanciate your modules (that basically define all the graphic components that display or make possible to modify the properties), through domino.js's modules factory, that will take care of all the connecting part.
  3. Finally, when a module will dispatch an event, it will automatically update the related properties, and the modules that are listening to these properties' output events. So you never have to connect two modules by yourself.

But the most important feature of domino.js is probably the possibility to add arbitrarily hacks. A hack is just a function bound to one or more events. This function will be executed in its own scope, and can update properties, call AJAX services, dispatch other events, and a lot more. So basically, it gives a strict and clear place to write all those sh*tty features that were not considered in your original design.

It might be easier with examples. In the following one, we just declare a boolean property, named "flag", and bind two modules on it - one to update it, and one to know when it is updated:

// First, let's instanciate domino.js:
var controller = new domino({
  properties: [
    // We only declare one property, named "flag", that will contain
    // a boolean value:
    {
      id: 'flag',
      dispatch: 'flagUpdated',
      triggers: 'updateFlag'
    }
  ]
});

// Here is the module that will modify the value:
function emettorModule() {
  domino.module.call(this);

  // We add a method to update easily the value:
  this.updateFlag = function(newFlagValue) {
    // The method "dispatchEvent" from "domino.module" will trigger
    // the update in the domino.js instance. So, it will update the
    // value, and then check if anything is bound the any of the
    // output events, and trigger:
    this.dispatchEvent('updateFlag', {
      flag: !!newFlagValue
    });
  }
}

// Here is the module that receive the events when the flag is
// updated.
function receptorModule() {
  domino.module.call(this);

  // We add a trigger on the "flagUpdated" event, that will just
  // display the new value:
  this.triggers.events['flagUpdated'] = function(dominoInstance) {
    console.log('New flag value: '+dominoInstance.get('flag'));
  };
}

// Finally, we have to instanciate our modules:
var emettor = controller.addModule(emettorModule),
    receptor = controller.addModule(receptorModule);

// Now, let's test it:
emettor.updateFlag(true);  // log: "New flag value: true"
emettor.updateFlag(false); // log: "New flag value: false"

Now, the following example is basically the same than the previous one. But instead of a boolean, our property is a string, and we do not want it to exceed 5 characters. So, we add a hack bound on the output event of our property, and that will check the length of our string, and truncate it if it is too long:

// As previously, let's first instanciate domino.js:
var controller = new domino({
  properties: [
    // We only declare one property, named "string", that will
    // contain a string value:
    {
      id: 'string',
      dispatch: 'stringUpdated',
      triggers: 'updateString'
    }
  ],
  hacks: [
    {
      triggers: 'stringUpdated',
      method: function() {
        var str = this.get('string');

        if (str.length > 5) {
          console.log('The string has been truncated!');
          this.string = str.substr(0,5);
        }
      }
    }
  ]
});

// Here is the module that will dispatch
function emettorModule() {
  domino.module.call(this);

  // We add a method to update easily the value:
  this.updateString = function(newStringValue) {
    this.dispatchEvent('updateString', {
      string: newStringValue
    });
  }
}

// Here is the module that receive the events when the string
// is updated.
function receptorModule() {
  domino.module.call(this);

  // We add a trigger on the "stringUpdated" event, that will
  // just display the new value:
  this.triggers.events['stringUpdated'] =
    function(dominoInstance) {
      console.log(
        'New string value: '+dominoInstance.get('string')
      );
    };
}

// Finally, we have to instanciate our modules:
var emettor = controller.addModule(emettorModule),
    receptor = controller.addModule(receptorModule);

// Now, let's test it:
emettor.updateString('abc');
  // log: "New string value: abc"
emettor.updateString('abcdefghi');
  // log: "New string value: abcdefghi"
  //      "The string has been truncated!"
  //      "New string value: abcde"

Properties (↑)

The minimal declaration of a property is just a unique string id. Here is the exhaustive list of all the other parameters you can add to describe your property:

Here is a more complete example on how to declare string:

// [...] inside the properties declaration:
{
  id: 'stringLessThan5Chars',
  label: 'String less than 5 chars',
  triggers: 'updateStringLessThan5Chars',
  // In this example, we associate two output events to the
  // property. It is often useful - for example if you have to
  // reinitialize some data or call a service when one on ten
  // different properties is updated:
  dispatch: ['stringLessThan5CharsUpdate', 'aStringIsUpdated'],
  type: 'string',
  setter: function(val) {
    // First, we check the length of the new value:
    val = val.length>5 ?
      val.substr(0,5) :
      val;

    // If the value has not changed, returning false will cancel
    // the update of this property, ie the output events
    // ('stringLessThan5CharsUpdate' and 'aStringIsUpdated') will
    // not be dispatched.
    if(val === this.get('stringLessThan5Chars'))
      return false;

    this.stringLessThan5Chars = val;
    return true;
  },
  // Here, since the setter will be used to set the initial value,
  // the initial value will be "abcde" and not "abcdefghi":
  value: 'abcdefghi'
}
// [...]

It basically makes the same thing as in the second example, but without the use of a hack.

Modules (↑)

Most of the time, the modules represent each graphic components - buttons to dispatch events, checkboxes to represent boolean properties, etc... Exactly as it is for the properties, designing your modules atomically is one of the best ways to keep your code maintainable.

Any module must extend the domino.module basic class. This class has just the methods to listen to and dispatch events, and empty objects that you will fill with your triggers. You can bind a trigger on an event or directly to a property (it will then be triggered any time the property is effectively updated).

Here is a quick and pratical example using jQuery, of a module corresponding to an HTML checkbox, and representing the boolean property "flag" from the first example:

function Checkbox() {
  domino.module.call(this);

  var self = this,
      html = $('<fieldset>' +
                 '<input type="checkbox" id="flag" />' +
                 '<label for="flag">Flag</label>' +
               '</fieldset>');

  // When the checkbox is clicked, it will update the "flag" in
  // domino, and dispatch output events:
  html.find('input').change(function() {
    var data = {};
    data['flag'] = $(this).is(':checked');

    // Dispatch the event
    self.dispatchEvent('updateFlag', data);
  });

  // When the "flag" is updated, we update the state of the
  // checkbox ("self.triggers.properties['flag']" could have
  // been used as well):
  self.triggers.events['flagUpdated'] = function(dominoInstance) {
    html.find('input').attr(
      'checked',
      !dominoInstance.get('flag') ?
        'checked' :
        null
    );
  };

  this.html = html;
};

Once this module class is declared, if you want to add an instance to a DOM element, you just have to write:

// with "controller" our domino.js instance, and "dom" the DOM parent:
var myCheckbox = controller.addModule(Checkbox);
myCheckbox.html.appendTo(dom);

And that's it, the module is here and connected. And you can even create two instances or more, and there will not be any conflict, and they will all stay synchronized, of course.

Also, it is possible to specify an id for a module. It is not possible to add two modules with the same id, the oldest one will have to be killed before. Also, the instance method .modules() can retrieve modules identified by the specified id.

Here is how to add a module with a specified id:

var myModule = controller.addModule(MyModuleConstructor, null, { id: 'myModuleId' });

// Then, it is possible to get a reference to that module through
// the domino instance like that:
myModule === controller.modules('myModuleId'); // returns true

Hacks (↑)

Hacks are useful to implement all those features that you can not predict in the definition of your projects - they actually are real hacks. Here are some examples of the kind of "features" that can be a disaster for your code, but are easily implementable with domino.js:

Let's consider the following practical case: You have three different flags (properties flag1, flag2 and flag3), and you want to have the three values always stored in an array (property list).

var controller = new domino({
  properties: [
    {
      id: 'flag1',
      triggers: 'updateFlag1',
      dispatch: ['flag1Updated', 'flagUpdated']
    },
    {
      id: 'flag2',
      triggers: 'updateFlag2',
      dispatch: ['flag2Updated', 'flagUpdated']
    },
    {
      id: 'flag3',
      triggers: 'updateFlag3',
      dispatch: ['flag3Updated', 'flagUpdated']
    },
    {
      id: 'list',
      dispatch: 'listUpdated'
    }
  ],
  hacks: [
    {
      triggers: 'flagUpdated',
      method: function() {
        // Here you can refresh the list:
        this.list = [
          this.get('flag1'),
          this.get('flag2'),
          this.get('flag3')
        ];
      }
    }
  ]
});

And that's it: Any time one flag is updated, the list will automatically be refreshed, and the event "listUpdated" dispatched.

The different methods you can call from the hacks are described in the Scopes management section.

Specifications:

Each hack must be an object

Services (↑)

domino.js provides an helper to interact with Web services. Basically, referencing a service will create a shortcut to call in an easy way you Web service.

Here is a basic example:

var controller = new domino({
  properties: [
    {
      id: 'theProperty',
      label: 'The Property',
      value: 42,
      type: 'number',
      triggers: 'updateTheProperty',
      dispatch: 'thePropertyUpdated'
    }
  ],
  services: [
    {
      id: 'getTheProperty',
      setter: 'theProperty',
      url: '/path/to/get/the/property'
    }
  ]
});

Then, executing controller.request('getTheProperty'); will make a GET call to the indicated URL, set the received data as theProperty value, and dispatch a "thePropertyUpdated" event.

Shortcuts:

Also, to help manipulating services, it is possible to use shortcuts to avoid declare explicitely lots of things. Here is an example:

var controller = new domino({
  properties: [
    {
      id: 'prop1',
      label: 'Property 1',
      value: 42,
      type: 'number',
      triggers: 'updateProp1',
      dispatch: 'prop1Updated'
    },
    {
      id: 'prop2',
      label: 'Property 2',
      value: 42,
      type: 'number',
      triggers: 'updateProp2',
      dispatch: 'prop2Updated'
    }
  ],
  services: [
    {
      id: 'propN',
      setter: ':property',
      url: '/path/to/get/:property'
    }
  ]
});

// Let's update prop1
controller.request('propN', {
  shortcuts: {
    property: 'prop1'
  }
});

// Now, let's update prop2
controller.request('propN', {
  shortcuts: {
    property: 'prop2'
  }
});

// Finally, the following line will throw an error, since :property
// can not be resolved:
controller.request('propN');

// Note that it is possible to throw several requests at the same time:
controller.request([
  {
    service: 'propN',
    shortcuts: {
      property: 'prop1'
    }
  },
  {
    service: 'propN',
    shortcuts: {
      property: 'prop2'
    }
  },
  'propN'
]);

Here is how domino.js resolves shortcuts:

Note: It is possible to specify a description attribute for shortcuts, that can be accessed exactly as properties and services descriptions, through the help() instance method.

Here is an example with shortcuts declared directly in domino.js instance:

var controller = new domino({
  properties: [
    {
      id: 'prop',
      label: 'Property',
      value: 42,
      type: 'number',
      triggers: 'updateProp',
      dispatch: 'propUpdated'
    }
  ],
  services: [
    {
      id: 'prop',
      setter: 'prop',
      url: '/path/to/get/property?date=:date'
    }
  ],
  shortcuts: [
    {
      id: 'date',
      method: function(domino) {
        return (new Date()).toString();
      }
    }
  ]
});

In this last example, when the service 'prop' is called, the shortcut ':date' will be resolved as the current date.

Service specifications:

Here is the list of attributes to precise a service:

Request specifications:

Finally, here is a precise description of the options given to the request method:

Help (↑)

When descriptions are specified for properties, shortcuts, hacks and/or services, the method help() of the domino instance can retrieve these descriptions. Here are some use cases of this method:

// The two following test cases work exactly samely for shortcuts
// and services:
domInst.help('properties', 'myProperty1');
// Returns something like: 'Description of myProperty1'
domInst.help('properties');
// Returns something like:
// {
//   myProperty1: 'Description of myProperty1',
//   myProperty2: 'Description of myProperty2',
//   myProperty3: '[no description is specified]'
// }

// Here is how it works for hacks:
domInst.help('hacks', 'trigger', 'myEvent1');
// Returns something like:
// [
//   'Description of my hack n°1',
//   'Description of my hack n°2'
// ]
domInst.help('hacks', 'dispatch', 'myEvent2');
// Returns something like: 'Description of my hack n°1'
domInst.help('hacks');
// Returns something like:
// [
//   'Description of my hack n°1',
//   'Description of my hack n°2',
//   'Description of my hack n°3'
// ]

// Finally, it is possible to display everything:
domInst.help('full');
// Returns something like:
// {
//   properties: {
//     myProperty1: 'Description of myProperty1',
//     myProperty2: 'Description of myProperty2',
//     myProperty3: '[no description is specified]'
//   },
//   services: {
//     myService1: 'Description of myService1',
//     myService2: 'Description of myService2',
//     myService3: '[no description is specified]'
//   },
//   shortcuts: {
//     myShortcut1: 'Description of myShortcut1',
//     myShortcut2: 'Description of myShortcut2',
//     myShortcut3: '[no description is specified]'
//   },
//   hacks: [
//     'Description of my hack n°1',
//     'Description of my hack n°2',
//     'Description of my hack n°3'
//   ]
// }

Specifications summary (↑)

Here is a summary of the specifications of domino.js instanciation:

Main loop: Inside domino.js (↑)

The core function in domino.js manages the events chain.

Basically, when an event is dispatched from a module, it will trigger this loop. Then, the related properties will be updated, any module or hack listening to this event will be triggered - causing eventually new updates. After all these actions, new events are to be triggered. So the loop we be called again, but with all those new events instead of the one from the module, etc.

This same loop is also called as an output for services success and error function, and the global update method (accessible only through the domino.js instance itself).

Here is an example:

(module) -> updateProp -> event1 -> hack1 -> event3
                       -> event2 -> hack2 -> event4

Here, a module updates the property prop which dispatches events event1 and event2. Hack hack1 is triggered on event1, and hack hack2 is triggered on event2. Finally, hack1 dispatches event3 and hack2 dispatches event4.

The problem here is that, with a classic synchronous events management system, event3 would be dispatched before event2, when it is expected to be triggered "later".

The domino.js's main loop resolves this issue by executing the previous events chain as following:

(module) -> updateProp -> event1, event2 -> hack1, hack2 -> event3, event4

And even better: when an event is about to be triggered twice or more, it is dispatched only once instead.

For example, the following chain:

(module) -> updateProp -> event1 -> hack1 -> event3
                       -> event2 -> hack2 -> event3

... will become:

(module) -> updateProp -> event1, event2 -> hack1, hack2 -> event3

Scopes management (↑)

There is a lot of functions given to domino.js through the initial configuration and the modules. One particularity of domino.js is that these methods are called in a specific scope, that contains safe accesses to different properties, and tools to display logs.

Also, for some type of functions, some other parameters or values can be added in the scope - and some parameters can be added or modified directly in the scope - something like:

this.anyProperty = 42;

Default scope methods:

Here is the default methods that any of the functions you give to domino.js will find in its scope.

Additional methods:

Also, some functions you will give to domino.js will have access to some more methods, that can update properties or call AJAX services. Here is the list of thoses methods:

Important: All the methods described (the default and the additional ones) are also available in the object returned by the domino.js constructor itself. Also, the default scope is always given as the first parameter to the modules constructors.

Functions given to domino:

Here is the list of every types of functions you can give to domino.js, with the related specifications (what you can modify directly in the scope, which parameters are given, which additional methods are available):

Logs and global settings (↑)

The global method domino.settings is used to manage global domino.js settings. It works like most jQuery methods:

Here is the list of currently recognized global settings:

Also, domino.js provides its own functions to log, warn or throw errors:

Finally, all the logs/warns/errors will be prefixed by the instance name if specified (the string "domino" otherwise).

Killing instances and modules (↑)

Killing modules:

It is possible to destroy a module by calling the method killModule() of the related domino instance. It will basically destroy every ascending or descending connections between the module and the instance:

// Kill the module:
controller.killModule(myModule);

If the module has an id field specified, it is also possible to kill it through its id:

// Kill the module:
controller.killModule('myModuleId');

Killing a domino instance:

Also, it is possible to kill a domino instance, by calling its method kill(). It will basically remove every external reference in the instance, and destroy every connections with eventually existing modules. Also, if the instance as a name, it will be possible again to call an instance with the same name.

// Kill the instance:
controller.kill();

Structures (↑)

domino.js provides its own helpers to manipulate some "Closure like" types in the domino.struct object. Since they can be more complex than simple string types, they are called structures.

Those structures are:

Except for 'undefined' and 'null', all the previously described structures are valid to characterize a property.

Here the list of the available functions to manipulate those structures:

domino.struct.get(null);      // 'null'
domino.struct.get(undefined); // 'undefined'
domino.struct.get(42);        // 'number'
domino.struct.get('toto');    // 'string'
domino.struct.get({a: 1});    // 'object'
domino.struct.get([1,2,3]);   // 'array'
domino.struct.check({a: 'number'}, {a: 1}); // true
domino.struct.check('object', {a: 1});      // true
domino.struct.check('?object', {a: 1});     // true
domino.struct.check('*', {a: 1});           // true
domino.struct.check({a: '?number'}, {});    // true
domino.struct.check('*', {a: 1});           // true
domino.struct.check({a: 'number'}, {});     // false
domino.struct.check(['number'], []);        // true
domino.struct.check(['number'], [1]);       // true
domino.struct.check(['number'], [1, 2]);    // true
domino.struct.isValid({a: 'number'});   // true
domino.struct.isValid([{a: 'number'}]); // true
domino.struct.isValid('object');        // true
domino.struct.isValid('?object');       // true
domino.struct.isValid('?object|array'); // true
domino.struct.isValid('?object|');      // false
domino.struct.isValid('undefined');     // false
domino.struct.isValid('null');          // false
domino.struct.deepScalar('number');        // true
domino.struct.deepScalar('?number');       // true
domino.struct.deepScalar(['?number']);     // true
domino.struct.deepScalar('string|number'); // true
domino.struct.deepScalar({a: 'number'});   // true
domino.struct.deepScalar([{a: 'number'}]); // true
domino.struct.deepScalar('object');        // false
domino.struct.deepScalar('?object');       // false
domino.struct.deepScalar('object|number'); // false

Also, it is possible to define globally structures, with the method domino.struct.add(). This makes possible to use recursive structures, and to avoid declaring several times the same structures in different instances of domino.js. Also, it is possible to define abstract structures with just a method that can determine wether any value matches the structure or not.

This method can be used by different ways:

Here are some examples:

// Here is how to add the "integer" structure:
domino.struct.add('integer', function(v) {
  // v===+v  tests if the value is a number
  // v===~~v tests if the value is an integer
  return v===+v && v===~~v;
});

// Here are some tests:
[
  123,
  -24,
  12.4,
  '12',
  '12.4',
  'twelve'
].forEach(function(v) {
  console.log(
    domino.struct.check('integer', v) ?
      v + ' is an integer' :
      v + ' is not an integer'
  );
});

// Here is another example, more data-oriented:
domino.struct.add({
  id: 'user',
  struct: {
    login: 'string',
    name: '?string',
    friends: ['string']
  }
});

// Here are some tests:
domino.struct.check('user', {
  login: 'bwayne',
  name: 'Bruce Wayne',
  friends: [
    'batman',
    'apennyworth'
  ]
});

The parameter 'proto' will indicates structures that must be considered as valid, event if they are not already created - only interesting if you need to use multi recursive structures, as in the following example:

// "struct1" describes an array of "struct2" elements. Since "struct2" is not
// defined yet, it is declared in "proto" values, to avoid "Wrong type error".
domino.struct.add({
  id: 'struct1',
  proto: ['struct2'],
  struct: ['struct2']
});

// "struct2" describes an object that is empty or might contain a "struct1"
// element associated to the key "key".
domino.struct.add('struct2', {
  key: '?struct1'
});