1. Custom Attribute Directives & Host Bindings
An attribute directive attaches behavior to an existing element without changing what's
rendered — exactly the same host object you used on components in Week 7,
just on a class with @Directive instead of @Component.
import { Directive, input, signal } from '@angular/core';
@Directive({
selector: '[appHighlight]',
host: {
'[style.background-color]': 'color()',
'(mouseenter)': 'color.set(highlightColor())',
'(mouseleave)': "color.set('')",
},
})
export class Highlight {
highlightColor = input('#fff3cd', { alias: 'appHighlight' });
protected color = signal('');
}
<p appHighlight="#d1e7ff">Hover to highlight this paragraph.</p>
The alias option lets the directive's selector itself double as the input
name — a common Angular convention you've likely noticed with directives like
[ngClass] long before now.
2. Custom Structural Directives
Before @if/@for existed as built-in syntax, every conditional
and loop was a structural directive — *ngIf, *ngFor — and they
worked by manipulating two low-level primitives: TemplateRef (a reference to
the content marked with the asterisk) and ViewContainerRef (a place to
insert or remove that content from the DOM).
import { Directive, input, effect, TemplateRef, ViewContainerRef, inject } from '@angular/core';
@Directive({
selector: '[appUnless]',
})
export class Unless {
private templateRef = inject(TemplateRef);
private viewContainerRef = inject(ViewContainerRef);
condition = input.required<boolean>({ alias: 'appUnless' });
constructor() {
effect(() => {
this.viewContainerRef.clear();
if (!this.condition()) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
}
});
}
}
<p *appUnless="isLoading()">Content shown only once loading finishes.</p>
You'll almost never write one of these day to day — @if/@for
cover the overwhelming majority of cases — but seeing exactly how *ngIf
worked demystifies the asterisk syntax you'll still encounter in older codebases, and the
same TemplateRef/ViewContainerRef pair shows up again anywhere
you dynamically render content, including NgTemplateOutlet.
3. Pure vs. Impure Pipes
A pipe transforms a value for display — {{ value | myPipe }}. By default,
pipes are pure: Angular only re-runs the transform when the input's
reference changes, not on every change-detection cycle.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
// pure: true is the default -- shown here for clarity
})
export class Truncate implements PipeTransform {
transform(value: string, maxLength = 80): string {
return value.length > maxLength ? value.slice(0, maxLength) + '…' : value;
}
}
<p>{{ article.body | truncate:120 }}</p>
An { pure: false } pipe re-runs on every change-detection cycle, regardless of whether its input actually changed — a real performance cost in a large template. It's occasionally justified for a pipe that needs to watch a mutating array or object in place, but reach for it only after confirming a pure pipe genuinely can't do the job (usually by making the input immutable instead).
4. CDK Overlay, Portal & DragDrop Basics
The CDK ships the hard, easy-to-get-subtly-wrong parts of common UI patterns as standalone, unstyled building blocks — you supply the visuals, the CDK handles positioning, focus, and interaction logic.
- Overlay — renders floating content (tooltips, menus, dropdowns) positioned relative to a trigger element, correctly handling viewport edges and scrolling.
- Portal — a piece of content (component or template) that can be dynamically attached somewhere else in the DOM, which is exactly what the Overlay service uses internally.
- DragDrop — sortable lists and drag targets via directives, no manual pointer-event math.
<div cdkDropList (cdkDropListDropped)="onDrop($event)">
@for (task of tasks(); track task.id) {
<div cdkDrag class="task-row">{{ task.title }}</div>
}
</div>
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
onDrop(event: CdkDragDrop<Task[]>) {
this.tasks.update((list) => {
const copy = [...list];
moveItemInArray(copy, event.previousIndex, event.currentIndex);
return copy;
});
}
moveItemInArray is a small utility, but it saves you from hand-writing
splice logic that's easy to get off-by-one wrong — a good example of the CDK's general
shape: small, focused, composable pieces rather than an all-in-one UI kit.
5. CDK A11y Utilities
You used cdkTrapFocus in last week's modal exercise without a deep
explanation — this section is that explanation, plus one more tool worth knowing.
import { Component, inject } from '@angular/core';
import { LiveAnnouncer } from '@angular/cdk/a11y';
@Component({
selector: 'app-delete-button',
template: `<button (click)="delete()">Delete</button>`,
})
export class DeleteButton {
private announcer = inject(LiveAnnouncer);
delete() {
// ...actually delete the item...
this.announcer.announce('Item deleted', 'assertive');
}
}
LiveAnnouncer injects a visually-hidden, screen-reader-only element and
updates its text — the standard technique for telling assistive-technology users about a
change that has no natural place to announce itself (nothing moved focus, no dialog
opened, the row just silently disappeared). cdkTrapFocus solves the
companion problem: once a modal or menu is open, Tab shouldn't be able to move focus to
anything behind it.
6. When to Build vs. Reach for a Library
A few questions worth asking before writing a directive or pipe from scratch:
- Does the CDK already solve this? Overlay positioning, focus trapping, drag-and-drop, and virtual scrolling (Week 20) are all deceptively hard to get right — favor the CDK's battle-tested version over a bespoke one.
- Is this genuinely reusable, or a one-off? A directive used in exactly one template is often better as plain logic in that one component.
- Does a pipe need to be impure? Almost always, restructuring the data (making it immutable, or precomputing the display value) is better than paying the performance cost of an impure pipe.
This isn't a rule to memorize so much as a habit to build: the CDK exists precisely so you're not re-solving accessibility and positioning problems the Angular team has already solved carefully.
7. Hands-on Exercise
Build a clickOutside directive and a small reusable pipe library
Two focused pieces this week, both reusable across the capstone project later.
Requirements — directive:
- Create
[appClickOutside], an attribute directive that emits anappClickOutsideoutput event whenever a click occurs outside the host element. - Implement it with a
host: { '(document:click)': '...' }listener, checking whether the click target is contained within the host viaElementRef. - Wire it into last week's
Modalcomponent so clicking the backdrop closes it — alongside the existing Escape-key handler.
Requirements — pipe library:
currency— formats a number as currency (you may useIntl.NumberFormatinternally).relativeTime— formats aDateas "3 minutes ago" / "2 days ago" (you may useIntl.RelativeTimeFormat).truncate— from Section 3, with a configurable max length argument.- All three should be pure pipes. Write one unit test per pipe confirming the output for at least two inputs each.
If appClickOutside fires immediately when the modal opens (because the triggering click hasn't finished bubbling yet), that's a real timing bug worth chasing — not something to work around by disabling the directive on open.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What two primitives does a structural directive use to add or remove content from the DOM?
What two primitives does a structural directive use to add or remove content from the DOM?
TemplateRef, a reference to the content marked with the asterisk, and ViewContainerRef, the location where that content can be inserted or removed. createEmbeddedView() renders it in; clear() removes it.
Q2
Why does a pure pipe re-run only when its input reference changes, and why is that usually desirable?
Why does a pure pipe re-run only when its input reference changes, and why is that usually desirable?
It's a deliberate performance optimization — Angular can skip re-running the transform entirely if the reference is identical to last time, on the assumption that unchanged references mean unchanged data. It's usually desirable because most values in a well-structured Angular app are treated immutably (new object/array on change, not mutated in place), so reference equality is a cheap, accurate proxy for "did this actually change."
Q3
What problem does LiveAnnouncer solve that simply updating the DOM doesn't?
What problem does LiveAnnouncer solve that simply updating the DOM doesn't?
Screen readers only narrate content changes in specific, predictable situations (focus moving, certain ARIA live regions). A row silently vanishing from a list — with focus staying put and no dialog involved — produces no automatic announcement at all. LiveAnnouncer creates and updates a dedicated ARIA live region specifically so changes like that get spoken.
Q4
Before writing a custom directive for a floating, positioned menu, what should you check first — and why?
Before writing a custom directive for a floating, positioned menu, what should you check first — and why?
Whether the CDK's Overlay service already covers it — which, for floating positioned content, it almost certainly does. Positioning relative to a trigger while correctly handling viewport edges, scrolling, and z-index stacking is exactly the kind of "looks simple, is full of edge cases" problem the CDK exists to solve once, well, instead of every app solving it slightly differently.