node package manager

toy-box

TOY BOX - Simple, Efficient and Powerful DI For Javascript

Travis npm Maintenance Made With Love

ATTN!

This is more of a proof of concept then anything else, while I do dog food my own work in my projects and will continue to maintain this project I cannot guarantee this is for every one.

Building off the ideas behind Factory Function Pattern and ideas inspired by Symfony Framework and how they do DI.

Toy Box is more of a container that holds registered functions and there associated dependencies. Containers, upon fetching, can have other containers injected into them as arguments, instantiated objects injected and even have there associated setter methods setup for them, so that you get back a fully built object.

This is heavily inspired by Symfony's Container based system and even uses similar method names, such as register, addArgument and addReference.

When using Toy Box, it is important that you instantiate the container at the top levels of the app and pass down the reference to allow for the objects to be created as you need them to be.

For example you might have an object that sets up the container and then passes an instance to some kind of App object. from there the user could do something like: App.fetch('registeredObject').methodName().

Why Toy Box

Toy Box was created to allow me to explore the concepts of dependency injection in the land of javascript coming from a heavy framework oriented world, known as PHP. While I appreciate the concepts and advances of ES6 and even employ their services here, such as import and const fn = () => { ... }, It is important to know that I am simply exploring.

While the code is clean and well tested, as well as well documented. The tool is in very early stages of development and there could be a lot of breaking changes. With that in mind I do hope that Toy Box can see some kind of adoption.

The goal was simple: See what works and what doesn't work with Toy Box. The core API is very simple to use and understand, and when worked with at top levels of the application development process its power is quickly trickled down.

How To install:

npm install toy_box

You can also clone the repo and run the tests by doing: npm install && npm test.

To make the distribution version: gulp make:toy_box.

How to use:

The most basic and simple way to use Toy Box is to register a function:

import ToyBox from 'toy_box';
 
const Foo = () => { return 'hello world!'; };
 
const toyBox = ToyBox();
 
toyBox.register('foo', Foo);

We can then do:

toyBox.fetch('foo'); // => 'Hello world!' 

In the background we find the registered function and do a .call on it. If there are arguments, which are described below, we will call .apply instead.

.fetch is the key component here. It is responsible for taking all the arguments, all of the "things" that make up the object its self and combine them together to give you the "toy" in question from the "box".

Fetching From The Container

fetch(containerName) simply fetches the object from the container, injects its dependencies and sets up its setters based on any container specific or global setters that were assigned to the container.

fetch(containerName) will return the string representation of the container name if the container cannot be found.

Adding Arguments

The following example allow you to pass in arguments to a registered container object.

import ToyBox from 'toy_box';
 
const Foo = (message) => { return message; };
 
const toyBox = ToyBox();
 
toyBox.register('foo', Foo).addArgument('hello world');

The addArgument() method allows you to chain multiple addArgument's to then add multiple arguments to function it's self.

With the arguments added can now do:

toyBox.fetch('foo'); // => hello world 

References

We can pass a reference of another function in to the function being registered in the container. To do this we call addreference.

import ToyBox from 'toy_box';
 
const Foo = (message) => { return message; };
const Bar = () => { return 'hello world!'; };
 
const toyBox = ToyBox();
 
toyBox.register('foo', Foo).addReference(Bar);
 
toyBox.fetch('foo'); // => 'hello world!' 

We can also do this with container registered objects:

import ToyBox from 'toy_box';
 
const Foo = (message) => { return message; };
const Bar = () => { return 'hello world!'; };
 
const toyBox = ToyBox();
 
toyBox.register('bar', Bar);
toyBox.register('foo', Foo).addReference('bar');
 
toyBox.fetch('foo'); // => 'hello world!' 

You can also add container based objects as arguments to another container: toyBox.register('foo', Foo).addArgument('bar'); addReference and addArgument are the same under the hood. The main difference is that addReference is meant to pass an instantiated object to the container object, where as addArgument can pass any type of argument.

ATTN!

If you are attempting to reference a container that doesn't exist as a dependency of another container and we cannot find the container in to be referenced we will throw an error.

Setters

Sometimes the function you are working with will have setters and getters which are apart of Ecma Script 5.1. If so you can set them up with either an object from the container or with a value. Lets look at both.

import ToyBox from 'toy_box';
 
const Foo = () => {
  let _message = '';
 
  return {
    set message(msg) {
      _message = msg;
    }
 
    get message() {
      return _message;
    }
  }
};
 
const Bar = () => { return 'hello world!'; };
 
const toyBox = ToyBox();
 
toyBox.register('foo', Foo).setUpSetter('message', Bar);
 
toyBox.fetch('foo').message; // => 'hello world!' 

In the above example we pass in Bar which returns 'hello world!'. This is then set up on the setter called message which belongs to the function we registered called Foo.

This equates to us doing: const foo = Foo(); foo.message = Bar();

