container-js icon indicating copy to clipboard operation
container-js copied to clipboard

ContainerJS is a Dependency Injection Continer for JavaScript Application.

trafficstars

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

  • IE9+
  • GoogleChrome
  • Firefox4+
  • IE7,8 with es5-shim ( https://github.com/kriskowal/es5-shim )

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";
        } );
    });