How to Write a Custom Structural Directive in Angular - Part 2
Tags: angular, typescript, webdev, directive
Published: 2021-12-14 23:01:56


In [the previous article](https://www.thisdot.co/blog/getting-started-with-custom-structural-directives-in-angular) I've shown how you can implement a custom structural directive in Angular. We've covered a simple custom structural directive that implements interface similar to Angular's NgIf directive. If you don't know what structural directives are, or are interested in basic concepts behind writing custom one, please read [the previous article](https://www.thisdot.co/blog/getting-started-with-custom-structural-directives-in-angular) first.

In this article, I will show how to create a more complex structural directive that:

- passes properties into the rendered template
- enables strict type checking for the template variables

## Starting point

I am basing this article on the example implemented in the part 1 article. You can use [example on Stackblitz](https://stackblitz.com/github/ktrz/custom-structural-directive/tree/getting-started-solution?&preset=node) as a starting point if you wish to follow along with the code examples.

## Custom [NgForOf](https://angular.io/api/common/NgForOf) directive

This time, I would like to use Angular's NgForOf directive as an example to re-implement as a custom `CsdFor` directive. Let's start off by using Angular CLI to create a new module, and directive files:

```shell
ng generate module for
ng generate directive for/for --module for

# or shorthand
# ng g m for
# ng g d for/for --module for
```

First, we need to follow similar steps as with the `CsdIf` directive.

- add constructor with `TemplateRef`, and `ViewContainerRef` injected
- add an `@Input` property to hold the array of items that we want to display

```ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[csdFor]',
})
export class ForDirective<T> {
  constructor(
    private templateRef: TemplateRef<unknown>,
    private vcr: ViewContainerRef
  ) {}

  @Input() csdForOf: T[] = [];
}
```

Then, in the `ngOnInit` hook we can render all the items using the provided template:

```ts
export class ForDirective<T> implements OnInit {
  private items: T[] = [];

  constructor(
    private templateRef: TemplateRef<unknown>,
    private vcr: ViewContainerRef
  ) {}

  @Input() csdForOf: T[] = [];

  ngOnInit(): void {
    this.renderItems();
  }

  private renderItems(): void {
    this.vcr.clear();
    this.csdForOf.map(() => {
      this.vcr.createEmbeddedView(this.templateRef);
    });
  }
}
```

Now, we can verify that it displays the items properly by adding the following template code to our `AppComponent`.

```html
<div *csdFor="let item of [1, 2, 3, 4, 5]">
  <p>This is item</p>
</div>
```

It displays the items correctly, but doesn't allow for changing the displayed collection yet. To implement that, we can modify the `csdForOf` property to be a setter and rerender items then:

```ts
export class ForDirective<T> {
  private items: T[] = [];

  constructor(
    private templateRef: TemplateRef<unknown>,
    private vcr: ViewContainerRef
  ) {}

  @Input() set csdForOf(items: T[]) {
    this.items = items;
    this.renderItems();
  }

  private renderItems(): void {
    this.vcr.clear();
    this.items.map(() => {
      this.vcr.createEmbeddedView(this.templateRef);
    });
  }
}
```

Now, our custom directive will render the fresh items every time the collection changes (its reference).

## Accessing item property

The above example works nice already, but it doesn't allow us to display the item's content yet. The following code will display `"no content"` for each template rendered.

```html
<div *csdFor="let item of [1, 2, 3, 4, 5]">
  <p>This is item: {{ item || '"no content"' }}</p>
</div>
```

![csdFor no content](//images.contentful.com/zojzzdop0fzx/3wOp2dBfVvY9RS9iQTIZq8/bd4dddd90cceb73d721a6f9003a3938b/csdFor_no_content.png)

To resolve this, we need to provide a value of each item into a template that we are rendering. We can do this by providing second param to `createEmbeddedView` method of `ViewContainerRef`.

```ts
export class ForDirective<T> {
  /* rest of the class */

  private renderItems(): void {
    this.vcr.clear();
    this.items.map((item) => {
      this.vcr.createEmbeddedView(this.templateRef, {
        // provide item value here
      });
    });
  }
}
```

The question is what key do we provide to assign it under `item` variable in the template. In our case, the item is a default param, and Angular uses a reserved `$implicit` key to pass that variable. With that knowledge, we can finish
the `renderItems` method:

```ts
export class ForDirective<T> {
  /* rest of the class */

  private renderItems(): void {
    this.vcr.clear();
    this.items.map((item) => {
      this.vcr.createEmbeddedView(this.templateRef, {
        $implicit: item,
      });
    });
  }
}
```

Now, the content of the item is properly displayed:

![csdFor with content](//images.contentful.com/zojzzdop0fzx/6lZS1B6TqszVBNcKc1o4zP/82bd645521e7bd072488517db6a717e5/csdFor_with_content.png)

## Adding more variables to the template's context

Original `NgForOf` directives allows developers to access a set of useful properties on an item's template:

- `index` - the index of the current item in the collection.
- `count` - the length of collection
- `first` - true when the item is the first item in the collection
- `last` - true when the item is the last item in the collection
- `even` - true when the item has an even index in the collection
- `odd` - true when the item has an odd index in the collection

We can pass those as well when creating a view for a given element along with the `$implicit` parameter:

```ts
export class ForDirective<T> {
  /* rest of the class */

  private renderItems(): void {
    this.vcr.clear();
    this.items.map((item, index, arr) => {
      this.vcr.createEmbeddedView(this.templateRef, {
        $implicit: item,
        index,
        first: index === 0,
        last: index === arr.length - 1,
        even: (index & 1) === 0,
        odd: (index & 1) === 1,
        count: arr.length,
      });
    });
  }
}
```

And now, we can use those properties in our template.

```html
<div
  *csdFor="
        let item of [1, 2, 3, 4, 5];
        let i = index;
        let isFirst = first;
        let isLast = last;
        let isEven = even;
        let isOdd = odd;
        let size = count
      "
>
  <p>This is item: {{ item }}.</p>
  <pre>
        Index: {{ i }}
        First: {{ isFirst }}
        Last: {{ isLast }}
        Even: {{ isEven }}
        Odd: {{ isOdd }}
        Count: {{ size }}
      </pre
  >
</div>
```

![csdFor with additional props](//images.contentful.com/zojzzdop0fzx/1RRg7KeOi9U1zKApHyOAe8/a003039cb8d8e7633965e33251fc061b/csdFor_with_additional_props.png)

## Improve template type checking

Lastly, as a developer using the directive it improves, the experience if I can have type checking in the template used by `csdFor` directive. This is very useful as it will make sure we don't mistype the property name as well as we only use the `item`, and additional properties properly. Angular's compiler allows us to define a static `ngTemplateContextGuard` methods on a directive that it will use to type-check the variables defined in the template. The method has a following shape:

```ts
static ngTemplateContextGuard(
  dir: DirectiveClass,
  ctx: unknown): ctx is DirectiveContext {
    return true;
}
```

This makes sure that the properties of template rendered by our `DirectiveClass` will need to conform to `DirectiveContext`. In our case, this can be the following:

```ts
interface ForDirectiveContext<T> {
  $implicit: T;
  index: number;
  first: boolean;
  last: boolean;
  even: boolean;
  odd: boolean;
  count: number;
}

@Directive({
  selector: '[csdFor]',
})
export class ForDirective<T> {
  static ngTemplateContextGuard<T>(
    dir: ForDirective<T>,
    ctx: unknown
  ): ctx is ForDirectiveContext<T> {
    return true;
  }

  /* rest of the class */
}
```

Now, if we eg. try to access item's property that doesn't exist on the item's interface, we will get a compilation error:

```html
<div *csdFor="let item of [1, 2, 3, 4, 5]">
  <p>This is item: {{ item.someProperty }}.</p>
</div>
```

![csdFor item compilation error](//images.contentful.com/zojzzdop0fzx/7AwdsChfQaaC1TFzakMTP9/3809684b05e89348190fb509eafee9d9/csdFor_item_compilation_error.png)

The same would happen if we made a typo in any of the context property names:

```html
<div *csdFor="let item of [1, 2, 3, 4, 5]; let isFirst = firts">
  <p>This is item: {{ item }}.</p>
</div>
```

![csdFor context variable typo](//images.contentful.com/zojzzdop0fzx/5kBSI4AB3qRGAoieNiDdyq/0d60bac2ec791f19ff9508f613f3b3c0/csdFor_context_variable_typo.png)

## Summary

In this article, we've created a clone of Angular's built-in `NgForOf` directive. The same approach can be used to create any other custom directive that your project might need. As you can see, implementing a custom directive with additional template properties and great type checking experience is not very hard.

If something was not clear, or you want to play with the example directive, please visit the [example on Stackblitz](https://stackblitz.com/github/ktrz/custom-structural-directive/tree/getting-started-p2-solution?file=apps%2Fcustom-structural-directive%2Fsrc%2Fapp%2Ffor%2Ffor.directive.ts&preset=node).

In case you have any questions, you can always tweet or DM me at [@ktrz](https://twitter.com/ktrz__). I'm always happy to help!

---

_This Dot Labs is a development consultancy focused on providing staff augmentation, architectural guidance, and consulting to companies._

_We help implement and teach modern web best practices with technologies such as React, Angular, Vue, Web Components, GraphQL, Node, and more._