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.
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:
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:
- The
@Injectable
decorator 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.@Injectable
with options will requireoptions.name
.@Injectable
without any arguments will be removed.
- The
@Inject
decorator 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.@Inject
with options will requireoptions.name
.@Inject
with a string will be removed, use@Inject
with options instead.@Inject
without any arguments will be removed, use@Inject
with options instead.
- The
@Configuration
decorator and itsconfiguration
will be removed and thestrict
mode behavior will be the default. Less magic is magic.
- Installation
- Getting started
- API
- Decorators
- Configuration
- In-dept
- Contributing
npm i --save @philips-software/odin
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.
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.
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
domain
is astring
that represents a chain ofbundles
, hierarchically organized, with just a single level, or multiple levels separated by forward slashes (/
).
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' });
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
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
The CustomProvider
is used to provide values for injects that are not provided by the container
. Resolvers can be registered into the CustomProvider
, which will be called to resolve identifiers when the container is not able to. An instance of CustomProvider
can be shared with multiple containers, instead of creating duplicates. Sharing or not, beware of resource deallocation and leaks.
The
CustomProvider
can be used directly, or be extended into your own custom provider.
This snippet shows how to create a CustomProvider
and supply it to a container
:
import { odin, CustomProvider } from '@philips-software/odin';
// creates a new provider
const provider = new CustomProvider();
// creates a new container, supplying the provider
const container = odin.container('domain', provider);
This method is used to register a resolver to resolve a name or identifier when the container
is not able to resolve it itself. It receives a name or identifier and an instance of ValueResolver
(itself or extended).
This snippet shows how to provide a counter that automatically increments itself every time it's injected:
import { odin, CustomProvider, ValueResolver } from '@philips-software/odin';
// creates a new resolver
const resolver = new ValueResolver(() => {
// ...
});
// creates a new provider and registers the resolver with the provider
const provider = new CustomProvider();
provider.register('nameOrIdentifier', resolver);
// creates a new container, supplying the provider
const container = odin.container('domain', provider);
This is the base of all resolvers. Any place that accepts a resolver will accept a ValueResolver
or an extension of it. Its implementation is as simple as it gets: it accepts a resolver function in the constructor and where every time a resolution is requested, the resolver function is called to resolve the value.
This method is used to request a resolution, returning the resolved value.
This snippet shows a resolver of a counter that automatically increments itself every time it's injected:
import { ValueResolver } from '@philips-software/odin';
let autoIncrementCounter = 0;
const resolver = new ValueResolver(() => {
return ++autoIncrementCounter; // called 3 times
});
resolver.get(); // returns 1 (resolved)
resolver.get(); // returns 2 (resolved)
resolver.get(); // returns 3 (resolved)
This method optionally accepts an instance of an injectable as the argument, which will be forwarded to the resolver function. When the resolution is requested by a container
while injecting the value into an injectable, the instance will be the instance into which the value being resolved will be injected.
This snippet shows a resolver that uses the instance argument:
import { odin, CustomProvider, ValueResolver } from '@philips-software/odin';
@Injectable({ domain: 'domain' })
class Injectable {
@Inject({ name: 'nameOrIdentifier' })
value;
}
// creates a new resolver
const resolver = new ValueResolver((instance) => {
return instance;
});
// creates a new provider and registers the resolver with the provider
const provider = new CustomProvider();
provider.register('nameOrIdentifier', resolver);
// creates a new container, supplying the provider
const container = odin.container('domain', provider);
// requesting the resolution without being injected by a parent
container.provide('nameOrIdentifier', true); // returns undefined
// requesting the resolution during injection within a parent
const instance = container.provide(Injectable.name, true); // returns instance of Injectable
instance.value // instance of Injectable
This resolver is used to speed up resolution by resolving only once and caching the value. The resolver function will be called only once, during the first resolution request. Any resolution requested after the first will be resolved to the cached value.
This snippet shows the effect this resolver would have on a counter that automatically increments itself every time it's injected:
import { FinalValueResolver } from '@philips-software/odin';
let autoIncrementCounter = 0;
const resolver = new FinalValueResolver(() => {
return ++autoIncrementCounter; // called only once
});
resolver.get(); // returns 1 (resolved)
resolver.get(); // returns 1 (cached)
resolver.get(); // returns 1 (cached)
Deprecated, to be removed in a future major version. Strict mode will be the default and only behavior.
This decorator is used to apply a configuration to odin
before other decorators are evaluated. For it to work, this decorator needs to be evaluated before any other decorator (in the import/evaluation tree). The options can be seen in the ConfigurationOptions
interface.
This snippet shows how to apply a configuration using the decorator:
import { Configuration } from '@philips-software/odin';
@Configuration({ strict: true })
class OdinConfiguration { }
This decorator can only decorate injectable classes.
It registers the injectable in a bundle
using the class name.
See: Instantiation lifecycle.
This is the recommended use of this decorator. It accepts options that configure the registration behavior. The options can be seen in the InjectableOptions
interface.
This snippet shows multiple variations of options and their effect:
@Injectable({ domain: 'sample' })
class StandardInjectable { } // registerd in the sample domain, new instance every time
@Injectable({ name: 'Custom' })
class NamedInjectable { } // registered with the supplied name
@Injectable({ singleton: true })
class SingletonInjectable { } // registered in the root domain, same instance every time
Deprecated, to be removed in a future major version. Use
@Injectable
with options instead.
This variation is a syntax sugar for @Injectable
with options. When no argument is provided, the decorator will use the class name as the options.name
value.
This snippet shows how the class field name could be used as the name or identifier:
@Injectable
class Injectable { }
This decorator can only decorate class fields of injectables. Can be used multiple times per class.
Learn more about its purpose in the container.provide
method documentation.
See: Instantiation lifecycle.
This is the recommended use of this decorator. It accepts options that configure the inject behavior. The options can be seen in the InjectOptions
interface.
This snippet shows multiple variations of options and their effect:
@Injectable
class Injectable { }
@Injectable
class Usage {
@Inject({ name: 'Injectable' })
standardField; // provided upon first access
@Inject({ name: 'Injectable', eager: true })
eagerField; // provided after construction and before the initializer is called
@Inject({ name: 'Injectable', optional: true })
optionalField; // not provided if not found in the container
}
Deprecated, to be removed in a future major version. Use
@Inject
with options instead.
This variation is a syntax sugar for @Inject
with options. When provided with a simple string
as its argument, the @Inject
decorator will use the string
as the options.name
value.
This snippet shows how the name or identifier can be supplied directly to the decorator:
@Injectable
class Injectable { }
@Injectable
class Usage {
@Inject('Injectable')
injectableField; // matches Injectable
}
Deprecated, to be removed in a future major version. Use
@Inject
with options instead.
This variation is a syntax sugar for @Inject
with options. When no argument is provided, the decorator will use the class field name as the options.name
value.
This snippet shows how the class field name could be used as the name or identifier:
@Injectable
class Injectable { }
@Injectable
class Usage {
@Inject
injectable; // matches Injectable
}
This decorator can only decorate class methods of injectables. Only one per class is allowed.
When a new injectable is created, its default constructor is called before injects are provided. The initializer method is called right after the injects are provided and available for use.
See: Instantiation lifecycle.
This snippet shows how to use this decorator to access injected fields after they are initialized:
@Injectable
class Injectable { }
@Injectable
class Usage {
@Inject({ name: 'Injectable' })
injectableField;
constructor() {
console.log(this.injectableField); // null
}
@Initializer
initialize(): void {
console.log(this.injectableField); // instance of Injectable
}
}
Deprecated, to be removed in a future major version. Strict mode will be the default and only behavior.
Behavior can be customized through this configuration.
Check the @Configuration
decorator to understand how to configure it.
Deprecated, to be removed in a future major version. Strict mode will be the default and only behavior.
The strict
mode indicates whether dependency injection is case-sensitive or not. It means that a name could be repeated when used with a different case, as seen below:
@Injectable
class Injectable { }
@Injectable
class INJECTABLE { }
@Injectable
class Usage {
@Inject({ name: 'Injectable' })
injectable1;
@Inject({ name: 'INJECTABLE' })
injectable2;
}
To better understand and use this library, or any other, it's very important to be aware of how things happen under the hood. Thus, being aware of the odin
lifecycle and what exactly this on-demand feature means, is a good idea.
Represents how the decorators are evaluated and used. The code is evaluated by the JavaScript engine (browser or node), and every time an @Injectable
decorator is found, it's registered in odin
. In-class decorators (like @Inject
and @Initializer
) are evaluated along with its respective injectable, and used to configure its behavior during other lifecycles.
flowchart TB
evaluation[code evaluation by the engine] --> decorator[found a decorator \n register in odin]
decorator --> validation[odin validates injectable]
validation --> available[injectable registered \n available for use]
available --> ready([application ready])
available -.-> decorator
Represents how the application consumes injectable instances during its execution.
The application is responsible for retrieving containers
from odin
, which are then used to retrieve instances of injectables and/or values previously registered in bundles
or providers
, while considering their scope and managing their lifecycle. There is no rule regarding the number of active containers
at the same time during the application execution and each container is isolated and never interacts with other containers
.
The
container provides instance
step is detailed in Provisioning lifecycle.
flowchart TB
ready[[application ready]] --> run[container is created \n application runs]
run --> execution[normal execution]
execution --> requestInjectable[requests injectable instance]
requestInjectable --> provide[container provides instance]
provide --> execution
execution -.-> requestToDiscard[requests to discard a singleton instance]
requestToDiscard -.-> discard[odin discards singleton instance]
discard -.-> execution
Represents the logic flow used in container.provide
.
The
instantiate
step is detailed in Instantiation lifecycle.
flowchart TB
provide[[container.provide]] --> cached{is cached?}
cached -->|yes| return{should resolve?}
cached -->|no| exists{in container? \n resolution}
return -->|yes| returnInstance([returns instance/value])
return -->|no| returnResolver([returns resolver])
exists -->|yes| instantiate[instantiate]
exists -->|no| provider{in custom provider?}
instantiate --> singleton{is singleton?}
provider -->|yes| return
provider -->|no| returnUndefined([returns undefined])
singleton -->|yes| cache[add to the cache]
singleton -->|no| return
cache --> return
Represents placement within domains and the flow of resolution through the hierarchy.
Injectables can only be found within the hierarchy by searching through the parent domain when not found in the child domain, recursively. A domain that is not directly in the hierarchy line (like a sibling or an uncle), cannot be reached. The hierarchy is only searched upwards and never downwards.
flowchart
subgraph R [root domain]
direction BT
RI(injectables)
subgraph A1 [domain A1]
A1I(injectables)
subgraph A2 [domain A2]
A2I(injectables)
end
end
subgraph B [domain B]
BI(injectables)
end
end
A1I -->|check parent| RI
BI -->|check parent| RI
A2I -->|check parent| A1I
A1I x-.-x|not in hierarchy| BI
A2I x-.-x|not in hierarchy| BI
Represents how an injectable is instantiated.
An inject will only be resolved when necessary. When an instance is created by odin
, it doesn't eagerly resolve the injections. This strategy is adopted to reduce as much of the unnecessary creation of resources as possible. By default, all injections are lazy, but this behavior can be changed through in a case-by-case basis through @Inject
with options.
See:
@Injectable
,@Inject
and@Initializer
.
flowchart TB
instantiation[[Injectable instantiation]] --> invokeConstructor[invoke class constructor]
invokeConstructor --> inject[bind inject to field]
inject --> eager{is eager?}
eager -->|yes| resolve{in container?}
eager -->|no| bindAccessor[bind get accessor to field]
resolve --> bindInstance[bind resolved instance/value to field]
bindAccessor --> invokeInitializer[invoke initializer]
bindInstance --> invokeInitializer
invokeInitializer --> ready[instance ready for use]
invokeInitializer -.-> fieldAccess
subgraph R [normal instance usage]
ready -.-> fieldAccess[field access]
fieldAccess --> firstAccess{first access?}
firstAccess -->|yes| bindInstanceUponFirstUse([bind resolved instance/value\nif not already resolved])
firstAccess -->|no| secondaryAccess([use already bound instance/value])
bindInstanceUponFirstUse --> ready
secondaryAccess --> ready
end
See our contribution guide.