Post contents
Angular is a powerful framework. Most folks know of it as the component framework, but it's much more than that.
For example, did you know about Angular directives?
Directives allow you to bind to an element via an attribute and change the behavior of said element.
import { Component, Directive } from '@angular/core';@Directive({ selector: '[doNothing]', standalone: true,})class DoNothingDirective {}@Component({ selector: 'app-root', standalone: true, imports: [DoNothingDirective], template: ` <p doNothing>I am currently unchanged.</p>`,})class AppComponent {}
Do Nothing Directive - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component, Directive } from "@angular/core";@Directive({ selector: "[doNothing]", standalone: true,})class DoNothingDirective {}@Component({ selector: "app-root", standalone: true, imports: [DoNothingDirective], template: ` <p doNothing>I am currently unchanged.</p> `,})class AppComponent {}bootstrapApplication(AppComponent);
Think of them as components without templates. They can use lifecycle methods:
@Directive({ selector: '[alertOnDestroy]', standalone: true,})class AlertOnDestroyDirective implements OnDestroy { ngOnDestroy() { alert('Element was unrendered!'); }}@Component({ selector: 'app-root', standalone: true, imports: [AlertOnDestroyDirective, NgIf], template: ` <p *ngIf="render" alertOnDestroy>Unmount me to see an alert!</p> <button (click)="render = !render">Toggle</button>`,})class AppComponent { render = true;}
Alert On Destroy - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component, Directive, OnDestroy } from "@angular/core";import { NgIf } from "@angular/common";@Directive({ selector: "[alertOnDestroy]", standalone: true,})class AlertOnDestroyDirective implements OnDestroy { ngOnDestroy() { alert("Element was unrendered!"); }}@Component({ selector: "app-root", standalone: true, imports: [AlertOnDestroyDirective, NgIf], template: ` <p *ngIf="render" alertOnDestroy>Unmount me to see an alert!</p> <button (click)="render = !render">Toggle</button> `,})class AppComponent { render = true;}bootstrapApplication(AppComponent);
Store state:
@Directive({ selector: '[listenForEvents]', standalone: true,})class ListenForEventDirective implements OnInit { count = 0; ngOnInit() { document.addEventListener('hello', () => { alert(`You sent this many events: ${++this.count}`); }); }}@Component({ selector: 'app-root', standalone: true, imports: [ListenForEventDirective], template: ` <p listenForEvents>This paragraph tag listens for events!</p> <button (click)="sendEvent()">Send event</button>`,})class AppComponent { sendEvent() { const event = new CustomEvent('hello'); document.dispatchEvent(event); }}
Listen for Events - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component, Directive, OnInit } from "@angular/core";@Directive({ selector: "[listenForEvents]", standalone: true,})class ListenForEventDirective implements OnInit { count = 0; ngOnInit() { document.addEventListener("hello", () => { alert(`You sent this many events: ${++this.count}`); }); }}@Component({ selector: "app-root", standalone: true, imports: [ListenForEventDirective], template: ` <p listenForEvents>This paragraph tag listens for events!</p> <button (click)="sendEvent()">Send event</button> `,})class AppComponent { sendEvent() { const event = new CustomEvent("hello"); document.dispatchEvent(event); }}bootstrapApplication(AppComponent);
Use the inject
function:
import { Component, Directive, inject, OnInit } from '@angular/core';import { DOCUMENT } from '@angular/common';@Directive({ selector: '[listenForEvents]', standalone: true,})class ListenForEventDirective implements OnInit { count = 0; doc = inject(DOCUMENT); ngOnInit() { this.doc.addEventListener('hello', () => { alert(`You sent this many events: ${++this.count}`); }); }}
Listen for Events Inject - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component, Directive, inject, OnInit } from "@angular/core";import { DOCUMENT } from "@angular/common";@Directive({ selector: "[listenForEvents]", standalone: true,})class ListenForEventDirective implements OnInit { count = 0; doc = inject(DOCUMENT); ngOnInit() { this.doc.addEventListener("hello", () => { alert(`You sent this many events: ${++this.count}`); }); }}@Component({ selector: "app-root", standalone: true, imports: [ListenForEventDirective], template: ` <p listenForEvents>This paragraph tag listens for events!</p> <button (click)="sendEvent()">Send event</button> `,})class AppComponent { sendEvent() { const event = new CustomEvent("hello"); document.dispatchEvent(event); }}bootstrapApplication(AppComponent);
And do just about anything else a component can do without a template of its own.
Accessing a directives' element with ElementRef
Because a directive is attached to an element, a typical usage of a directive is to modify the element it's attached to using ElementRef
and inject
; like so:
const injectAndGetEl = () => { const el = inject(ElementRef); console.log(el.nativeElement); return el;};@Directive({ selector: '[logEl]', standalone: true,})class LogElDirective { _el = injectAndGetEl();}
Log Element - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component, Directive, ElementRef, inject } from "@angular/core";const injectAndGetEl = () => { const el = inject(ElementRef); console.log(el.nativeElement); return el;};@Directive({ selector: "[logEl]", standalone: true,})class LogElDirective { _el = injectAndGetEl();}@Component({ selector: "app-root", standalone: true, imports: [LogElDirective], template: ` <p logEl>This paragraph tag will be logged!</p> `,})class AppComponent {}bootstrapApplication(AppComponent);
While this doesn't do anything yet, it logs the element to the console.log
method. Let's instead change this code to make the attached element have a red background and white text:
import { Component, Directive, ElementRef, inject } from '@angular/core';const injectAndMakeRed = () => { const el = inject(ElementRef); el.nativeElement.style.backgroundColor = 'red'; el.nativeElement.style.color = 'white';};@Directive({ selector: '[red]', standalone: true,})class RedDirective { _el = injectAndMakeRed();}@Component({ selector: 'app-root', standalone: true, imports: [RedDirective], template: ` <p red>This is red</p>`,})class AppComponent {}
Red Directive - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component, Directive, ElementRef, inject } from "@angular/core";const injectAndMakeRed = () => { const el = inject(ElementRef); el.nativeElement.style.backgroundColor = "red"; el.nativeElement.style.color = "white";};@Directive({ selector: "[red]", standalone: true,})class RedDirective { _el = injectAndMakeRed();}@Component({ selector: "app-root", standalone: true, imports: [RedDirective], template: ` <p red>This is red</p> `,})class AppComponent {}bootstrapApplication(AppComponent);
host
property binding
While the inject
method works, there's a better way to bind an element: the host
property.
@Directive({ selector: '[red]', standalone: true, host: { style: 'background-color: red; color: white;', },})class RedDirective {}@Component({ selector: 'app-root', standalone: true, imports: [RedDirective], template: ` <p red>This is red</p>`,})class AppComponent {}
Red Host Directive - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component, Directive } from "@angular/core";@Directive({ selector: "[red]", standalone: true, host: { style: "background-color: red; color: white;", },})class RedDirective {}@Component({ selector: "app-root", standalone: true, imports: [RedDirective], template: ` <p red>This is red</p> `,})class AppComponent {}bootstrapApplication(AppComponent);
Here, host
refers to the element the directive is attached to. We can use it to then attach new attributes to the parent element like we did above.
Dynamic host
property binding
host
isn't just useful for static attribute bindings either, you can use it with attribute binding and event listening using the same []
and ()
syntax you're familiar with:
@Directive({ selector: '[red]', standalone: true, host: { '[style]': `selected ? 'background-color: red; color: white;' : ''`, '(click)': 'selected = !selected', },})class RedDirective { selected = false;}@Component({ selector: 'app-root', standalone: true, imports: [RedDirective], template: ` <p red>This is red when I am selected</p>`,})class AppComponent {}
Red Dynamic Host Directive - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component, Directive } from "@angular/core";@Directive({ selector: "[red]", standalone: true, host: { "[style]": `selected ? 'background-color: red; color: white;' : ''`, "(click)": "selected = !selected", },})class RedDirective { selected = false;}@Component({ selector: "app-root", standalone: true, imports: [RedDirective], template: ` <p red>This is red when I am selected</p> `,})class AppComponent {}bootstrapApplication(AppComponent);
Using host
property with Components
Because components are just like directives but with a template, complete with a host element, we can use the same host
directive on components as well as directives:
@Component({ selector: 'red-div', standalone: true, host: { '[style]': `selected ? 'background-color: red; color: white;' : ''`, '(click)': 'selected = !selected', }, template: ` <span><ng-content/></span> `,})class RedDirective { selected = false;}@Component({ selector: 'app-root', standalone: true, imports: [RedDirective], template: ` <red-div>This is red when I am selected</red-div>`,})class AppComponent {}
Red Div Component - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component } from "@angular/core";@Component({ selector: "red-div", standalone: true, host: { "[style]": `selected ? 'background-color: red; color: white;' : ''`, "(click)": "selected = !selected", }, template: ` <span><ng-content /></span> `,})class RedDirective { selected = false;}@Component({ selector: "app-root", standalone: true, imports: [RedDirective], template: ` <red-div>This is red when I am selected</red-div> `,})class AppComponent {}bootstrapApplication(AppComponent);
This will output to something akin to the following Angular template:
<red-div [style]="selected ? 'background-color: red; color: white;' : ''" (click)="selected = !selected"> <span>This is red when I am selected</span></red-div>