But what if we want to set up a setter with another registered object? This is where it gets even more fun:

import ToyBox from 'toy_box';
 
const Foo = () => {
  let _message = '';
 
  return {
    set message(msg) {
      _message = msg;
    }
 
    get message() {
      return _message;
    }
  }
};
 
const Bar = (message) => { return message; };
 
const toyBox = ToyBox();
 
toyBox.register('bar', Bar).addArgument('hello world!');
toyBox.register('foo', Foo).setUpSetter('message', 'bar');
 
toyBox.fetch('foo').message; // => 'hello world!' 

The above is the same as the first example, accept we have registered Bar with a dependency that is auto injected. We can then set up Foo and say that the setter for Foo, in this case message, will be set to bar, which in this case would cause us to build Bar and then build Foo.

The idea behind this comes from Symfony's DI where you can inject a class via a setter method. The same concept can be applied here. We are injecting some value into the objects setter method.

ATTN!

If you are attempting to set up a setter with a container object that doesn't exist we will throw an error. The reason behind this is because the concept of this function is that you are registering a dependency not through the constructor but through the setter of the object in question. This is good practice when the object in question has said dependency as an optional dependency.

Make Note: setUpSetter does not return anything, thus you cannot chain off of it.

Global Setters

What if you wanted to set a global setter? That is, for all functions that have a setter, unless specifically over ridden we would set that value to the global container value? Well consider the following:

import ToyBox from 'toy_box';
 
const Foo = function() {
  let _message = '';
 
  return {
    set message(msg) {
      _message = msg;
    }
 
    get message(msg) {
      return _message;
    }
  }
};
 
const Bar = function(message) { return message; };
 
const toyBox = ToyBox();
 
toyBox.register('bar', Bar).addArgument('hello world!');
toyBox.register('foo', Foo);
 
toybox.setupGlobalSetters('message', 'bar');
 
toyBox.fetch('foo').message; // => 'hello world!' 

In the above example we state that the container is to have a global setter called message. That is any function or object that has a setter method called message will get the value of the registered container object bar. Because you may have multiple setters or different types of setters across these objects you can pass an array of arrays:

toybox.setupGlobalSetters([['message', 'bar'], [ ... ], ...]);

ATTN!!

The same concept as above is applied here as well. If the container to be registered as a global setter does not exist we will throw an error.

Over Riding Global Setters with other Global Setters

It should be no secret that doing: toybox.setupGlobalSetters([['message', 'bar'], [ 'message', 'foo-bar' ]]);

Will set the objects setter method of message to 'foo-bar'. Instead what you will probably mean is to do:

toybox.setupGlobalSetters('message', 'bar');
toyBox.register('foo', Foo).setUpSetter('message', 'foo-bar');

It doesn't pollute the global setters for other objects that want 'bar' as the setter and still allows you to change the setter value of message on the foo container.

Resolving Dependencies

The difference between fetch and resolve is that resolve executes an event after the object is resolved of its dependencies. The object is then passed to the event function and should thus be returned from said event function.

For example:

import ToyBox from 'toy_box';
 
const toyBox = ToyBox();
 
const Foo = () => {
  let _message = '';
 
  return {
    set message(msg) {
      _message = msg;
    },
 
    get message() {
      return _message;
    }
  }
}
 
toyBox.register('foo', Foo);
 
const fooInstance = toyBox.resolve('foo', function(obj){
  obj.message('hello world');
 
  // ... 
 
  return obj;
});
 
console.log(fooInstance.message); // => 'hello world'; 

As you can see after resolving the object you can then do additional logic before returning the object instance for the out side world to use.

ATTN!!

Unlike fetch, where if the container cannot be resolved we return the string name of the container, this function will throw an error stipulating that the container could not be resolved.

Compiler

The compiler is a great way to check if you have any issues with your objects and their dependencies. Another reason from calling on the .compiler property is to access two of the main of the main functions: compile and registerCompilerPass These concepts are taken straight from symfony with some slight modifications.

One is that the compiler will compile the container and create a compiled container that fetch will then use to fetch the built object from. If the object does not exist in the compiled container we will search the main container and build the object.

ATTN!

If we are forced to search the main container to build the object based on its dependencies we will not add this newly created object to the compiled container.

The compiler can also have what are called registered compiler pass functions. That is functions run at the time we compile the container. These registered function can manipulate the container before its compiled. This allows you to write Passes for individual container objects or for the container as whole.

Compiler: .compile()

This function will compile the container after it runs any registered compiler pass functions. It can throw errors if something goes wrong in compiling the objects or if the compiler pass functions them selves do have a process function defined.

toolBox.compiler.compile();
 
// Now fetch will read from the compiled container instead of creating the object it's self every time you need it. 

Compile: .registerCompilerPass(obj)

