container-js

Dependency Injection Container for JavaScript Web Application.


Keywords
DI, Dependency Injection, IoC, Inversion of Control, Aspect oriented programming, Lazily module loading
License
BSD-3-Clause-Clear
Install
npm install container-js@1.0.6

Documentation

Build Status Code Climate

About

ContainerJS is a Dependency Injection Container for JavaScript Web Application.

Features

  • Dependency Resolution and Injection
    • ContainerJS is responsible for the creation of container-managed objects, and the resolution and injection of its dependent components.
      • You can specify a dependency in the component definition by JavaScript code, or can be defined declaratively in the class.
      • Since there is no interface in JavaScript, dependency resolution is done in the name assigned to the component.
    • By using the dependency injection container, you can automate the wiring.
    • Because of the component is cached by the container, you can reduce the creation of unnecessary objects.
  • Lazy Module Loading
    • It loads the required modules lazily and asynchronously by working with the require.js.
    • Until the component is actually used by the user's operation or the like, you can delay the loading and evaluation of the JavaScript source.
  • Supports Aspect Oriented Programing
    • You can weave a method interceptor to a container-managed component.
    • You can aggregate a cross-class features like a performance measurement, into the interceptor.

License

New BSD License

Dependent Libraries

ContainerJS is dependent on the following modules.

In addition, we use the following testing framework.

Support Browsers

Getting Started

Here is an example of the "Hello World". Please also see 'samples/hello-world'.

file layout:

  • index.html
  • scripts/
    • main.js
    • require.js
    • container.js
    • app/
      • model.js
      • view.js
    • utils/
      • observable.js

scripts/app/model.js:

define(["utils/observable"], function(Observable){

     "use strict";

     /**
     * @class
     */
    var Model = function() {};

    Model.prototype = new Observable();

    /**
     * @public
     */
    Model.prototype.initialize = function() {
        this.fire( "updated", {
            property: "message",
            value :"hello world."
        });
    };

    return Model;
});

scripts/app/view.js:

define(["container"], function( ContainerJS ){

    "use strict";

    /**
     * @class
     */
    var View = function() {
        this.model = ContainerJS.Inject("app.Model");
    };

    /**
     * @public
     */
    View.prototype.initialize = function() {
        this.model.addObserver("updated", function( ev ) {
            if ( ev.property != "message" ) return;
            var e = document.getElementById(this.elementId);
            e.innerHTML = ev.value;
        }.bind(this));
    };

    return View;
});

scripts/app/main.js:

require.config({
    baseUrl: "scripts",
});
require(["container"], function( ContainerJS ) {

    var container = new ContainerJS.Container( function( binder ){

        binder.bind("app.View").withProperties({
            elementId : "console"
        }).onInitialize("initialize")
        .inScope(ContainerJS.Scope.EAGER_SINGLETON);

        binder.bind("app.Model");

    });
    container.onEagerSingletonConponentsInitialized.then(function() {
        container.get("app.Model").then(function( model ){
            model.initialize();
        }, function( error ) {
            alert( error.toString() );
        });
    });

});

index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello World</title>
    <script type="text/javascript" data-main="scripts/main.js" src="scripts/require.js"></script>
  </head>
  <body>
    <div id="console"></div>
  </body>
</html>

References

Binding

Supports the binding of components by the following 5 ways.

  • Class Binding
  • Prototype Binding
  • Object Binding
  • Provider Binding
  • Instance Binding

Class Binding

  • Specifies a class (same as a constructor function) to a component.
  • The object that is created by the new operator will be the component.
  • The constructor function is load asynchronously using the require.js's require().
  • You can specify an argument passed to the constructor function by using withConstructorArgument().

Component Definition:

var container = new ContainerJS.Container( function( binder ){
    binder.bind("app.Class");
    binder.bind("anotherName").to("app.Class").withConstructorArgument({
        foo:"foo",
        var:ContainerJS.Inject("app.Class") // Dependency injection can also
    });
});

app/class.js:

define(function(){
    /**
     * @class
     */
    var Class = function(arg) {
        this.foo = args.foo;
        this.var = args.var;
    };
    return Class;
});

