angulardependency-injection

Call inject() inside a property decorator


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.


Solution

  • 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();
            };
          },
        },
      ],
    });