Inserting programmatically created components with templates and content projection in Angular
Tags: angular, webdev, tutorial, typescript
Published: 2023-05-26 17:53:58

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](https://stackblitz.com/edit/angular-program-component) project.

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

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

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

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

```ts
// 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:

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

Using 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](https://angular.io/api/core/createComponent) expects an array of an array of nodes: `Node[][]`

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

```ts
// 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]`

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

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

```ts
// 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:

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

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

{% embed https://garage.sekrab.com/posts/templates-and-content-projection %}