DEV Community

Connie Leung
Connie Leung

Posted on

How to register providers in environment injector in Angular

Introduction

In this blog post, I describe how to register providers in environment injector in Angular. One way to create an environment injector is to use the ENVIRONMENT_INITIALIZER token. When I have several providers and they don't have to execute any logic during bootstrap, I can use makeEnvironmentProviders to wrap an array of providers to EnvironmentProviders. Moreover, EnvironmentProviders is accepted in environment injector and they cannot be used in components by accident.

My practice is to create a custom provider function that calls makeEnvironmentProviders internally. Then, I can specify it in the providers array in bootstrapApplication to load the application.

Use case of the demo

In this demo, AppComponent has two child components, CensoredFormComponent and CensoredSentenceComponent. CensoredFormComponent contains a template-driven form that allows user to input free texts into a TextArea element. Since the input is free text, it can easily contain foul language such as fxxk and axxhole.

The responsibility of the providers is to use regular expression to identify the profanity and replace the bad words with characters such as asterisks. Then, CensoredSentenceComponent displays the clean version that is less offensive to readers.

// main.ts

// ... omit import statements ...

const LANGUAGE = 'English';

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CensoredSentenceComponent, CensoredFormComponent],
  template: `
    <div class="container">
      <h2>Replace bad {{language}} words with {{character}}</h2>
      <app-censored-form (sentenceChange)="sentence = $event" />
      <app-censored-sentence [sentence]="sentence" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  language =  LANGUAGE;
  character = inject(MASKED_CHARACTER);
  sentence = '';
}

bootstrapApplication(App, {
  providers: [provideSanitization(LANGUAGE)],
}).then(() => console.log('Application started successfully'));
Enter fullscreen mode Exit fullscreen mode

provideSanitization function accepts language and calls makeEnvironmentProviders function to register the providers in an environment injector. When language is English, a service masks bad English words with characters. Similarly, a different service masks bad Spanish words when language is Spanish.

// censorform-field.component.ts

// ... import statements ...

@Component({
  selector: 'app-censored-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #myForm="ngForm">
      <div>
        <label for="sentence">
          <span class="label">Sentence: </span>
          <textarea id="sentence" name="sentence" rows="8" cols="45"
            [ngModel]="sentence"
            (ngModelChange)="sentenceChange.emit($event)">
          </textarea>
        </label>
      </div>
    </form>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CensoredFormComponent {
  sentence = '';

  @Output()
  sentenceChange = new EventEmitter<string>();
}
Enter fullscreen mode Exit fullscreen mode
//  censored-sentence.component.ts

// ... omit import statements ...

@Component({
  selector: 'app-censored-sentence',
  standalone: true,
  imports: [SanitizePipe],
  template: `
    <p>
      <label for="result">
        <span class="label">Cleansed sentence: </span>
        <span id="result" name="result" [innerHtml]="sentence | sanitize" ></span>
      </label>
    </p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush, 
})
export class CensoredSentenceComponent {
  @Input({ required: true })
  sentence!: string;
}
Enter fullscreen mode Exit fullscreen mode

SantizePipe is a standalone pipe that masks the bad words with characters, applies CSS styles according to options and renders the final HTML codes.

// sanitiaze.pipe.ts

// ...omit import statements...

@Pipe({
  name: 'sanitize',
  standalone: true,
})
export class SanitizePipe implements PipeTransform {
  sanitizeService = inject(SanitizeService);
  domSanitizer = inject(DomSanitizer);

  transform(value: string): SafeHtml {
    const html = this.sanitizeService.cleanse(value);
    return this.domSanitizer.bypassSecurityTrustHtml(html)
  }
}
Enter fullscreen mode Exit fullscreen mode

SanitizePipe injects SanitizeService and the concrete service is provided by provideSanitization based on the value of language parameter. I am going to show how to register providers in environment injector in the next section.

Define custom providers and bootstrap the application

First, I have to define some injection tokens in order to provide CSS styling options and the character to mask swear words.

// sanitization-options.interface.ts

export interface SanitizeOptions {
  isBold: boolean;
  isItalic: boolean;
  isUnderline: boolean;
  character?: string;
  color?: string;
}
Enter fullscreen mode Exit fullscreen mode
// sanitization-options.token.ts

import { InjectionToken } from "@angular/core";
import { SanitizeOptions } from "../interfaces/sanitization-options.interface";

export const SANITIZATION_OPTIONS = new InjectionToken<SanitizeOptions>('SANITIZATION_OPTIONS');
Enter fullscreen mode Exit fullscreen mode
// masked-character.token.ts

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

