NgTemplateOutlet Typed checked (with @ContentChild)
Tags: angular, typescript, challenge
Published: 2022-12-07 11:50:00

## Welcome to Angular challenges #4.

> _The aim of this series of Angular challenges is to increase your skills by practicing on real life exemples. Moreover you can submit your work though a PR which I or other can review; as you will do on real work project or if you want to contribute to Open Source Software._

The fourth challenge will teach us how to use the **ngTemplateOutlet** structural directive of  Angular with strong typing.

If you haven't done the challenge yet, I invite you to try it first by going to [Angular Challenges](https://github.com/tomalaforge/angular-challenges/blob/main/apps/context-outlet-type/README.md) and coming back afterward to compare your solution with mine. _(You can also submit a PR that I'll review)_

---

In this challenge we start with two components, PersonComponent and ListComponent where we can customize their templates. 

```typescript
<person [person]="person">
  <ng-template #personRef let-name let-age="age">
    {{ name }}: {{ age }}
  </ng-template>
</person>

<list [list]="students">
  <ng-template #listRef let-student let-i="index">
    {{ student.name }}: {{ student.age }} - {{ i }}
  </ng-template>
</list>

<list [list]="cities">
  <ng-template #listRef let-city let-i="index">
    {{ city.name }}: {{ city.country }} - {{ i }}
  </ng-template>
</list>
```
Despite the fact that this example works perfectly at **runtime**, we are not taking full advantage of Typescript at **compile time**. As you can see below, there is no typing, so this code is not safe for future refactoring or development. 

![IDE type inference for PersonComponent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/plf8ldjnfb5y8jqi77mm.png)

![IDE type inference for ListComponent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hqukb5jr79bs5qocl66s.png)

