How to register providers in environment injector in Angular
Tags: angular, webdev, tutorial
Published: 2023-10-19 13:36:14

##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.

```typescript
// 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'));
```

`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.   

```typescript
// 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>();
}
```

```typescript
//  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;
}
```

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

```typescript
// 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)
  }
}
```

`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. 

```typescript
// sanitization-options.interface.ts

export interface SanitizeOptions {
  isBold: boolean;
  isItalic: boolean;
  isUnderline: boolean;
  character?: string;
  color?: string;
}
```

```typescript
// 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');
```

```typescript
// masked-character.token.ts

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

export const MASKED_CHARACTER = new InjectionToken<string>('MASKED_CHARACTER');
```

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.

```typescript
// sanitize.service.ts

export abstract class SanitizeService {
  abstract cleanse(sentence: string): string;
}
```

`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.

```typescript
// 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;
  }
}
```

```typescript
// 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;
  }
}
```

`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. 

```typescript
// language.type.ts
export type Language = 'English' | 'Spanish';
```

```typescript
// 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 || '*'      
    }
  ]);
}
```

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

```typescript
const LANGUAGE = 'English';

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

`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 

```typescript
Type 'EnvironmentProviders' is not assignable to type 'Provider'.
```

```typescript
@Component({
  selector: 'app-censored-form',
  standalone: true,
  ..  other properties ...
  providers: [provideSanitization('Spanish')]   <-- Error occurs on this line
})
export class CensoredFormComponent {}
```

The following Stackblitz repo shows the final results:
{%embed https://stackblitz.com/edit/stackblitz-starters-6ywb5d?file=src%2Fmain.ts %}

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:

- Github Repo: https://github.com/railsstudent/ng-watch-your-language-demo
- Stackblitz: https://stackblitz.com/edit/stackblitz-starters-6ywb5d?file=src%2Fmain.ts
- Youtube: https://www.youtube.com/watch?v=snOIwJmxAq4&t=1s
- Angular documentation:  https://angular.io/api/core/makeEnvironmentProviders