export const MASKED_CHARACTER = new InjectionToken<string>('MASKED_CHARACTER');
Enter fullscreen mode Exit fullscreen mode

Second, I have to define new services that identify English/Spanish swear words and replace them with chosen characters. Moreover, logic is performed to provide the correct service in the context of makeEnvironmentProviders.

// sanitize.service.ts

export abstract class SanitizeService {
  abstract cleanse(sentence: string): string;
}
Enter fullscreen mode Exit fullscreen mode

SanitizeService is an abstract class with a cleanse method to clean up the free texts. Concrete services extend it to implement the method and SanitizeService can also serve as an injection token.

// mask-words.service.ts

@Injectable()
export class MaskWordsService extends SanitizeService  {
  private badWords = [
    'motherfucker',
    'fuck',
    'bitch',
    'shit',
    'asshole',
  ];

  sanitizeOptions = inject(SANITIZATION_OPTIONS);
  styles = getStyles(this.sanitizeOptions);
  getMaskedWordsFn = getMaskedWords(this.sanitizeOptions);

  cleanse(sentence: string): string {
    let text = sentence;
    for (const word of this.badWords) {
      const regex = new RegExp(word, 'gi');
      const maskedWords = this.getMaskedWordsFn(word);

      text = text.replace(regex, `<span ${this.styles}>${maskedWords}</span>`);
    }

    return text;
  }
}
Enter fullscreen mode Exit fullscreen mode
// mask-spanish-words.service.ts

@Injectable()
export class MaskSpanishWordsService extends SanitizeService {
  private badWords = [
    'puta',
    'tu puta madre',
    'mierda',
  ];

  sanitizeOptions = inject(SANITIZATION_OPTIONS);
  styles = getStyles(this.sanitizeOptions);
  getMaskedWordsFn = getMaskedWords(this.sanitizeOptions);

  cleanse(sentence: string): string {
    let text = sentence;
    for (const word of this.badWords) {
      const regex = new RegExp(word, 'gi');
      const maskedWords = this.getMaskedWordsFn(word);

      text = text.replace(regex, `<span ${this.styles}>${maskedWords}</span>`);
    }

    return text;
  }
}
Enter fullscreen mode Exit fullscreen mode

MaskWordsService is responsible for getting rid of English swear words while MaskSpanishService is responsible for getting rid of Spanish swear words.

After doing the above steps, I can finally define provideSanitization provider function.

// language.type.ts
export type Language = 'English' | 'Spanish';
Enter fullscreen mode Exit fullscreen mode
// core.provider.ts

function lookupService(language: Language): Type<SanitizeService> {
  if (language === 'English') {
    return MaskWordsService;
  } else if (language === 'Spanish') {
    return MaskSpanishWordsService;    
  } 

  throw new Error('Invalid language');
}

export function provideSanitization(language: Language): EnvironmentProviders {
  return makeEnvironmentProviders([
    {
      provide: SANITIZATION_OPTIONS,
      useValue: {
        isBold: true,
        isItalic: true,
        isUnderline: true,
        color: 'rebeccapurple',
        character: 'X',
      }
    },
    {
      provide: SanitizeService,
      useClass: lookupService(language),
    },
    {
      provide: MASKED_CHARACTER,
      useFactory: () => 
        inject(SANITIZATION_OPTIONS).character || '*'      
    }
  ]);
}
Enter fullscreen mode Exit fullscreen mode

I register SANITIZATION_OPTIONS to bold, italic, and underline the X character in rebeccapurple color. SanitizeService case is a little tricky; when language is English, it is registered to MaskWordsService. Otherwise, SanitizeService is registered to MaskSpanishWordsService. When I call inject(SanitizeService), this provider determines the service to use. MASKED_CHARACTER provider is a shortcut to return the character in SANITIZATION_OPTIONS interface

const LANGUAGE = 'English';

bootstrapApplication(App, {
  providers: [provideSanitization(LANGUAGE)],
}).then(() => console.log('Application started successfully'));
Enter fullscreen mode Exit fullscreen mode

provideSanitization is complete and I include it in the providers array during bootstrap.

What if I use provideSanitization in a component?

In CensoredFormComponent, when I specify provideSanitization('Spanish') in providers array, error occurs. In a sense, it is a good thing because the component cannot pass a different value to the provider function to provide a different SanitizeService. Otherwise, when CensoredFormComponent injects SanitizeService and invokes cleanse method, results become unexpected

Type 'EnvironmentProviders' is not assignable to type 'Provider'.
Enter fullscreen mode Exit fullscreen mode
@Component({
  selector: 'app-censored-form',
  standalone: true,
  ..  other properties ...
  providers: [provideSanitization('Spanish')]   <-- Error occurs on this line
})
export class CensoredFormComponent {}
Enter fullscreen mode Exit fullscreen mode

