Optional content projection/injection in Angular
Tags: angular
Published: 2021-05-08 11:23:10

---
title: Optional content projection/injection in Angular
published: true
description: Optional content projection/injection in Angular
tags: angular
---

# Optional content projection/injection in Angular

Recently I had the requirement to make part of a component (the header of a custom table) replaceable with custom content. In case nothing is provided the implementation was supposed to render "default" content. A simple property was not sufficient as the injected/projected content could be anything ranging from simple text to a slider/toggle...  
The requirements could be summarized as follows:

1. Render custom content in case it is provided
2. Render default content otherwise

I was torn between using `ng-content` or `ng-template` to solve the problem. To make an informed decision I created a POC implementing both options to see if one is superior to the other. In contrast to the requirements, the created [POC](https://github.com/remshams/optional-content-projection) allows for replacing multiple contents (like a header and a footer) to verify that the solution could be extended in the future (if needs arise). The next sections describe the alternatives I could come up with.

# ng-content

This is usually the first option as it is simple to implement and use. The custom contents are provided as children using `ng-content`. By using a `select` attribute multiple contents can be projected as well:

```html
<ng-content select="[slot='header']"></ng-content>
<ng-content select="[slot='footer']"></ng-content>
```

This covers the first requirement. The second is more difficult to realize using `ng-content` alone. The figure out whether to render the custom or default content requires some means to determine if something has been passed as `ng-content` or not. I was not able to find any build-in feature to query/get that information from the component or template so a custom solution is required.  
One option is to create a directive which is put on the content to be projected (`appSlot` in the example below):

```html
<app-render-slot>
  <div appSlot slot="header">Custom Header</div>
  <div appSlot slot="footer">Custom Footer</div>
</app-render-slot>
```

The component can search for the directive(s) using a `@ContentChildren` query. In case something is found for the placeholder the custom content is used, otherwise it falls back to the default content:

```typescript
@Component({
  selector: 'app-render-slot',
  templateUrl: './component.html',
  styleUrls: ['./component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class RenderSlotComponent {
  @ContentChildren(SlotDirective, { read: ElementRef }) set slotDirectives(
    value: QueryList<ElementRef>
  ) {
    this.nativeSlots.next(Array.from(value));
  }

  private nativeSlots: BehaviorSubject<Array<ElementRef>>;
  readonly slotNames$: Observable<SlotNames>;

  constructor() {
    this.nativeSlots = new BehaviorSubject<Array<ElementRef>>([]);

    this.slotNames$ = this.setSlotsByName(this.nativeSlots.asObservable());
  }

  isSlotSet(slotName: SlotName): Observable<boolean> {
    return this.slotNames$.pipe(
      map((slotNames) => slotNames.includes(slotName))
    );
  }

  private setSlotsByName(
    slots$: Observable<Array<ElementRef>>
  ): Observable<SlotNames> {
    return slots$.pipe(
      map((slots) =>
        slots.map((slot) => slot.nativeElement.getAttribute('slot'))
      )
    );
  }
}
```

For the example, the "name" of the slot ("header" or "footer") is extracted based on what has been set for the custom "slot" attribute on the projected content. The `ElementRef` to look for is marked/identified by the `SlotDirective` and extracted with the `@ContentChildren` query. The other part of the implementation just maps the list of `ElementRef`s to the slot names.  
With help of the `isSlotSet` method, the template can either render the custom content (in case the slot is found) or fall back to a default content.

For the sake of the example, the template of the component is kept simple containing only the `ng-content` placeholders:

```html
<ng-content
  select="[slot='header']"
  *ngIf="isSlotSet('header') | async; else defaultHeader"
></ng-content>
<ng-content
  select="[slot='footer']"
  *ngIf="isSlotSet('footer') | async; else defaultFooter"
></ng-content>

<ng-template #defaultHeader> Default Header </ng-template>
<ng-template #defaultFooter> Default Footer </ng-template>
```

The alternative described here can be found in the `ng-content/render-slot` folder in the example repository. When removing either the "Custom Header" or "Custom Footer" `div` in the `AppComponent` template for `app-render-slot` the default fallback will be rendered.

## with SlotRenderer

_Heads up: This solution does not work, so please skip ahead in case not of interest._

The approach described above has the disadvantage that each component with optional content projection does have to implement the mechanism to find/determine the rendered content.  
My idea was to improve the solution by creating a "helper" component called `SlotRendererComponent`, which would be responsible for rendering the content passed by the using component:

```html
<app-slot-renderer [defaultSlotContent]="defaultHeader"
  ><ng-content select="[slot='header']"></ng-content
></app-slot-renderer>
<app-slot-renderer [defaultSlotContent]="defaultFooter"
  ><ng-content select="[slot='footer']"></ng-content
></app-slot-renderer>

<ng-template #defaultHeader> <div>Default Header</div> </ng-template>
<ng-template #defaultFooter> <div>Default Footer</div> </ng-template>
```

The custom content gets provided using `ng-content` and the `select` attribute (the latter could be omitted in case there is only a single `ng-content` to project). The default content is passed as `TemplateRef` using an `Input` property.

The `SlotRendererComponent` also uses `ng-content` to render what has been projected from the using component which would be

```html
<ng-content *ngIf="isSlotSet$ | async; else defaultSlotContent"></ng-content>
```

The custom content originally passed is therefore projected twice:

- First to the outer component (`RenderSlotSlotRendererComponent` in the example)
- Second to the `SlotRendererComponent`

The flattened hierarchy looks something like this (not the real DOM structure):

```html
<!-- From SlotRendererComponent  -->
<ng-content *ngIf="isSlotSet$ | async; else defaultSlotContent">
  <!-- From RenderSlotSlotRendererComponent  -->
  <ng-content select="[slot='header']">
    <!-- Projected custom content   -->
    <div appSlot slot="header">Custom Header</div>
  </ng-content>
</ng-content>
<!-- Same for the footer -->
```

By the same mechanism as in the first approach, the custom or default content will be rendered by `SlotRendererComponent`.
The reason why this solution is not working is due to `@ContentChildren` not being able to query nested `ng-content`s. Setting `{ descendants: true }` also did not work for me. I found an [issue](https://github.com/angulardart/angular/issues/195) describing the problem for the `AngularDart` repository so maybe it is related (or I am doing something wrong here ;) ).

# ng-template

## with template properties

One option for the `ng-template` based solutions is to directly pass the custom contents in a property as `TemplateRef`s.

```html
<app-template-render-props
  [templates]="{ 'header': header, 'footer': footer }"
></app-template-render-props>

<ng-template #header><div>Custom Header</div></ng-template>
<ng-template #footer><div>Custom Footer</div></ng-template>
```

The provided `TemplateRef` for each slot is rendered using `*ngTemplateOutlet`. Same as for the `ng-content` approach the component falls back to a default content in case nothing has been defined (done by the `RenderTemplateComponent` helper in the example).

```html
<app-render-template
  [template]="{ customTemplate: templates.header, defaultTemplate: defaultHeader }"
></app-render-template>
<app-render-template
  [template]="{ customTemplate: templates.footer, defaultTemplate: defaultHeader }"
></app-render-template>

<ng-template #defaultHeader> <div>Default Header</div> </ng-template>
<ng-template #defaultFooter> <div>Default Footer</div> </ng-template>
```

## with directive

Having to define a dedicated `ng-template` wrapper for each custom content is inconvenient to use and clutters the template of the using component. This can be avoided by using a structural directive storing the `TemplateRef` as well as the slot name:

```typescript
@Directive({
  selector: '[appTemplateSlot]'
})
export class TemplateSlotDirective {
  @Input() appTemplateSlot: SlotName | null = null;

  constructor(public template: TemplateRef<unknown>) {}
}
```

The directive takes the slot name ("header" or "footer" in the example) as input property and stores the associated `TemplateRef` in a public `template` property (the `unknown` type of `TemplateRef` could be replaced by the associated context in case it is known/available).

The rendering component can now query for the `TemplateSlotDirective`s using `@ContentChildren` and render the stored `template` to the associated slot:

```typescript
@Component({
  selector: 'app-render-props-directive',
  templateUrl: './component.html',
  styleUrls: ['./component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class RenderPropsDirectiveComponent {
  @ContentChildren(TemplateSlotDirective) set templateSlots(
    templateSlots: QueryList<TemplateSlotDirective>
  ) {
    this.templateDirectives.next(
      templateSlots.length > 0 ? Array.from(templateSlots) : []
    );
  }

  private templateDirectives: ReplaySubject<Array<TemplateSlotDirective>>;
  templates$: Observable<Partial<Templates>>;

  constructor() {
    this.templateDirectives = new ReplaySubject(1);

    this.templates$ = this.setupTemplates(
      this.templateDirectives.asObservable()
    );
  }

  private setupTemplates(
    templateDirectives$: Observable<Array<TemplateSlotDirective>>
  ): Observable<Partial<Templates>> {
    return templateDirectives$.pipe(
      map((templateDirectives) =>
        templateDirectives.reduce(
          (partialTemplateDirectives, templateDirective) =>
            templateDirective.appTemplateSlot
              ? {
                  ...partialTemplateDirectives,
                  [templateDirective.appTemplateSlot]:
                    templateDirective.template
                }
              : partialTemplateDirectives,
          {}
        )
      ),
      shareReplay({ bufferSize: 1, refCount: true })
    );
  }
}
```

As usual the rendering component now either renders the custom or fallback content for each slot:

```html
<app-render-template
  [template]="{ customTemplate: (templates$ | async)?.header, defaultTemplate: defaultHeader }"
></app-render-template>
<app-render-template
  [template]="{ customTemplate: (templates$ | async)?.footer, defaultTemplate: defaultHeader }"
></app-render-template>

<ng-template #defaultHeader> <div>Default Header</div> </ng-template>
<ng-template #defaultFooter> <div>Default Footer</div> </ng-template>
```

As shown below the `ng-template` wrapper is now replaced by putting the `TemplateSlotDirective` selector on the the custom content:

```html
<app-render-props-directive>
  <div *appTemplateSlot="'header'">Custom Header</div>
  <div *appTemplateSlot="'footer'">Custom Footer</div>
</app-render-props-directive>
```

# Conclusion

With both the `ng-content` as well as the `ng-template` it was/is possible to fulfill the requirements to either display custom content or fall back to rendering a default.
I prefer the `ng-template` based solution as:

- When used with a structural directive provides the same ease of use as `ng-content` for the using component (especially within the template).
- It allows for extracting all the repetitive rendering related implementations which can be reused for components requiring the same "feature". This is/was not possible for the `ng-content` based solution due to the issue with querying nested `ng-content`s using `@ContentChildren`.

The complete code for the POC can be found [here](https://github.com/remshams/optional-content-projection).