Sekrab Garage

Angular programmatically created components

Providing services to programmatically created components in Angular

Angular May 30, 23
Subscribe to Sekrab Parts newsletter.
Series:

Programmatic components

In Angular 13, the ComponentFactoryResolver became deprecated. Some libraries that depended on it had to be rewritten using the new introduced function createComponent, that is more aligned with the standalone mantra. Let's dig into it and put it to good use.
  1. 8 months ago
    Creating a component dynamically, and programmatically in Angular
  2. 8 months ago
    Inserting Peach component into Rose at runtime
  3. 8 months ago
    Inserting programmatically created components with templates and content projection in Angular
  4. 8 months ago
    Providing services to programmatically created components in Angular
  5. 8 months ago
    Homemade dialog service 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.

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

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

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

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

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

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

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

And this is how we use it

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

anchorPutting 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?

  1. 8 months ago
    Creating a component dynamically, and programmatically in Angular
  2. 8 months ago
    Inserting Peach component into Rose at runtime
  3. 8 months ago
    Inserting programmatically created components with templates and content projection in Angular
  4. 8 months ago
    Providing services to programmatically created components in Angular
  5. 8 months ago
    Homemade dialog service in Angular