Red Green Repeat Adventures of a Spec Driven Junkie

Learning Angular: Directives - Custom

I am continuing from my last Learning Angular article. This time, I write a custom directive using Angular’s documentation to write a custom appUnless directive to write my own: appFor2 - a specialized for directive.

By reading this article, you will understand how to create your own directive and change an item in Angular’s TemplateRef.

This article will take you less than five minutes to read.

Christopher Dresser - Spoon Holder source and more information

Introduction

As I used ngFor in my ngFor article, it annoyed me that I couldn’t not write the loop in this form:

<ul *ngFor="let item in Object.keys(items)">
  <li> {\{ item }\} </li>
</ul>

Angular does not execute code past one level in the template, (i.e. Object.keys(items) is a string in the template) there needs to be a temporary variable to hold the keys of items.

In the Angular documentation, the document how to create your own directive, I thought: why not create a directive so it would be:

<ul *appFor2="items">
  <li>{\{ item }\}</li>
</ul>

This should be easy, right?!

Starting with unless

Angular documentation shows how to make custom directive for the unless function.

Cool, this is a good starting point as:

  • it has the setup to create a new directive
  • it’s similar to what I want to make
  • not part of the ‘standard’ library

I’m basically halfway there!

Following Along

If you would like to follow along with this code, you can use the Github link to get a final copy of the code or view the project in your browser using this StackBlitz link.

Port unless into my project

To get a better feel of whats going on, I translate the unless function into my application along with its infrastructure. Everywhere there is an unless, I replace with For2 or its equivalent.

The app.module.ts file:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { For2Directive } from './for2.directive';

@NgModule({
  declarations: [
	  AppComponent,
	  For2Directive,
  ],
  imports: [
	  BrowserModule,
	  FormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

The for2.directive.ts file:

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

@Directive({ selector: '[appFor2]'})
export class For2Directive {
	constructor(
		private templateRef: TemplateRef<any>,
		private viewContainer: ViewContainerRef) { }

	@Input() set appFor2(items: Array<any>) {
		console.log("hi from appFor2");
	}
}

Without this example in the documentation, I would not have such an easy time setting up a custom directive in Angular.

appFor2 v.0

Instead of keeping the appFor2 function body the same as the original unless, I change the appFor2 function to the following:

@Input() set appFor2(items: Array<any>) {
  console.log("hi from appFor2");
}
  • changing condition: <boolean> to: items: Array<any> sets my function to accept an array of <any> item instead of a boolean.
  • using console.log("hi from appFor2"); validates the configuration through a simple message in the console.

Using appFor2

To use appFor2, the app.component.html file needs an entry, let’s keep it simple and reuse the words list from my ngFor article:

<ul *appFor2="words">
  <li>  </li>
</ul>

Let’s run the application, open our browser and make sure things are working.

Nothing on the page, which in this case, is a good sign.

Let’s open the browser’s console and see if there’s anything:

console.log output

Great - appFor2 sends a message to the console. This means we can execute any code in appFor2.

Let’s get a sweet for function going!

Next Step

In the documentation, unless had this code in it:

if (!condition && !this.hasView) {
  this.viewContainer.createEmbeddedView(this.templateRef);
  this.hasView = true;
}

The this.viewContainer.createEmbeddedView(this.templateRef); looks important. Let’s add this to our appFor2 function in the following manner:

@Input() set appFor2(items: Array<any>) {
  for(var item of Object.keys(items)) {
    let view = this.viewContainer.createEmbeddedView(this.templateRef);
  }
}

Let’s run it and see what we get in our browser:

Four bullet points

Interesting, there’s four bullets with nothing in them.

The fact that there are bullet points and not just “empty space” is important. That means the viewContainer has <li> element details and Angular passes them in through templateRef.

How can the appFor2 function put the contents of the items array into the viewContainer to populate the bullet points?

Digging around

Honestly, I was looking around the Angular documentation with a hope I would find an article on working with TemplateRef. The documentation on TemplateRef did not have the details I wanted. I did find another article that pointed me the right direction!

The TemplateRef is an abstraction and designed so Angular can render into anything: different web browsers, mobile devices, native applications.

Digging through TemplateRef by outputting the object onto the console using console.log(view) let me interactively poke it.

I kept poking around until I found the textContent entry, which the above article mentions.

Let’s see if setting this textContent value as: item will do anything using the following code:

@Input() set appFor2(items: Array<any>) {
  for(var item of Object.keys(items)) {
    let view = this.viewContainer.createEmbeddedView(this.templateRef);
    console.log(view)
    view.rootNodes[0].children[0].textContent = item;
  }
}

appFor2 custom list

Wow, that works!!!

Wait a minute…

That worked for a list, what if we use appFor2 in a paragraph? Like so:

<p *appFor2="words">
  <b>{\{ item }\}</b>
</p>

Looking at the page, the result is:

appFor2 with paragraph markup

Wow, that’s works too. I basically have a new implementation for for the way I want, in a <ul>, <p>, and more.

Conclusion

Using the Angular documentation for creating a custom directive as a basis, I created my own custom directive that is a for loop, done the way I want.

Finding the way to manage the TemplateRef was the hardest part and took trial and error.

I won’t go making my own custom for loop every time I want to work with a list of items. This exercise definitely gives me a little understanding of what’s going on in TemplateRef and how display items.