Odin
On-demand dependency injection for JavaScript.
Install / Use
/learn @philips-software/OdinREADME
odin
Stands for on-demand dependency injection, enables a lazy loading pattern in JavaScript.
Dependency injection is a technique used to reduce concern about object creation and lifecycle. When delegating resource management to a dependency injection engine, it's possible to build a flexible coupling among resources.
Resources are only instantiated when necessary. When a class is instantiated, not necessarily its dependencies will. By providing dependencies at the last possible moment, it's possible to save computational resources, improving performance and decreasing memory footprint.
Disclaimers
Decorators
Since v3, odin uses the new stage 3 decorators released with TypeScript v5. Ensure your build tool outputs code in a modern version of ECMAScript that supports this version of decorators.
See:
- https://arai-a.github.io/ecma262-compare/?pr=2417
- https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators
In v1 and v2 (legacy versions), odin used the experimental stage 2 decorators proposal from TC39. Since that implementation of decorators is not natively supported by browsers or node, projects using odin had to rely on babel and its decorators polyfill (in legacy mode).
See:
- https://tc39.es/proposal-decorators
- https://www.typescriptlang.org/docs/handbook/decorators
- https://babeljs.io
- https://babeljs.io/docs/en/babel-plugin-proposal-decorators
Roadmap
Breaking changes planned for future major versions
- The
@Injectabledecorator will no longer use the class name to register the injectable, which will instead have to be explicitly supplied throughoptions.name. This allows build tools to change the class name during optimization and minification.@Injectablewith options will requireoptions.name.@Injectablewithout any arguments will be removed.
- The
@Injectdecorator will no longer use the class field name to register the injection. The wanted name will instead have to be explicitly supplied throughoptions.name. This allows build tools to change the class field name during optimization and minification.@Injectwith options will requireoptions.name.@Injectwith a string will be removed, use@Injectwith options instead.@Injectwithout any arguments will be removed, use@Injectwith options instead.
- The
@Configurationdecorator and itsconfigurationwill be removed and thestrictmode behavior will be the default. Less magic is magic.
Documentation
- Installation
- Getting started
- API
- Decorators
- Configuration
- In-dept
- Contributing
Installation
npm i --save @philips-software/odin
Getting started
Declare an injectable:
import { Injectable } from '@philips-software/odin';
@Injectable
class InjectableExample {
sayHi() {
console.log('We rode here in their minds, and we took root.');
}
}
Inject the injectable into another injectable:
import { Inject, Injectable } from '@philips-software/odin';
@Injectable
class UsageExample {
@Inject({ name: InjectableExample.name })
injectableExample;
run() {
this.injectableExample.sayHi();
}
}
Use odin to instantiate the injectable, which will inject its dependencies automagically:
import { odin } from '@philips-software/odin';
// creates a container based on the root bundle
// the container provides instances and dependencies
const container = odin.container();
// provides an instance of UsageExample
const usageExample = container.provide(UsageExample.name, true);
// uses the provided instance, which will inject and use its dependency
usageExample.run(); // We rode here in their minds, and we took root.
API
The exported odin core instance works like a facade object that exposes methods that can be used to manage injectables using bundles, and to provide/resolve injectables using containers.
This instance holds the root bundle, which is managed by odin itself. This instance a unique singleton, so everything that is registered using it will be available to the entire application.
Domain
The domain is used to identify bundles within a hierarchy.
Domains are useful to reduce coupling among different parts of the application by splitting it into smaller parts, making each part of application responsible for managing its own context-bound injectables. Injectables registered within a domain will only be available within the domain itself and its child domains, and not by parent or sibling domains.
Check the Resolution lifecycle.
A
domainis astringthat represents a chain ofbundles, hierarchically organized, with just a single level, or multiple levels separated by forward slashes (/).
Bundle
The bundle is used to manage the registration of injectables, working like a hierarchy of dictionaries. Injectables registered within a bundle will only be available within the bundle itself and its child bundles, and not by parent or sibling bundles.
When using Odin's Injectable decorator, the injectables are registered into the bundles automatically.
This snippet shows how to register an injectable manually:
import { odin } from '@philips-software/odin';
class Injectable { }
// gets odin's root bundle
const bundle = odin.bundle(); // accepts a domain
// registers the injectable
bundle.register(Injectable, { name: 'Injectable' });
Container
The container is used to provide dependencies and/or values by resolving injectables registered into a bundle or one of its parents or custom providers.
By default, odin containers can only provide instances of class, so the values provided by the containers are typically instances of those classes. However, odin also offers the possibility of implementing a CustomProvider to handle specific scenarios, when injecting another type of value is required, for example.
This snippet shows how to create/obtain a container:
import { odin } from '@philips-software/odin';
class Injectable { }
// gets odin's root bundle
const bundle = odin.bundle(); // accepts a domain
// registers the injectable
bundle.register(Injectable, { name: 'Injectable' });
// creates a new container based on odin's root bundle
const container = odin.container(); // accepts a domain
provide(nameOrIdentifier: string, resolve: boolean): Instance | Resolver | undefined
This method provides an instance of the injectable matching the nameOrIdentifier argument, or a resolver for it, depending on the resolve argument. Each time it's called, a new resolver is created, but a new instance will only be created if the injectable is not a singleton (or if the singleton hasn't yet been created or has already been discarded).
When a new instance is created, its dependencies (declared using the @Inject decorator) will be bound to an accessor that will provide their value upon their first access (lazy inject by default). If a dependency is declared as eager, it will be bound to its provided value during instantiation.
See: Provisioning lifecycle.
This snippet shows how to provide an instance of a previously registered injectable using the container:
// provides an instance of the previously registered injectable
container.provide(Injectable.name, true); // returns instance of Injectable
CustomProvider
The CustomProvider is used to provide values for injects that are not provided by the container. Resolvers can be registered into the `CustomPro