Allows you to register objects that are run at the time of compile() being called. These should be named after there individual container objects, how ever they can global. The important aspect is that they all have a process function which takes an argument which is an instance of Toy Box.

 
import ToyBox from 'toy_box.js';
import Foo from 'foo.js';
 
const FooCompilePass = () => {
  return {
    process(toyBox) {
      if (!toyBox.getName('foo')) {
        toyBox.register('foo', Foo).addArgument('hello world');
      }
    }
  }
}
 
ToyBox.compiler.registerCompilerPass(FooCompilePass());
ToyBox.compiler.compile();

Now when you run compile we will first run the compile pass function then compile the container down.

Deprecating Containers

In the interest of keeping backwards computability, you might want to deprecate a container object. If the container is deprecated then when ever a developer fetches that container they will be warned to use the new container instead.

import ToyBox from 'toy_box';
 
const Foo = function() {
  let _message = '';
 
  return {
    set message(msg) {
      _message = msg;
    }
 
    get message(msg) {
      return _message;
    }
  }
};
 
const Bar = function(message) { return message; };
 
const toyBox = ToyBox();
 
toyBox.register('bar', Bar).addArgument('hello world!');
toyBox.register('foo', Foo);
 
toybox.deprecate('foo', 'bar');
 
// Will throw a warning telling the developer they should use 'bar' instead. 
toyBox.fetch('foo').message = 'hello world';

ATTN!

This will affect compiled containers as well.

Aliasing Container Names

Should you have a contain name, for example, foo.inner.child.bar you can alias the container name to foo. This allows you to call foo when you call .fetch().

If the aliased container doesn't exist we will fall back to searching for the container under the name of foo and if that cannot be found then we throw an error.

import ToyBox from 'toy_box';
 
const Foo = function() {
  let _message = '';
 
  return {
    set message(msg) {
      _message = msg;
    }
 
    get message(msg) {
      return _message;
    }
  }
};
 
const toyBox = ToyBox();
 
toyBox.register('foo.inner.containr.name', Foo);
 
toybox.alias('foo.inner.containr.name', 'foo');
 
toyBox.fetch('foo'); // Returns instance of foo.inner.containr.name 

ATTN!

This will affect compiled containers as well.

Regarding Compiled Container For Alias and Deprecation

When you alias or deprecate a container object and then call compile() we will set that information into the compiled container object so that when you fetch from the compiled container, if the object is deprecated or aliased, you will get the appropriate warning or the container object back.

API Chart

Param Chainable Details Throws Error?
register(containerName, obj) Yes Base function to call in order to register the object to a container. You cannot chain multiple registers, but you can chain other function of this one. No
addArgument(argument) Yes Chained onto register in order to add arguments to that specific container as "constructor" dependencies. No
addReference(reference) Yes Can be chained to add a reference to another object or another container. Unlike addArgument this function is designed to only pass in references to other objects or containers. Yes - If the container to be referenced doesn't exist.
setupSetter(setter, value) No Used to set up a reference to an object or container as a "optional" dependency. Yes - if the container to be used as the setter doesn't exist. Or If the value passed in is not a function.
setupGlobalSetters() No The arguments can be either setter, value or [[setter, value], ...]. Works the same way as setupSetter, accept this one injects each class fetched or resolved with the global setter assuming the obj in question contains the setter. Global setters can be overridden with setupSetter on a specific container. Yes - if the container to be used as the setter doesn't exist. Or if the value passed in is not a function.
fetch(name) No Fetches the container and builds the object based on all the dependencies including setters (global or not). No - If the container doesn't exist then we return the string representation back.
resolve(name, fn(obj){}) No Fetches the container and builds it based on the dependencies and setters, then passes an instance of the object to an event thats passed in. Yes - if the container to be resolved doesn't exist.
.container No Returns the container at the time of the call No
getGlobalSetters() No Returns all global setters at the time of call No
.compiler No Returns an instance of Compile No
compile() No Requires you to call: toyBox.compiler.compile(). Runs the compiler on the container creating a compiled array of objects No
registerCompilerPass(fn) No Requires you to call: toyBox.compiler.registerCompilerPass(). Allows you to register a compiler pass object that must contain the function: process which takes an argument that is the instance of Toy Box. Yes - if the object to be registered doesn't contain a process function.
deprecate(deprecatedContainerName, useInstead) No Deprecates a container name, so that when fetch is called on that container it will warn the developer to use the useInstead container. Yes - If neither container can be found.
alias(currentContainerName, newContainerName) No Aliases a container name to create a short cut of sorts. You can use this new name to fetch the container. Yes - If the aliased container cannot be found we fall back and search the main container. If that cannot be found we throw an error.

Special Notes:

For functions like reference, setupSetter and setupGlobalSetters the order is important when passing containers. All dependencies to be used as a setter or global setter must be registered first, the same thing does for containers that reference other containers. The "other container" must be registered first.