View on GitHub

shoveJS

a micro-architecture for JavaScript.

Download this project as a .zip file Download this project as a tar.gz file
version 0.1

shoveJS

...is a JavaScript microframework (lightly) inspired by RobotLegs (ActionScript 3) and its default dependency injection library SwiftSuspenders. It's meant to provide implementations of common patterns needed while developing web apps. If you don't like a specific piece, you should be able to swap it out with another implementation, and if you don't need a piece, you should be able to omit it.

The overall goal is to make bootstrapping a modular application a little more sane. shoveJS encourages modularity, flexibility, and loose-coupling.

A few of the provided features:

It currently depends on the following 3rd party libraries:

Some examples require additional 3rd party libraries, but they're not otherwise required:

Application Bootstrapping

An application's HTML typically includes requirejs and creates a new instance of Main in the callback (which is passed an HTMLElement to serve as the root view).

The application main will then:

Dependency Injection

Defining Injectable Fields for a Class

Each class that will have properties injected, defines....

    SomeClass.injectableFields = {fieldName:FieldClass, fieldName2:FieldClass};

In most cases, your classes will have their injectable fields mapped automatically. This is the case for:

Manually register a class to have its injectable fields mapped:

    context.injector.mapInjectionFields(SomeClass);

Defining Injection Values

Values are mapped to injectable fields:

    context.injector.mapValue = (SomeClass, "injectionField", injectionValue)

The injectionValue will be mapped to any injectable field of type SomeClass matching "injectionField".

NOTE: the type of class being injected into does not matter. More on child Injectors later.

Injection Life-cycle

The above set-up must be in place first. Afterwards:

    context.injector.injectInto(someInstance);

Serialization / Deserialization

Register Model Class

shoveJS makes no assumptions about your model classes. It should not be necessary to store unique ids, (de)serialization logic, etc in your models. shoveJS promotes a model containing nothing but data and (eg update) signals.

Any model (and the models it uses via composition) need to be registered in the class registry:

    context.registerClass(SomeModelClass);

This feature relies on a ClassRegistry which maps Classes to an id. The proviced ClassRegistry implementation will call toString as a static method of the registering Class if an id is not provided.

For example:

    context.registerClass(SomeClass.toString(), SomeClass);
would be equivalent to:
    context.registerClass(SomeClass);

Register Model Factories

    var someModelFactoryInstance = new SomeModelFactory();
    context.factoryRegistry.set(SomeModel, someModelFactoryInstance);

Factory Interface

Factory Patterns

toJSON

The Factory is passed the instance to be serialized:

    DictionaryFactory.prototype.toJSON = function (dictionary) { /* */ };

A typical toJSON implementation starts by creating an empty object:

    var jsonObj = {};

Fields containing only Numbers & Strings (or Arrays/Objects containing only Numbers, Strings, Arrays, and Objects) can be serialized directly:

    jsonObj.field = model.field;

The JSONService exposes a method toJSONref which can be utilized by factory.toJSON methods to convert composed objects into "JSON references" pointing to registered Objects. Objects / values for which no entry is found are returned unchanged. //????

For example, the DictionaryFactory:

    jsonObj.keys = [];
    for (var i=0, len=dictionary.keys.length; i<len; i++)
    {
        jsonObj.keys.push( this.jsonService.toJSONref(dictionary.keys[i]) );
    }

A new keys field is created on the Dictionary's JSON object. For each key, a value is placed in that Array containing a special object described in the JSON Format section below.

fromJSON

A factory is passed the jsonObject to be de-serialized:

    DictionaryFactory.prototype.toJSON = function (dictionary) { /* */ };

The typical fromJSON implementation starts by creating an empty model:

    var dictionary = new Dictionary();

Fields containing only Numbers & Strings (or Arrays/Objects containing only Numbers, Strings, Arrays, and Objects) can be deserialized directly:

    model.field = jsonObj.field;

JSONService uses the Q library's Promise implementation to provide instance references:

    jsonObj.referencedObject.promise.then( function (referencedObject) {model.referencedObject = referencedObject;} );

If your model needs to do some processing once the value is available use a setter function:

    jsonObj.normalizedPoint.promise.then ( function (point) {model.setNormalizedPoint(point); } );

