Creating a Structural Directive

In this lesson, we'll look at how to create structural directives.

We’ll be creating another directive that will replace the ngFor directive. The last directive we created was an attribute directive. The ngFor directive is a structural directive because it adds/removes elements from the document.

Generating a directive

In the command line, run the following command:

ng generate directive loop

Two files are created: loop.directive.ts and loop.directive.spec.ts. We’ll be focusing on the loop.directive.ts file. Structural directives aren’t all that different from attribute directives in the way that they’re created.

Referencing the elements

The next step is to grab the element to which the directive is applied. In the directive, we’ll need to import two classes: ViewContainerRef and TemplateRef.

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

The ViewContainerRef class will give us a reference to the element to which we’re applying the directive. It’s similar to the ElementRef class. The difference between the two is that the ViewContainerRef class will expose methods for inserting/removing elements inside the element. It allows us to interact with the contents of the element.

The TemplateRef class will provide a reference to the children of the element to which the directive is attached.

Both classes will help us loop through the elements. You’ll see how in a moment. We’ll update the constructor() function in the class to use the classes.

export class LoopDirective {
  constructor(
    private viewContainer: ViewContainerRef,
    private templateRef: TemplateRef<any>
  ) { }
}

Note: Once again, this is dependency injection. We’ll be exploring how dependency injection works in a future lesson.

Looping through the elements

We can start to iterate through the elements. First, we’ll need to know how many times we should loop through the elements. We want the number to be provided by the component, so we’ll import the Input decorator to be able to accept data.

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

Next, we’ll add a property with the @Input() decorator.

export class LoopDirective {

  constructor(
    private viewContainer: ViewContainerRef,
    private templateRef: TemplateRef<any>
  ) { }

  @Input('appLoop') set render(steps: number) {

  }
}

In the example above, we’re checking for a property, called appLoop, on the element. This is the same name we used in the selector option. This will allow the user to bind the directive without having to create two separate properties.

We’re creating an alias for the property. Its alias is render. We’re using the set keyword to wait for when the property changes. If it does change, we’ll accept the change in the parameters of the steps function.

The next step is to create a loop in the function.

@Input('appLoop') set render(steps: number) {
  this.viewContainer.clear();

  for (let step = 0; step < steps; step++) {
    this.viewContainer.createEmbeddedView(this.templateRef, {
      index: step
    });
  }
}

There are a couple of things going on. First, we’re clearing the contents of the element to which the directive is applied. We want to make sure we start with a clean slate before start inserting content into it. We can do this using the this.viewContainer.clear() function.

Second, we’re creating a for() loop. It will run until the step variable exceeds the steps parameter.

Inside the loop, we’re inserting a new element using the this.viewContainer.createEmbeddedView() function. This function will ensure that the content we insert is processed before it’s inserted. If we have directives or expressions inside the template, they’ll be processed.

The first argument of the function is the template we want to insert. The goal is to create copies of the contents of the element to which the directive is applied. For example, let’s say we applied the directive to the following element:

<ul *appLoop="5">
  <li>Create copies of me!</li>
</ul>

The <li> element will be replicated. The templateRef property represents the <li> element in this example since it’s the inner contents of the element to which the directive is attached.

The second argument of the function is called the context object. We can provide the template with some data through this object. In the example, we’re going to provide the template with the current index of the loop. We’re doing this because we’re going to replace the ngFor loop we have in the component template with this new directive. The original loop uses the index for the numbers in the buttons. Let’s tackle the template next.

Updating the template

Our new structural directive is ready. Let’s update the <ng-container> element that we applied the ngFor directive to with the appLoop directive.

Get hands-on with 1200+ tech skills courses.