Prototype Binding

  • Specifies a prototype to a component.
  • The object that is created by Object#create(<prototype>) will be the component.
  • The prototype is load asynchronously using the require.js's require().

Component Definition:

var container = new ContainerJS.Container( function( binder ){
    binder.bind("app.Prototype").asPrototype();
    binder.bind("anotherName").toPrototype("app.Prototype", {
       foo : { value: "foo" } // You can specify the arguments to be passed to `Object#Create()` in the second argument.
    });
});

app/prototype.js:

define(function(){
    /**
     * @class
     */
    var Prototype = {
        method : function( arg ) {
            return arg;
        }
    }
    return Prototype;
});

Object Binding

  • The object that loaded by requirejs's "require" will be a component.

Component Definition:

var container = new ContainerJS.Container( function( binder ){
    binder.bind("app.Object").asObject();
    binder.bind("anotherName").toObject("app.Object");
});

app/object.js:

define(function(){
    var Obj = {
        method : function( arg ) {
            return arg;
        }
    }
    return Obj;
});

Provider Binding

  • Specifies a function to generate the component.
  • The function's return value will be the component.

Component Definition:

var container = new ContainerJS.Container( function( binder ){
    binder.bind("name").toProvider(function(){
        return "foo";
    });
});

Instance Binding

  • Specifies the component itself.

Component Definition:

var container = new ContainerJS.Container( function( binder ){
    binder.bind("name").toInstance("foo");
});

Packaging Policy

By setting the Packaging Policy, where you can control the loading of modules.

MODULE_PER_CLASS

This Is the default policy. It assumes that the module are separated per class. A Component is loaded from <A class name "-" was separated>.js following the same path as the namespace.

file layout:

  • app/
    • foo/
      • hoge-hoge.js
      • fuga-fuga.js
  • main.js

app/foo/hoge-hoge.js:

define(function(){
    /**
     * @class
     */
    var HogeHoge = function(arg) {};
    return HogeHoge;
});

app/foo/fuga-fuga.js:

define(function(){
    /**
     * @class
     */
    var FugaFuga = function(arg) {};
    return FugaFuga;
});

main.js:

var container = new ContainerJS.Container( function( binder ){
    binder.bind("app.foo.HogeHoge");
    binder.bind("app.foo.FugaFuga");
});

MODULE_PER_PACKAGE

It assumes that the module are separated per package.

file layout:

  • app/
    • foo.js
  • main.js

app/foo.js:

define(function(){

    /**
     * @class
     */
    var HogeHoge = function(arg) {};

    /**
     * @class
     */
    var FugaFuga = function(arg) {};

    return {
        HogeHoge : HogeHoge,
        FugaFuga : FugaFuga
    }
});

main.js:

var container = new ContainerJS.Container( function( binder ){
    binder.bind("app.foo.HogeHoge")
        .assign(ContainerJS.PackagingPolicy.MODULE_PER_PACKAGE);
    binder.bind("app.foo.FugaFuga")
        .assign(ContainerJS.PackagingPolicy.MODULE_PER_PACKAGE);
});

SINGLE_FILE

It assumes that all of the classes in a namespace are defined into a single file.

file layout:

  • app.js
  • main.js

app.js:

define(function(){

    /**
     * @class
     */
    var HogeHoge = function(arg) {};

    /**
     * @class
     */
    var FugaFuga = function(arg) {};

    return {
        foo : {
            HogeHoge : HogeHoge,
            FugaFuga : FugaFuga
        }
    }
});

main.js:

var container = new ContainerJS.Container( function( binder ){
    binder.bind("app.foo.HogeHoge")
        .assign(ContainerJS.PackagingPolicy.SINGLE_FILE);
    binder.bind("app.foo.FugaFuga")
        .assign(ContainerJS.PackagingPolicy.SINGLE_FILE);
});

In addition to be specified in the component definition, The default packaging policy can also be specified in the constructor arguments of the container.

var container = new ContainerJS.Container( function( binder ){
    binder.bind("app.foo.HogeHoge");
    binder.bind("app.foo.FugaFuga");
}, ContainerJS.PackagingPolicy.SINGLE_FILE); // specified the default settings by the constructor arguments.

Scope