Writing deserialization promise logic is by far the trickiest part of using the framework. (It's worth it!) The included Factory classes contain several patterns worth using as starting points.

JSON Format

The JSON returned from toJSON will always contain two fields:

or:
    {
        "meta":{ /* */ },
        "data":{ /* */ }
    }

The meta field contains a hash of object references:

    "meta":
    {
      "bfb37975-148e-5f87-6f12-f4a15dbf3b34:0": {},
      "bfb37975-148e-5f87-6f12-f4a15dbf3b34:1": {}
    }
    

Each of the values contains two fields:

If any property within value's tree is itself a registered object, it will be converted to a JSON reference of the form:

{"$ref": "bfb37975-148e-5f87-6f12-f4a15dbf3b34:1"}:

The same reference objects are used in the data branch of the object tree. When sending the JSON string to the server the resulting JSON might look like:

    {"meta" :{
      "bfb37975-148e-5f87-6f12-f4a15dbf3b34:0": {
        "type": "[Some Class]",
        "value": {
          "field1": [
            {
              "$ref": "bfb37975-148e-5f87-6f12-f4a15dbf3b34:1"
            }
          ]
        }
      },
      "bfb37975-148e-5f87-6f12-f4a15dbf3b34:1": {
        "type": "[Some Class]",
        "value": {
          "field1": [
            {
              "$ref": "bfb37975-148e-5f87-6f12-f4a15dbf3b34:0"
            }
          ],
          "field2": {
            "$ref": "bfb37975-148e-5f87-6f12-f4a15dbf3b34:7"
          }
        }
      },
      "bfb37975-148e-5f87-6f12-f4a15dbf3b34:7": {
        "type": "[Dictionary Class]",
        "value": {
          "keys": [
            {
              "$ref": "bfb37975-148e-5f87-6f12-f4a15dbf3b34:11"
            }
          ],
          "values": [
            {
              "$ref": "bfb37975-148e-5f87-6f12-f4a15dbf3b34:12"
            }
          ]
        }
      },
      "bfb37975-148e-5f87-6f12-f4a15dbf3b34:11": {
        "type": "[Data1 Class]",
        "value": 123
      },
      "bfb37975-148e-5f87-6f12-f4a15dbf3b34:12": {
        "type": "[Data2 Class]",
        "value": "xxx"
      }
    }, "data" :{
      "a": {
        "$ref": "bfb37975-148e-5f87-6f12-f4a15dbf3b34:0"
      },
      "a": {
        "$ref": "bfb37975-148e-5f87-6f12-f4a15dbf3b34:1"
      },
      "c": {
        "$ref": "bfb37975-148e-5f87-6f12-f4a15dbf3b34:2"
      }
    }} 

When deserializing, the returned object will match the structure defined in the data field.

About the ObjectRegistry

An ObjectRegistry maps a unique object id to an instance. Usually, this is handled automatically.

Manually:

    objectRegistry.registerModel(id, instance);

The JSONService has dependencies mapped for injection:

These are mapped for you.

The JSONService handles calling factory.toJSON and factory.fromJSON for each object type referenced while walking data objects and json objects respectively & recursively.

Automatic View Creation / Mediation

Register Model Classes

(See above)

Map Model Classes to View Classes

    context.mapModelView(Controls,      ControlsView);

an additional parameter allows multiple views per model class

    context.mapModelView(Controls,      ControlsView,   'map');

Register View Factory Classes

(See above)

    context.mapViewMediator(ControlsView,       ControlsCommandMap);

Mediator Classes

Mediator instances are automatically created and linked to views when views are created. They're responsible for linking UI-events to Command classes. Signals published by the View class are automatically mapped to the Mediator's injection fields of the same name.

    ControlsCommandMap.injectionFields = {
        context:Context, 
        loadRequested:signals.Signal,
        saveRequested:signals.Signal
    };

View Classes

Views have:

The Mediator's map/unmap methods link/unlink these signals to Command classes.

    ControlsCommandMap.prototype.map = function () {
        this.context.mapSignal(this.loadRequested,  LoadNewFeatureDataCommand,  this);
        this.context.mapSignal(this.saveRequested,  SaveZoneDataCommand,        this);
    }

    ControlsCommandMap.prototype.unmap = function () {
        this.context.unmapSignal(this.loadRequested,    LoadNewFeatureDataCommand,  this);
        this.context.unmapSignal(this.saveRequested,    SaveZoneDataCommand,        this);
    }

Command Interface

Commands:

Creating / Getting a View

    var controlsView = context.getViewByModel(controls);
    var controllerData = {controls:controls};
    context.registerView(controlsView, controllerData);

If you're using the multi-views per model class case above:

    var controlsView = context.getViewByModel(controls, 'map');

Service Initialization

Service Interface

Services have:

Service Life-cycle

When you register a service, the following things happen:

The service's injectable fields are mapped:

    context.injector.mapInjectionFields(SomeService);

The service is registered under its id & class:

    var someService = new SomeService(); // simplified here
    context.injector.mapValue(SomeService, 'someService', someService);

When the context is initialized:

Utilities

A few "internals" that might be useful.

Dictionary

Easily map from any kind of object to any other kind of object. The JavaScript Array/Object classes are limited to Numeric/String keys.

The Dictionary is used heavily throughout the framework. The implementation could be improved performance-wise, but I've left it in a more readable form, during development.

ModelMediator

Note: This is different from the View Mediator pattern discussed above.

Pass a model instance to the constructor function. The object you get back will proxy set/get access on the original model object while providing a change handler for each field in the model. In the default implementation, that's every field accessible by for..in

The change signals are stored in a Dictionary called __on__

    var mediator = new ModelMediator(modelInstance);
    mediator.__on__.someField.add(function () { console.log("someField changed!") })