1

I have an Angular 9 (9.1.4) project and there is a custom factory provider for BASE_URL (the project is based on .NET Core Angular template)

// main.ts

export function getBaseUrl() {
  return document.getElementsByTagName('base')[0].href;
}

const providers = [
  { provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] }
];

platformBrowserDynamic(providers).bootstrapModule(AppModule)
  .catch(err => console.error(err));

If I want to use the BASE_URL in a class, its super easy, I can just ask for it to be injected in the constructor

export class SomeService {

  constructor(
    private _httpClient: HttpClient,
    @Inject('BASE_URL') private _baseUrl: string  // here we go
  ) { }

and it works as expected. So far so good...

I recently added an APP_INITIALIZER to the app, so I can load some configuration from server up-front. This is done by registering a provider with factory function, into which dependencies can also be injected.

However, the simple approach using @Inject does not work there for some reason:

// app.module.ts
@NgModule({
  // other stuff
  providers: [
    SomeService, // here the @Inject works
    {
      provide: APP_INITIALIZER,
      useFactory: (httpClient: HttpClient, @Inject('BASE_URL') baseUrl: string) => {
        // httpClient is OK, but baseUrl is undefined
        // app init implementation
      },
      deps: [HttpClient],
      multi: true
    }],


because baseUrl is undefined during runtime.

The only way I could get it to work was to inject the Injector from @angular/core:

{
  provide: APP_INITIALIZER,
  useFactory: (httpClient: HttpClient, injector: Injector) => {
    cont baseUrl = injector.get('BASE_URL'); // works, but deprecated
    // app init implementation
  },
  deps: [HttpClient, Injector],
  multi: true
}

but there the problem is the deprecation warning on get method (deprecated since v4.0.0).

What's the proper way to get such factory injection working?

1 Answer 1

4

It is possible to provide the desired value in the same way as you do it with HttpClient.

Since the value is a simple string a custom InjectionToken is needed:

base-url.ts (I used a separate file to avoid a circular dependency)

import { InjectionToken } from "@angular/core";

export const BASE_URL = new InjectionToken<string>('BaseUrl');

export function getBaseUrl() {
  return document.getElementsByTagName('base')[0].href;
}

app.module.ts

import { BASE_URL } from "../base-url";
// ...
  providers: [{
    provide: APP_INITIALIZER,
    useFactory: (httpClient: HttpClient, baseUrl: string) => {
      return () => {
        console.log(baseUrl);
        return Promise.resolve();
      };
    },
    deps: [HttpClient, BASE_URL], // <-- Here
    multi: true
  }],
// ...

Edit
Yes, actually, a string could be used as a token so adding it as a second value to deps array does a trick. The initial problem was in fact that it is not possible to decorate function's (not method's) arguments and that is why in case of injecting a factory deps array is used.
So, this works:

  providers: [{
    provide: APP_INITIALIZER,
    useFactory: (httpClient: HttpClient, baseUrl: string) => {
      return () => {
        console.log(baseUrl);
        return Promise.resolve();
      };
    },
    deps: [HttpClient, 'BASE_URL'], // <-- Here
    multi: true
  }],

But I would really advice use InjectionToken instead because it is safier. Please also note that the deprecation warning you've got for injector.get('BASE_URL'); relates to string tokens, since only the signature of get that accepts stings is depricated.

Sign up to request clarification or add additional context in comments.

2 Comments

yes, that would work, but is there a way to do it without creating my own token? aka. is there a way to inject it using originally provided string token 'BASE_URL' (similarly how its working in constructors)? main.ts is generated by template, so I wonder if i can directly use already provided token/provider somehow...
@yohny, I edited the answer adding an alternative option and a small explanation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.