Home

Awesome

phantomdi

Version CircleCI

phantomdi is a no-boilerplate DI framework for classes and functions which can optionally leverage typescript-rtti.

import { injector, provide } from 'phantomdi';
import { reify } from 'typescript-rtti';

interface Foobar { 
    version : number;
}

class A {
    constructor(readonly foobar : Foobar) {

    }

    get version() { return this.foobar.version; }
}

let a = injector([ provide(reify<Foobar>, () => ({ version: 123 })), provide(A) ]).provide(A)
expect(a.version).to.equal(123);

Functions:

import { injector, provide } from 'phantomdi';

class A { foo = 123 }
class B { bar = 321 }

function foobar(a : A, b : B) {
    return a.foo + b.bar;
}

expect(injector([ provide(A), provide(B) ]).invoke(globalThis, foobar))
    .to.equal(123 + 321);

Optional:

import { injector, provide } from 'phantomdi';

class A { foo: 123 }
class B { bar: 321 }

function foobar(a : A, b? : B) {
    return a.foo + (b?.bar ?? 555);
}

expect(injector([ provide(A) ]).invoke(globalThis, foobar))
    .to.equal(123 + 555);

Initializers:

import { injector, provide } from 'phantomdi';

class A { foo: 123 }
class B { bar: 321 }

function foobar(a : A, b = new B(555)) {
    return a.foo + (b?.bar);
}

expect(injector([ provide(A) ]).invoke(globalThis, foobar))
    .to.equal(123 + 555);

Heirarchical injection:

import { injector, provide } from 'phantomdi';

class A { 
    constructor(readonly foo = 123) {
    }
}

class B {
    bar = 321;
}

let parent = injector([ provide(A), provide(B) ]);
let injector = injector([ provide(A, () => new A(555))], parent)

expect(injector.provide(A).foo).to.equal(555);
expect(injector.provide(B).bar).to.equal(321);

Alterations:

import { provide, alter, injector } from 'phantomdi';

class A {
    bar = 123;
    foo() {
        return 'original';
    }
}

let i = injector([ provide(A), alter(A, {
    beforeFoo() {
        console.log(`foo() is about to run`);
    }

    afterFoo() {
        console.log(`foo() is finished running`);
    }

    aroundFoo(foo : () => string) {
        return function () {
            return `around(${foo.call(this)})`
        }
    }

})]);

let a = i.provide(A);

expect(a.bar).to.equal(123);
expect(a.foo()).to.equal('around(original)');

API

The injector() function (and the Injector constructor) accept an array of providers. Each provider is a tuple of two values: a token and a function which provides the value for that token.

let i = injector([
    ['foo', () => 123],
    ['bar', () => 321]
]);

expect(i.provide('foo')).to.equal(123);
expect(i.provide('bar')).to.equal(321);

The provide() function provides syntactic sugar for defining these:

let i = injector([
    provide('foo', () => 123),
    provide('bar', () => 321)
]);

expect(i.provide('foo')).to.equal(123);
expect(i.provide('bar')).to.equal(321);

Provider functions are also subject to dependency injection:

let i = injector([
    provide(Number, () => 123),
    provide('bar', (num : number) => num + 1)
])

expect(i.provide('bar').to.equal(124));

Calling provide() with a class constructor will provide that class using its constructor as the token:

class Foo { }

injector([ provide(Foo) ]);

This is done using the construct(constructor) function. It returns a provider function which constructs the given class using the dependency injector.

You can provide a class token using another class:

class Foo { }
class Bar extends Foo { }

injector([ provide(Foo, Bar) ]);

You can also invoke a function by injecting its parameters based on the available metadata:

class Foo { bar = 123; }

let result = injector([ provide(Foo) ]).invoke((foo : Foo) => foo.bar);

expect(result).to.equal(123);

In addition to parameter injection, you can do property injection:

class Foo {
    baz = 123;
}

class Bar {
    @Inject() foo : Foo;
}

let result = inject([ provide(Foo), provide(Bar) ]).provide(Bar).foo.baz;

expect(result).to.equal(123);

You can define an onInjectionCompleted() method which will get called after all injection is resolved:

class Foo {
    baz = 123;
}

class Bar {
    @Inject() foo : Foo;

    onInjectionCompleted() {
        expect(this.foo.baz).to.equal(123);
    }
}

let bar = inject([ provide(Foo), provide(Bar) ]).provide(Bar);

If you specify that a parameter or property is optional, it will be treated as optional. If you specify an initializer for a property or parameter it will automatically be considered "optional", with its value set automatically to the initializer.

Using without typescript-rtti

When using typescript-rtti, no decorators are required, the library will automatically determine all relevant Typescript types and do the right thing. However you can still use the library without it- the library provides @Injectable() along with @Inject() and @Optional(), and it supports emitDecoratorMetadata:

@Injectable()
class Foo {
    baz = 123;
}

@Injectable()
class Bar {
    constructor(readonly foo : Foo) {
    }
}

let result = inject([ provide(Foo), provide(Bar) ]).provide(Bar).foo.baz;

expect(result).to.equal(123);

As with other dependency injection libraries, technically any decorator on the class being injected is fine, the specific use of @Injectable() is not enforced.

Alterations

Alterations are special providers. Use alter(token, provider) to define an alteration provider. The provider function is invoked in a special child injector where token is already provided, and the provider is expected to return a new value for token. An alteration provider usually uses Proxy to modify the value injected for token in some way, but it could also completely replace the object.

You can also pass a special alteration definition object (Alteration<T>) to alter() which will create the Proxy for you. That definition supports adding functions before (ie beforeMethod()), after (ie afterMethod()), and around (ie aroundMethod()) the original function value. You can completely replace the method by providing a function property with the same name as the method (ie method())