DEV Community

Cover image for Providing services to programmatically created components in Angular
Ayyash
Ayyash

Posted on • Originally published at garage.sekrab.com

Providing services to programmatically created components in Angular

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.

Mint service

Here is the example service:

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

Enter fullscreen mode Exit fullscreen mode

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.

// 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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

Enter fullscreen mode Exit fullscreen mode

Clicking on Insert content button will result in NullInjectorError. To fix, that, in comes the 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():

// 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,
  });
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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,
  });

  // ...

}
Enter fullscreen mode Exit fullscreen mode

And this is how we use it

this.roseService.open(MintPartialComponent, {
  title: 'Mint',
  // ...
  providers: [{ provide: MintService, useValue: this.mintService }],
});
Enter fullscreen mode Exit fullscreen mode

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, 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

Providing services to programmatically created components in Angular - Sekrab Garage

Angular programmatically created components. 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 instan.... Posted in Angular

favicon garage.sekrab.com

Top comments (0)