Providing services to programmatically created components in Angular
Tags: angular, tutorial, webdev, typescript
Published: 2023-05-30 07:48:50

The last bit to investigate before we put everything together as a Dialog service is how to pass instances of provided services to programmatically created components.

There are three shapes of instances that a component may use:

-   Provided in root
-   Provided locally and injected in the new component
-   Provided in the parent component, and injected in the new component

Injecting any service provided in root in the newly created component works exactly as expected.

The second one is also to the point, the newly created standalone component can have `providers` array in its definition.

The third, is not as straightforward.

Injecting a service provided in the parent component
-----------------------------------------------------

This scenario may be rarely used, but here is example to test it. We will create a simple service that updates a message property. We will provide it in our local component, and then programmatically create a component that has this service injected. The idea is to be able to pass the same instance to the new component, to get and set the message in it.

Follow along on [StackBlitz](https://stackblitz.com/edit/angular-program-component?file=src%2Fcomponents%2Fmint.service.ts).

### Mint service

Here is the example service:

```ts
// components/mint.service

// an example service to provide
@Injectable()
export class MintService {

  message: string = 'default mint';
  constructor() {
    console.log('mint service created');
  }
  setMessage(message: string) {
    this.message = message;
  }
  getMessage() {
    return this.message;
  }
}

```

In our application (**Mint Component**), we shall provide the `MintService` and add few button clicks to set and get the message. We also begin with the basic of creating a new component and inserting it in an `ng-template` element.

```ts
// components/mint.component

@Component({
  // provide local instance
  providers: [MintService],
  // test buttons
  template: `
  <button class="btn" (click)="insertMint()">Insert content</button>
  <button class="btn" (click)="setMessage()">Update message</button>
  <button class="btn" (click)="getMessage()">Check message</button>

  <!-- insert into a template -->
  <ng-template #content></ng-template>
  `,
})
export class MintComponent {
  constructor(
    // inject appRef and mint service
    private appRef: ApplicationRef,
    private mintService: MintService
  ) {}

  // to use ng-templaet
  @ViewChild('content', { static: true, read: ElementRef })
  content!: ElementRef;

  insertMint(): void {
    // we start with the basic
    // this is where we need to figure out a way to pass the provided service
    const componentRef = createComponent(MintPartialComponent, {
      environmentInjector: this.appRef.injector,
    });
    this.appRef.attachView(componentRef.hostView);
    const contentElement = this.content.nativeElement;
    const child = (<EmbeddedViewRef<any>>componentRef.hostView).rootNodes[0];

    contentElement.after(child);
  }

  setMessage() {
    this.mintService.setMessage('Mint Parent');
    console.log('message set');
  }
  getMessage() {
    console.log('From parent', this.mintService.getMessage());
  }
}
```

### Mint partial component

The partial component that will be created is (`MintPartialComponent`). It injects the `MintService`, and naturally would break since it has not been provided. Here is the Mint partial component:

```ts
// components/mint.partial

@Component({
  template: `
    <div class="box">
    <button class="btn" (click)="setMessage()">Set Message</button>
    <button class="btn" (click)="getMessage()">Get Message</button>
    </div>
   `
   // ...
})
export class MintPartialComponent {
  // inject mint service
  constructor(private mintService: MintService) {}

  setMessage() {
    this.mintService.setMessage('Mint child');
    console.log('child message set');
  }

  getMessage() {
    console.log('From child', this.mintService.getMessage());
  }
}

```

Clicking on Insert content button will result in `NullInjectorError`. To fix, that, in comes the [ElementInjector](https://angular.io/guide/hierarchical-dependency-injection#elementinjector). According to Angular docs:

> Providing a service in the `@Component()` decorator using its `providers` or `viewProviders` property configures an `ElementInjector`

Our final solution should result in the following:

`providers:[{ provide:MintService, useValue: this.mintService }]`

We set the value to equal the already created instance in parent, to keep using it in child. We can create the injector using [Injector.create()](https://angular.io/api/core/Injector#create):

```ts
// component/mint.component

// updating insertMint so that we create an injector first to provide our service
insertMint(): void {

  // create injector first
  const _injector = Injector.create({
    providers: [{ provide: MintService, useValue: this.mintService }],
  });

  const componentRef = createComponent(MintPartialComponent, {
    environmentInjector: this.appRef.injector,
    // pass injector
    elementInjector: _injector,
  });
  // ...
}
```

Running the code, and clicking on getting and setting buttons, it is obvious that both Parent and Child share the same instance.

Rose service
-------------

Back to our future Dialog service, Rose. We need to pass providers from our code programmatically to the service, and create injectors optionally. We can adapt the service to handle one more property in the options argument:

```ts
// lib/RoseDialog/rose.service

// updating the open method
// open method
public open(
  c: Type<any>,
  options?: {
    // ...
    // add property of providers
    providers?: StaticProvider[];
  }
) {
  // ...
  // create injector and pass it to the child component
  const _injector = options?.providers?.length
    ? Injector.create({ providers: options.providers })
    : undefined;

  const childRef = createComponent(c, {
    environmentInjector: this.appRef.injector,
    elementInjector: _injector,
  });

  // ...

}
```

And this is how we use it

```ts
this.roseService.open(MintPartialComponent, {
  title: 'Mint',
  // ...
  providers: [{ provide: MintService, useValue: this.mintService }],
});
```

Putting it all together
------------------------

Let us make use of all the previous articles to create our own Dialog service. But enough for today, next Tuesday. We will also add styling, and add few bells and whistles of our own. 😴

#### Ranting: what's wrong with Angular Material Dialog component

If you had a look at the source code of Angular [Material Dialog](https://github.com/angular/components/blob/main/src/material/dialog/dialog.ts), you would have found a lot more stuff going on, I promise you, it does not do more than what we have accomplished thus far. May be just extra checks, some features, abstractions, and better typing. Bells and whistles.

**Then why should I use Material Dialog you ask? **You can use it already but remember:

-   Material framework is a whole lot of features. Using one part and ignoring the framework might backfire in terms of boilerplate code, and lack of consistency and maintainability.
-   Material has a lot of parts, in order to reduce rewrite of features, these parts depend on each other, and thus you will find yourself jumping from Dialog to Portal for example, and getting lost in more layers of abstraction.
-   Material still does not use `createComponent` function. I looked, I couldn't find it.
-   Material is done for millions of users. They need to abstract almost every property to serve them all, and they need to be backward compatible. If you want control, and simplicity, write your own stuff.
-   Material takes care of its own styling, and accessibility.

Thank you for reading this far, have you noticed how all my days are Tuesdays?

### RESOURCES

-   [Angular Material Dialog](https://github.com/angular/components/blob/main/src/material/dialog/dialog.ts)
-   [Angular Docs](https://angular.io/api/core/createComponent)
-   [StackBlitz project](https://stackblitz.com/edit/angular-program-component)

{% embed https://garage.sekrab.com/posts/providing-services-to-programmatically-created-components %}