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
@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.
- Installation
- Getting started
- API
- Decorators
- Configuration
- In-dept
- Contributing
npm i --save @philips-software/odinDeclare 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
domainis astringthat 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 domainThis 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 InjectableThe 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
CustomProvidercan 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 InjectableThis 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 timeDeprecated, to be removed in a future major version. Use
@Injectablewith 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
@Injectwith 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
@Injectwith 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 instancestep 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
instantiatestep 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,@Injectand@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.