Sekrab Garage

Angular programmatically created components

Inserting programmatically created components with templates and content projection in Angular

Angular May 26, 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. two years ago
    Creating a component dynamically, and programmatically in Angular
  2. two years ago
    Inserting Peach component into Rose at runtime
  3. two years ago
    Inserting programmatically created components with templates and content projection in Angular
  4. two years ago
    Providing services to programmatically created components in Angular
  5. two years ago
    Homemade dialog service in Angular

Previously we tried hosting the newly created component in an HTML element, whether existing in body, or created and appended to body. Today we try two other ways:

  • Hosting in ng-template
  • Content projecting in ng-content

Follow along with StackBlitz project.

anchorHosting in a ng-template

What we need is a reference to that template, to use it as a host element. Is it doable? Let’s dig in.

First, we add a template, and declare it. (I created a new fresh component in StackBlitz: DifferentComponent).

// components/different.component

@Component({
  template: `
   //... add an ng-template where the new component shall appear
      <ng-template #content></ng-template>
   `
})
export class DifferentComponent  {
  // ...
  
  // making it static, allows us to find it ahead of time, so that we can populate it
  @ViewChild('content', {static: true, read: ElementRef}) content: ElementRef;

}

The simple way is to get the ElementRef of the template, then find the nativeElement (which is comment node) then add element after it.

// different.component

// after creating a new component
const componentRef = createComponent(RosePartialComponent, {
    environmentInjector: this.appRef.injector
});

this.appRef.attachView(componentRef.hostView);

// insert after
this.content.nativeElement
  .after((<any>componentRef.hostView).rootNodes[0]);

Another way is using ng-container instead of ng-template. The result is identical.

anchorBack to Rose dialog

Back to our Rose component, that is supposed to host Peach, programmatically. We won't stop appending Rose dialog itself in the body, this is a clean solution. But we can make use of the above, by replacing the host element id="roseChild" with a simple ng-template.

// lib/RoseDialog/Rose.partial

@Component({
  template: `
    // remove this id: rosechild
   <div class="modal-body" xid="rosechild" >
      // and add a new template
      <ng-template #content></ng-template>
   </div>
   `,
})
export class RosePartialComponent {
  // ...
  
  // make a reference to it
  @ViewChild('content', { static: true, read: ElementRef })
  content!: ElementRef;
  //...
  
}

Then we can use it in our roseService, we need to create the child component without setting its hostElement property, then later insert it:

// lib/RoseDialog/rose.service

open(...) {
  // ...
  // ********* method 2: add to ng-template***********/
  // first create child element
  const childRef = createComponent(c, {
    environmentInjector: this.appRef.injector,
  });
  
  // gain reference to content element, and child root
  const nativeElement = componentRef.instance.content.nativeElement;
  const childElement = (<EmbeddedViewRef<any>>childRef.hostView).rootNodes[0];
  // insert after
  nativeElement.after(childElement);
  //...
}

anchorUsing content projection

Finally, in our Rose dialog component, there is one more method to insert our new component (Peach) into the created dialog component (Rose). If we use ng-content tag in Rose component, we can place anything in it using the projectableNodes property in createComponent. Confused? Here is how:

The optional parameter projectableNodes expects an array of an array of nodes: Node[][]

First, let's add the ng-content tag into our Rose partial component

// lib/RoseDialog/rose.partial

@Component({
  template: `
   //... add an ng-content where the new peach component shall appear
      <ng-content></ng-content>
   `
})

Then the dialog service (roseService) is supposed to create the child component first, before passing the root nodes to the projectableNodes property. Note how you need to place the root nodes, which is an array, in another array for that to work: [rootNodes]

// lib/RoseDialog/rose.service

// create a new open method that uses the content projection
public openWithContent(...) {

  // first, create the child component
  const childRef = createComponent(c, {
    environmentInjector: this.appRef.injector,
  });

  // attach view
  this.appRef.attachView(childRef.hostView);

  // get root nodes (array)
  const rootNodes = (<EmbeddedViewRef<any>>childRef.hostView).rootNodes;

  // then create the dialog that will host it
  const componentRef = createComponent(RosePartialComponent, {
    environmentInjector: this.appRef.injector,
    // pass the child nodes here (an array of an array)
    projectableNodes: [rootNodes],
  });

  // append to body
  this.doc.body.append(
    (<EmbeddedViewRef<any>>componentRef.hostView).rootNodes[0]
  );

  // attach view
  this.appRef.attachView(componentRef.hostView);

  // passing properties and listening to events, is the same
  // ...

  return childRef.instance;
}

anchorProjecting multiple slots programmatically

In my dialog service, I only need to pass one Peach into my Rose. But you might have different requirements, to pass multiple components. In the previous methods, using an exact HTML id, or using a defined ng-template is straightforward, because you can gain direct reference to them. In the last method of projectable nodes, you can have multiple slots of ng-content, and pass multiple elements to the projectableNodes in the service, like this:

projectableNodes: [rootNodesOfA, rootNodesOfB]

You can also use querySelector, to identify multiple slots in one Peach component. Using HTML id and ng-template is again easy, but in ng-content, it isn't as clear as I wished it to be. Here is how.

Create a new component, and let's assume it has two regions: above and below.

// example Peach with multiple slots (banana.partial)

Component({
  template: `
    <div above>
    Above part
    </div>

    <div below>
    Below part
    </div>
   `,
   // ...
})
export class BananaPartialComponent {}

In our Rose dialog, we can define multiple slots of ng-content. Then in our dialog service (roseService), we'll query for these parts and pass them in the projectableNodes array:

// lib/RoseDialog/rose.service

public openWithContent(...) {
  // ... create the childRef, then find the rootNode

  // get root node
  const rootNode = (<EmbeddedViewRef<any>>childRef.hostView).rootNodes[0];

  // query select different parts
  const above = rootNode.querySelector('[above]');
  const below = rootNode.querySelector('[below]');

  // then create the dialog that will host it
  const componentRef = createComponent(RosePartialComponent, {
    environmentInjector: this.appRef.injector,
    // pass the children here
    projectableNodes: [[above], [below]],
  });

  // then append to body and attach behavior as usual
  // ...
  
  return childRef.instance;
}

Unfortunately, the content will be projected in the order they appear in the projectableNodes property. I tried using the select attribute with ng-content, but that made no difference.

Doesn't work: <ng-content select="[above]"></ng-content>

If you are interested in that path, you might dig deeper. As for me, I had enough of Bananas.

anchorProviding instances to Peach

Our dialog component is almost ready, there is one small optional addition I want to learn about before jumping into house keeping and finalizing. What if we want to provide a local instance of a service to an inserted component? That and a rant about third party libraries is coming next. Stay tuned. đŸ˜´

Thank you for reading this far, did you get used to Rose and Peach already?

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