The following Stackblitz repo shows the final results:

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

Top comments (4)

Collapse
 
vmohir profile image
Vahid Mohammadi • Edited

How can I use the inject function in provideSanitization? I want to inject some settings and configure those tokens as I provide them

Collapse
 
marekozw profile image
.

Hi @vmohir !

The short answer is you can't because provideSanitization() function is not being called in Angular's injection context. You can look it up here angular.dev/guide/di/dependency-in....

So something like this will not work:

function provideFeature(): EnvironmentProviders {
  const myDep = inject(MY_DEP_TOKEN);

  return makeEnvironmentProviders([
    { provide: SOME_TOKEN, useValue: 1 },
  ])
}
Enter fullscreen mode Exit fullscreen mode

You will end up with the following error: "NG0203: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext. Find more at angular.io/errors/NG0203"

Nevertheless, as @railsstudent has mentioned in her comment, you actually can inject dependency to your factory function using deps array. This is cool option but in my opinion it is easier to just use inject() function and skip deps array entirely.

So instead of:

function provideFeature(): EnvironmentProviders {
  return makeEnvironmentProviders([
    { 
       provide: SOME_TOKEN,
       deps: [MyDep],
       useFactory: (myDep: MyDep) => {
         // myDep is available here
         return 1;
       } 
    },
  ])
}
Enter fullscreen mode Exit fullscreen mode

you can just do:

function provideFeature(): EnvironmentProviders {
  return makeEnvironmentProviders([
    { 
       provide: SOME_TOKEN,
       useFactory: () => {
         const myDep = inject(MyDep); 
         // myDep is available here
         return 1;
       } 
    },
  ])
}
Enter fullscreen mode Exit fullscreen mode

In my opinion inject function has some cool advantages over deps array.

1 - Type inference

The inject function automatically infers type of injection token you are trying to inject. So if you have const MY_TOKEN = new InjectionToken<string>("MY_TOKEN"); and do something like this const value = inject(MY_TOKEN);, typescript will know that value is of type string;

2 - No string injection token support.

In Angular only 3 things can be used as an injection token: a class itself (can be abstract), an instance of InjectionToken<T> class and a string literal. So for example, you basically can do something like this: { provide: "TITLE", useValue: "My title" } and it will be a valid provider definition. In order to inject its value you just do @Inject("TITLE") public title: string in a constructor or deps: ["TITLE"] in another's provider definition.

This is of course prone to typos and errors. So Angular team decided to disallow string injection tokens in inject() function. If you try to do const value = inject("TITLE") you will get Argument of type 'string' is not assignable to parameter of type 'ProviderToken<unknown>'. typescript error. It is always better to use class or InjectionToken instance as an injection token of your providers.

3 - Injection flags (options) made easy

Sometimes you want your dependency to be configured in certain way, e.g. optional and skip current injector (just like in the article). If you decide to use deps array approach you will end up with this nested-array thing notation. Each deps array item becomes a tuple of injection options flags and the provider token itself:

{ 
  provide: SOME_TOKEN,
  deps: [
    [new Optional(), new SkipSelf(), MyDep],
  ],
  useFactory: (myDep: MyDep) => {
    // myDep is available here
    return 1;
  } 
},
Enter fullscreen mode Exit fullscreen mode

source: v17.angular.io/api/core/FactoryPro...
I looks kinda convoluted and is hard to read.

Using inject() is much more clean in my opinion:

{ 
  provide: SOME_TOKEN,
  useFactory: () => {
    const myDep = inject(MyDep, {
      optional: true,
      skipSelf: true,
    });
    // myDep is available here
    return 1;
  } 
},
Enter fullscreen mode Exit fullscreen mode

I hope I've understood your question correctly and this is helpful. If not, please let me know!

Also, huge thanks to @railsstudent - really great article! :)

Collapse
 
railsstudent profile image
Connie Leung

This is a cool way to use inject in useFactory.

I use inject in a constructor, field initialization, and in runInInjectionContext

Collapse
 
railsstudent profile image
Connie Leung

Add deps array, and inject the dependencies in useFactory function.

{
      provide: MASKED_CHARACTER,
     deps: [A, B], 
     useFactory: (a: A, b: B) =>  {
        // do something with a and b here
        return inject(SANITIZATION_OPTIONS).character || '*'      
     }
}
Enter fullscreen mode Exit fullscreen mode

More information can be found here: angular.dev/guide/di/dependency-in...