Before moving on to solving this problem, I invite you to consult these articles to understand [Directive Type Checking](https://dev.to/this-is-angular/directive-type-checking-45oe) and [Typescript Type Guard](https://dev.to/this-is-angular/typescript-type-predicate-3gbn). 

---

## PersonComponent: Type is known in advance
Our PersonComponent look like this:

```typescript
@Component({
  standalone: true,
  imports: [NgTemplateOutlet],
  selector: 'person',
  template: `
    <ng-container
      *ngTemplateOutlet="
        personTemplateRef || emptyRef;
        context: { $implicit: person.name, age: person.age }
      "></ng-container>

    <ng-template #emptyRef> No Template </ng-template>
  `,
})
export class PersonComponent {
  @Input() person!: Person;

  @ContentChild('#personRef', { read: TemplateRef })
  personTemplateRef?: TemplateRef<unknown>;
}
```
We get the template reference from our parent component view via **@ContentChild** with a magic string `(#personRef)`. We inject the template into **ngTemplateOutlet** to display it. If personTemplateRef is not defined, we display a backup template. 

> _@ContentChild is a decorator used to access elements or directives that are projected into the component template. This is the difference with @ViewChild which is used to access elements or directives defined in the template._

We can apply what we learned in our Directive Type Checking article. 

First, we need to create a Directive and replace `#personRef` with its selector.

```typescript
// This directive seems unnecessary, but it's always better to reference a directive 
// than a magic string. And we will see that it can be very useful
@Directive({
  selector: 'ng-template[person]',
  standalone: true,
})
export class PersonDirective {}
```

```html
<person [person]="person">
  <!-- #personRef has been replaced with person (PersonDirective selector) -->
  <ng-template person let-name let-age="age">
    {{ name }}: {{ age }}
  </ng-template>
</person>
```
In our PersonComponent, we can now look up this directive reference by writing:

```typescript
@ContentChild(PersonDirective, { read: TemplateRef })
personTemplateRef?: TemplateRef<unknown>;
```
> _`read` lets us defined which element of the DOM we want to target. Without the `read` meta property, we will get PersonDirective as return type. _

The template is still of unknown type, but since we now referring to a directive, we can take advantage of **ngTemplateContextGuard** and set our context. 

```typescript
interface PersonContext {
  $implicit: string;
  age: number;
}

@Directive({
  selector: 'ng-template[person]',
  standalone: true,
})
export class PersonDirective {
  static ngTemplateContextGuard(
    dir: PersonDirective,
    ctx: unknown
  ): ctx is PersonContext {
    return true;
  }
}
```
And now let's the IDE works for us

![IDE type inference with strong typing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/85r0degkgn3k4zxw2qp8.png)


## Bonus: Typing NgTemplateOutlet
There is still one problem, the **ngTemplateOutlet** directive itself is not strongly typed _(at the time of writing)_. So in our template defining your outlet context, typing is still not present. 

```html
<!-- should give a compile error since $implicit wants a string, and context 
is looking for $implicit and age -->
<ng-container
  *ngTemplateOutlet="
    personTemplateRef || emptyRef;
    context: { $implicit: person.age }
  "></ng-container>
```
When looking at the source code of [NgTemplateOutlet](https://github.com/angular/angular/blob/main/packages/common/src/directives/ng_template_outlet.ts), we can clearly see that the context input property overrides our type with Object or null. 

```typescript
Input() public ngTemplateOutletContext: Object|null = null;
```
If we want to have correct typing, we can always create our own TemplateOutlet directive by copying and pasting the Angular core Directive and applying typing on it. 

```typescript
@Directive({
  selector: '[ngTemplateOutlet]',
  standalone: true,
})
// The directive is now waiting for a specific Type. 
export class AppTemplateOutlet<T> implements OnChanges {
  private _viewRef: EmbeddedViewRef<T> | null = null;

  @Input() public ngTemplateOutletContext: T | null = null;

  @Input() public ngTemplateOutlet: TemplateRef<T> | null = null;

  @Input() public ngTemplateOutletInjector: Injector | null = null;

  constructor(private _viewContainerRef: ViewContainerRef) {}

  ngOnChanges(changes: SimpleChanges) {
    if (changes['ngTemplateOutlet'] || changes['ngTemplateOutletInjector']) {
      const viewContainerRef = this._viewContainerRef;

      if (this._viewRef) {
        viewContainerRef.remove(viewContainerRef.indexOf(this._viewRef));
      }

      if (this.ngTemplateOutlet) {
        const {
          ngTemplateOutlet: template,
          ngTemplateOutletContext: context,
          ngTemplateOutletInjector: injector,
        } = this;
        this._viewRef = viewContainerRef.createEmbeddedView(
          template,
          context,
          injector ? { injector } : undefined
        ) as EmbeddedViewRef<T> | null;
      } else {
        this._viewRef = null;
      }
    } else if (
      this._viewRef &&
      changes['ngTemplateOutletContext'] &&
      this.ngTemplateOutletContext
    ) {
      this._viewRef.context = this.ngTemplateOutletContext;
    }
  }
}
```
Our previous code now gets a compiled error: 

![correct type error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r2bc823sr3wltg6578gw.png)


## ListComponent: Type is unkwown
Compared to PersonComponent, we don't know the type in advance. This is a bit trickier.

In the same way as before, let's create a directive and add a **ngTemplateContextGuard** to it. 

```typescript
interface ListTemplateContext {
  $implicit: any[]; // we don't know the type in advance
  appList: any[];
  index: number; // we know that index will always be of type number
}

@Directive({
  selector: 'ng-template[appList]',
  standalone: true,
})
export class ListTemplateDirective {
  static ngTemplateContextGuard(
    dir: ListTemplateDirective,
    ctx: unknown
  ): ctx is ListTemplateContext {
    return true;
  }
}
```

![type inference for ListComponent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6hbt8c271ytd16xkw0ez.png)


We haven't improved our typing much. Only index property is correctly typed to number. 

Let us use Typescript generic types to specify our context type: 

```typescript
interface ListTemplateContext<T> {
  $implicit: T;
  appList: T;
  index: number;
}

@Directive({
  selector: 'ng-template[appList]',
  standalone: true,
})
// T is still unknown. 
// Angular can only infer the correct type by referring to the type of inputs
export class ListTemplateDirective<T> {
  static ngTemplateContextGuard<TContext>(
    dir: ListTemplateDirective<TContext>,
    ctx: unknown
  ): ctx is ListTemplateContext<TContext> {
    return true;
  }
}
```
To give our directive the type of our list, we need to pass this type to our directive. In Angular, the only way to give this information at compile time is through Inputs. 

```typescript
@Directive({
  selector: 'ng-template[appList]',
  standalone: true,
})
export class ListTemplateDirective<T> {
  @Input('appList') list!: T[]

  static ngTemplateContextGuard<TContext>(
    dir: ListTemplateDirective<TContext>,
    ctx: unknown
  ): ctx is ListTemplateContext<TContext> {
    return true;
  }
}
```
Now, let's rewrite our template to pass this input. 

![correct type inference](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dswms8jm1s0eg8uh2u2e.png)


And here you go, we have strongly typed properties. 

### Bonus tip:
We can also write our template with the shorthand syntax *:

```html
<list [list]="students">
  <ng-container *appList="students as student; index as i">
    {{ student.name }}: {{ student.age }} - {{ i }}
  </ng-container>
</list>
```

---

I hope you enjoyed this fourth challenge and learned from it.

> 👉 _Other challenges are waiting for you at [Angular Challenges](https://github.com/tomalaforge/angular-challenges). Come and try them. I'll be happy to review you!_

Follow me on [Medium](https://medium.com/@thomas.laforge), [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge) to read more about upcoming Challenges!