Skip to content

Commit

Permalink
fix(create-injection-token): allow provideFn to accept factory (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
nartc authored Oct 7, 2023
1 parent 95ffa3d commit 55f31b3
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Injector, inject } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { createInjectionToken } from './create-injection-token';
import {
createInjectionToken,
createNoopInjectionToken,
} from './create-injection-token';

describe(createInjectionToken.name, () => {
describe('given root injection token', () => {
Expand Down Expand Up @@ -115,5 +118,65 @@ describe(createInjectionToken.name, () => {
expect(values).toEqual([1, 2]);
});
});

it('when pass a factory to provide then return correct value with injected dep', () => {
const [injectDepFn, provideDepFn] = createInjectionToken(() => 5);
TestBed.configureTestingModule({
providers: [
provideDepFn(),
provideFn(),
provideFn(() => injectDepFn()),
],
}).runInInjectionContext(() => {
const values = injectFn();
expect(values).toEqual([1, 5]);
});
});
});

describe('given injection token with function as value', () => {
const [injectDepFn, provideDepFn] = createInjectionToken(() => 5);
const [injectFn, provideFn] = createInjectionToken(
() => () => 1 as number,
{ multi: true }
);

it('then provide correct value when pass in a fn', () => {
TestBed.configureTestingModule({
providers: [
provideDepFn(),
provideFn(),
// NOTE: this is providing the function value as-is
provideFn(() => 2, true),
// NOTE: this is providing the function as a factory
provideFn(() => () => injectDepFn(), false),
],
}).runInInjectionContext(() => {
const fns = injectFn();
const values = fns.map((fn) => fn());
expect(values).toEqual([
1, // initial fn returning 1
2, // the function value as-is returning 2
5, // the function via factory returning the dep value 5
]);
});
});
});
});

describe(createNoopInjectionToken.name, () => {
describe('given an injection token', () => {
const [injectFn, provideFn] = createNoopInjectionToken<number, true>(
'noop',
{ multi: true }
);
it('then work properly', () => {
TestBed.configureTestingModule({
providers: [provideFn(1), provideFn(() => 2)],
}).runInInjectionContext(() => {
const values = injectFn();
expect(values).toEqual([1, 2]);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import {
Host,
InjectionToken,
Injector,
Optional,
Self,
SkipSelf,
inject,
runInInjectionContext,
type EnvironmentProviders,
type FactoryProvider,
type Host,
type InjectOptions,
type Injector,
type Optional,
type Provider,
type Self,
type SkipSelf,
type Type,
} from '@angular/core';
import { assertInjector } from 'ngxtension/assert-injector';
Expand Down Expand Up @@ -67,13 +67,23 @@ type InjectFn<TFactoryReturn> = {
): TFactoryReturn | null;
};

export type CreateInjectionTokenReturn<TFactoryReturn> = [
type ProvideFn<
TNoop extends boolean,
TFactoryReturn,
TReturn = TFactoryReturn extends Array<infer Item> ? Item : TFactoryReturn
> = (TNoop extends true
? (value: TReturn | (() => TReturn)) => Provider
: () => Provider) &
(TReturn extends Function
? (value: TReturn | (() => TReturn), isFunctionValue: boolean) => Provider
: (value: TReturn | (() => TReturn)) => Provider);

export type CreateInjectionTokenReturn<
TFactoryReturn,
TNoop extends boolean = false
> = [
InjectFn<TFactoryReturn>,
(
value?: TFactoryReturn extends Array<infer TReturn>
? TReturn
: TFactoryReturn
) => Provider,
ProvideFn<TNoop, TFactoryReturn>,
InjectionToken<TFactoryReturn>
];

Expand All @@ -99,10 +109,22 @@ function createProvideFn<
opts: CreateProvideFnOptions<TFactory, TFactoryDeps> = {}
) {
const { deps = [], multi = false, extraProviders = [] } = opts;
return (value?: TValue) => {
return (value?: TValue | (() => TValue), isFunctionValue = false) => {
let provider: Provider;
if (value) {
provider = { provide: token, useValue: value, multi };
// TODO: maybe this can be made better
const factory =
typeof value === 'function'
? isFunctionValue
? () => value
: value
: () => value;

provider = {
provide: token,
useFactory: factory,
multi,
};
} else {
provider = {
provide: token,
Expand Down Expand Up @@ -182,7 +204,7 @@ createInjectionToken is creating a root InjectionToken but an external token is
token,
factory,
opts as CreateProvideFnOptions<TFactory, TFactoryDeps>
),
) as CreateInjectionTokenReturn<TFactoryReturn>[1],
token,
];
}
Expand All @@ -195,7 +217,32 @@ createInjectionToken is creating a root InjectionToken but an external token is
token,
factory,
opts as CreateProvideFnOptions<TFactory, TFactoryDeps>
),
) as CreateInjectionTokenReturn<TFactoryReturn>[1],
token,
];
}

export function createNoopInjectionToken<
TValue,
TMulti extends boolean = false,
TOptions = Pick<
CreateInjectionTokenOptions<() => void, []>,
'extraProviders'
> &
(TMulti extends true ? { multi: true } : Record<string, never>)
>(description: string, options?: TOptions) {
type TReturn = TMulti extends true ? Array<TValue> : TValue;

const token =
(options as CreateInjectionTokenOptions<() => void, []>)?.token ||
new InjectionToken<TReturn>(description);
return [
createInjectFn(token) as CreateInjectionTokenReturn<TReturn, true>[0],
createProvideFn(
token,
() => null!,
(options || {}) as CreateProvideFnOptions<() => void, []>
) as CreateInjectionTokenReturn<TReturn, true>[1],
token,
] as CreateInjectionTokenReturn<TReturn, true>;
}

0 comments on commit 55f31b3

Please sign in to comment.