injector-next

--- Next gen Dependency Injector. Very similar to Angular injector. Extremely small ~1Kb

Usage no npm install needed!

<script type="module">
  import injectorNext from 'https://cdn.skypack.dev/injector-next';
</script>

README

Injector Next


Next gen Dependency Injector. Very similar to Angular injector. Extremely small ~1Kb

WARNING: It was meant to be used with Typescript. While it is possible to use with plain JS it is not advised and will be hard to use.

For Typescript, you need to have these two options turned on in tsconfig.json:

{
    "compilerOptions": {
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true
    }
}

Depends on reflect-metadata.

npm

WARNING: API is not yet final

Requirements

Technically it should have no requirements.


Installation

For yarn:

yarn add injector-next

For npm:

npm i injector-next

Docs


Imports:

// ES6 JS/Typescript style
import { Injector } from 'injector-next';

// require
const { Injector } = require('injector-next');

Class Decorators:

// Put this before the class you wish to be automatically injected as singleton
@Injectable()
// Alias to Injectable, does the same thing
@Service()

Parameter decorators:

// If you wish to provide your own factory, put this before the parameter
// Example below
@Token({ factory: () => any })

Basic usage:

@Injectable()
class ServiceA {
    a = 1;
}

@Injectable()
class ServiceB {
    constructor(
        // injector automatically figures out the class 
        // based on parameter type
        protected sa: ServiceA
    ) {}
}

const sb = Injector.resolve(ServiceB);

Manual registration:

class ExternalClass {
    a = 1;
}
// Notice that registration requires actual instance
Injector.register('unique-name', new ExternalClass());

// Now you can get it directly by name:
Injector.get('unique-name');

// Or by token itself
Injector.resolve(ExternalClass);

// You can also override it, calling register twice will result in error
// IMPORTANT: this will get rid of original instance
Injector.override('unique-name', new ExternalClass());

Custom resolver:

// External class that you cannot modify
class ServiceExternal {
    a = 1;
}

@Injectable()
class ServiceB {
    constructor(
        protected sa: any,
        protected cons: typeof console,
        protected dt: Date,
    ) {}
}

const customResolve = (token: any, idx: string) => {
    // Either check by index of parameter
    if (+idx === 0) {
        // this will make it basically factory that would spawn instance 
        // each time is resolved
        return new ServiceExternal();
    }
    if (+idx === 1) {
        return console;
    }
    // Or by class itself
    if (token === Date) {
        return new Date();
    }

    return null;
};

const sb = Injector.resolve(ServiceB, customResolve);

Result:

ServiceB {
  sa: ServiceExternal { a: 1 },
  cons: Object [console] {
    log: [Function: log],
    warn: [Function: warn],
    dir: [Function: dir],
    time: [Function: time],
    timeEnd: [Function: timeEnd],
    timeLog: [Function: timeLog],
    trace: [Function: trace],
    assert: [Function: assert],
    clear: [Function: clear],
    count: [Function: count],
    countReset: [Function: countReset],
    group: [Function: group],
    groupEnd: [Function: groupEnd],
    table: [Function: table],
    debug: [Function: debug],
    info: [Function: info],
    dirxml: [Function: dirxml],
    error: [Function: error],
    groupCollapsed: [Function: groupCollapsed],
    Console: [Function: Console],
    profile: [Function: profile],
    profileEnd: [Function: profileEnd],
    timeStamp: [Function: timeStamp],
    context: [Function: context]
  },
  dt: 2021-07-07T18:35:05.968Z
}

Custom tokens:

// create a factory class
const mapFactory = () => { return new Map(); }
// mark it as di factory (otherwise injector cannot distinguish 
// between actual class and factory
mapFactory.diFactory = true;

@Injectable()
class ServiceB {
    constructor(
        // Mark parameter directly
        @Token({ factory: mapFactory }) protected map: Map<any, any>
    ) {}
}

const sb = Injector.resolve(ServiceB);

Result (map is a factory):

ServiceB { map: Map(0) {} }

Custom token to provide non classes:

// create a factory class
const singletonInstance = {
    unnamed: 'object',
    that: 'has no constructor'
}
const configFactory = () => { return singletonInstance; }
configFactory.diFactory = true;

@Injectable()
class ServiceB {
    constructor(
        // Mark parameter directly
        @Token({ factory: configFactory }) 
        protected config: any
    ) {}
}

const sb = Injector.resolve(ServiceB);

Result (config is singleton):

ServiceB { config: { unnamed: 'object', that: 'has no constructor' } }

Injector as factory:

// create a factory class
const randFactory = () => { return Math.random(); }
randFactory.diFactory = true;

@Injectable()
class ServiceX {
    stable = Math.random();
}

@Injectable()
class Entity {
    constructor(
            protected serv: ServiceX,
            @Token({ factory: randFactory })
            protected rand: number
    ) {}
}

// Those will be just instanciated but not kept in registry
const e1 = Injector.resolve(Entity, null, true);
const e2 = Injector.resolve(Entity, null, true);
const e3 = Injector.resolve(Entity, null, true);

Result (serv is still a singleton but factory spawned random numbers each time):

Entity {
    serv: ServiceX { stable: 0.5692771742563438 },
    rand: 0.7034761836358194
}
Entity {
    serv: ServiceX { stable: 0.5692771742563438 },
    rand: 0.20460451477948371
}
Entity {
    serv: ServiceX { stable: 0.5692771742563438 },
    rand: 0.22173878210817932
}

Advanced usage:

WARNING modifying design:paramtypes directly may result in some odd behaviour if not done right.

interface CustomOptions {
    min: number;
    max: number;
    amount: number;
}

const CustomToken = (options: CustomOptions): ParameterDecorator => {
    return (target: Object, propertyKey: string | symbol, parameterIndex: number) => {
        // collect existing param types
        const tokens = Reflect.getMetadata('design:paramtypes', target) || [];

        // make new factory
        const factory = () => {
            const out = [];
            for (let i = 0; i < options.amount; i++) {
                out.push(Math.random() * (options.max - options.min) + options.min)
            }

            return out;
        };
        // mark it as factory
        factory.diFactory = true;
        
        tokens[parameterIndex] = factory;
        // redefine param types so we can check where is our namespace suppose to be injected
        Reflect.defineMetadata('design:paramtypes', tokens, target);
    };
};

@Injectable()
class ServiceB {
    constructor(
        @CustomToken({ min: 1, max: 10, amount: 3 }) 
        protected arr: number[],
        
        @CustomToken({ min: 10, max: 20, amount: 5 }) 
        protected arr2: number[],
        
        @CustomToken({ min: 100, max: 200, amount: 7 }) 
        protected arr3: number[],
    ) {}
}

const sb = Injector.resolve(ServiceB);

Result:

ServiceB {
  arr: [ 6.682922113497945, 2.1919589707056892, 3.3588555893813377 ],
  arr2: [
    15.002033575190106,
    11.684026606562002,
    16.84351565375917,
    16.820905407384693,
    16.210885789832872
  ],
  arr3: [
    166.39310836720702,
    130.8052358138108,
    185.8191399183811,
    164.22233809832989,
    131.44972544841204,
    186.65161324868083,
    110.1801704311695
  ]
}