I want to create a decorator that injects and uses a service
function MyDecorator(){
return function(target: any, prop: string){
// error: inject() must be called from an injection context
let service = inject(MyService);
// trying to call it inside the constructor
// but it seems that the constructor already called
let constructor = target.constructor;
target.constructor = function(...args: any[]){
constructor.apply(this, args);
service = inject(MyService)
}
// trying to call it inside ngOnInit
// will fail according to the docs because it is too late
// https://angular.dev/errors/NG0203
}
}
using runInInjectionContext()
also impossible because it needs something to be injected
return function(target: any, prop: string){
// inject environmentInjector somehow
runInInjectionContext(this.environmentInjector, () => {
service = inject(MyService);
});
}
Usage:
@Component(...)
class MyComp{
@Mydecorator() myProp: string
}
I don't want the consumer of this decorator to make any special changes to his component.
update:
solved initially by using AppInjector
export let AppInjector: Injector;
@Component(...)
export class AppComponent{
constructor(private injector: Injector) {
AppInjector = this.injector;
}
}
though it still gives an error ASSERTION ERROR: Unexpected state: hydrating an <ng-container>, but no hydration info is available. [Expected=> number === object <=Actual]
, but till now I can inject the service.
Using runInInjectionContext is what you need, you were already there, just create an Injector and pass it to the function like this:
function MyDecorator() {
return function (target: any, prop: string) {
const injector = Injector.create({ providers: [MyService] }); //Creates the injector, provide the services that you need
runInInjectionContext(injector, () => {
const service = inject(MyService);
Object.defineProperty(target, prop, {
get: function () {
return service;
},
});
});
};
}
That is it, if you want to improve the decorator just use some reflect-metadata.
Update tsconfig
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}
Update the decorator to inject any type:
function MyDecorator() {
return function (target: any, prop: string) {
const injector = Injector.create({ providers: [MyService] });
const type = Reflect.getMetadata('design:type', target, prop);
runInInjectionContext(injector, () => {
const service = inject(type); //now can inject any provided service
Object.defineProperty(target, prop, {
get: function () {
return service;
},
});
});
};
}
This is the example: https://stackblitz.com/edit/stackblitz-starters-qbjz1j?file=src%2Fmain.ts
UPDATE: The following is a nasty solution, but can work to inject dependencies in NodeInjector, if your services are provided at root, you only need the APP_INITIALIZER implementation.
The following code target.constructor.ɵcmp.providersResolver
can be avoided if your services are provided at root.
function MyDecorator() {
return function (target: any, prop: string) {
const type = Reflect.getMetadata('design:type', target, prop);
Object.defineProperty(target, prop, {
get: function () {
const injector: Injector = Reflect.getMetadata(
InjectorSymbol,
InjectorEnvironment
);
let service = null;
try {
//Nasty Solution to access the providers of the NodeInjector.
//Implements a catch error, we do not neet to execute the function again, just get the providers
target.constructor.ɵcmp.providersResolver(
target.constructor.ɵcmp,
(providers: any[]) => {
service = Injector.create({ providers, parent: injector }).get(
type
);
return providers;
}
);
} catch (_) {}
return service;
},
});
};
}
We use this for the metadata.
const InjectorSymbol = Symbol('InjectorSymbol');
class InjectorEnvironment {}
The following code registers the environment injector in the metadata, so the property decorator can access to it.
bootstrapApplication(App, {
providers: [
provideHttpClient(),
{
provide: APP_INITIALIZER,
deps: [Injector],
multi: true,
useFactory: (injector: Injector) => {
return () => {
Reflect.defineMetadata(InjectorSymbol, injector, InjectorEnvironment);
return Promise.resolve();
};
},
},
],
});