Supports the "Singleton", "EagerSingleton", "Prototype". the "Singleton" is the default.

  • Singleton
    • Creates only one component.
    • If you get the same component multiple times, the same component will return always.
    • The Components are discarded by Container#destroy().
  • EagerSingleton
    • Will return the single instance of like Singleton, an instance will be created when creating the container.
    • You can create a component to be effective only to be registered into the container.
  • Prototype
    • Each time you get a component, and then re-create the component.

Configuration change is done in the inScope() .

var container = new ContainerJS.Container( function( binder ){
    binder.bind("Foo").inScope( ContainerJS.Scope.SINGLETON ); // default
    binder.bind("Bar").inScope( ContainerJS.Scope.EAGER_SINGLETON );
    binder.bind("Val").inScope( ContainerJS.Scope.PROTOTYPE );
});
container.onEagerSingletonConponentsInitialized.then(function(){
    // called when all eager singleton conponents are initialized.
});

Injection

If you set a ContainerJS.Inject to the property, the dependent module is searched and Injected by the container.

  • Setting a ContainerJS.Inject, a component will be searched by the property name.
  • Using ContainerJS.Inject(name) , You can explicitly specify the name of the component to search.
  • Setting a ContainerJS.Inject.all or ContainerJS.Inject.all(name) , An array of components with the specified name will be injected.
  • Setting a ContainerJS.Inject.lazily,ContainerJS.Inject.lazily(name),ContainerJS.Inject.all.lazily,ContainerJS.Inject.all.lazily(name), Component(s) to be injected will then be load lazily.
    • Instead the component, Deferred in order to get the component is injected.

Example:

define(["container"], function(ContainerJS){
    /**
     * @class
     */
    var Class = function() {
        this.a = ContainerJS.Inject;
        this.b = ContainerJS.Inject("foo.var");
        this.c = ContainerJS.Inject.all;
        this.d = ContainerJS.Inject.all("foo.var");
        this.e = ContainerJS.Inject.lazily;
        this.f = ContainerJS.Inject.all.lazily("foo.bal");
    };
    Class.prototype.method1 = {
        this.e.then( function(component) {
            //
        }, function(error) {
            //
        } )
    };
    return Class;
});

You can also be injected at the time of the component definition.

var container = new ContainerJS.Container( function( binder ){
    binder.bind("Foo").withProperties({
        a : ContainerJS.Inject("foo.var")
    }).withConstructorArgument({
        b : ContainerJS.Inject.all.lazily("foo.bal")
    });
});

Initialization and Destruction

You can register a function to be called when creating and destroying components.

  • Initialization function is executed after when all of creation phases are completed.
  • Destruct function is executed when container.Container#destroy() is called if the following conditions are met.
    • the scope of the component is Singleton or EagerSingleton.
    • the component is already created.
  • You can specify the functions by the component method name or a function.
    • If you specify a function, components and containers will be passed as an argument.

Example:

var c = new ContainerJS.Container( function( binder ) {

    // specifies the component method name.
    binder.bind( "Foo" ).onInitialize("initialize").onDestroy("dispose");

    // specifies a function.
    binder.bind( "Bar" ).onInitialize( function( component, container ) {
        component.initialize();
    }).onDestroy( function( component, container ) {
        component.dispose();
    });

});

Method Interception

You can weave an interceptor to a method of the component.

  • The interceptor would be specified in the function. an object that contains the method name and arguments is passed in the argument.
  • you can specify a function to indicate the components and methods to be applied the interceptor in the second argument.
    • If the second argument is not explicitly specified, the interceptor applies to all methods of all components.

Example:

    var container = new ContainerJS.Container( function( binder ){

        binder.bind("app.Component");

        binder.bindInterceptor( function( jointpoint ) {
           jointpoint.methodName;
           jointpoint.self;
           jointpoint.arguments; // Arguments. Can be modified.
           jointpoint.context; // You can store the state that is shared between the invocation of this method.
           return jointpoint.proceed(); // Returns the result of calling the original method.
        }, function(binding, component, methodName) {
            if  (binding.name !== "app.Component" ) return false;
            return methodName === "method1"
                   || methodName === "method2";
        } );
    });