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.
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:
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:
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;
}
}
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:
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.