Custom structural directives use TemplateRef (the template fragment between the directive’s tags) and ViewContainerRef (the location in the DOM where views are inserted). They give you the same power as *ngIf and *ngFor — conditionally rendering or repeating DOM — with your own logic. The microsyntax (*directive="expression") is sugar that Angular’s compiler expands into the property binding form ([directive]="expression" with ng-template).
Custom Structural Directives
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';
// ── *appUnless — opposite of *ngIf ────────────────────────────────────────
// Usage: <div *appUnless="isLoggedIn">Please log in.</div>
// Microsyntax expands to:
// <ng-template [appUnless]="isLoggedIn"><div>Please log in.</div></ng-template>
@Directive({
selector: '[appUnless]',
standalone: true,
})
export class UnlessDirective {
private template = inject(TemplateRef<any>);
private viewContainer = inject(ViewContainerRef);
private hasView = false;
@Input({ alias: 'appUnless', required: true })
set condition(value: boolean) {
if (!value && !this.hasView) {
this.viewContainer.createEmbeddedView(this.template);
this.hasView = true;
} else if (value && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
// ── *appRepeat — renders template N times with index context ──────────────
// Usage: <div *appRepeat="5; let i = index">Item {{ i + 1 }}</div>
// Also: <app-skeleton-card *appRepeat="3" />
export interface RepeatContext {
$implicit: number; // the index (accessible as 'let i')
index: number; // same as $implicit
count: number; // total count
first: boolean;
last: boolean;
}
@Directive({
selector: '[appRepeat]',
standalone: true,
})
export class RepeatDirective implements OnChanges {
private template = inject(TemplateRef<RepeatContext>);
private viewContainer = inject(ViewContainerRef);
@Input({ alias: 'appRepeat', required: true }) count = 0;
ngOnChanges(): void {
this.viewContainer.clear();
for (let i = 0; i < this.count; i++) {
this.viewContainer.createEmbeddedView(this.template, {
$implicit: i, // 'let i' in the template
index: i,
count: this.count,
first: i === 0,
last: i === this.count - 1,
} as RepeatContext);
}
}
// Required for type safety of context variables in templates:
static ngTemplateContextGuard(
dir: RepeatDirective,
ctx: unknown
): ctx is RepeatContext {
return true;
}
}
// ── Usage examples ────────────────────────────────────────────────────────
// <!-- Skeleton loading cards ─────────────────────────────────── -->
// <app-skeleton-card *appRepeat="isLoading() ? 6 : 0" />
//
// <!-- Star rating display ─────────────────────────────────────── -->
// <mat-icon *appRepeat="rating; let i; let isLast = last">
// {{ i < value ? 'star' : 'star_border' }}
// </mat-icon>
$implicit context property maps to the unnamed let variable in the microsyntax (*appRepeat="5; let i" — i receives $implicit). Named context properties require explicit names: *appRepeat="5; let first = first". This mirrors how *ngFor works — let item of items maps each item to $implicit, while let i = index maps to the named index context property. The ngTemplateContextGuard static method enables TypeScript type inference for template context variables.@for but not worth a full component. A *appSkeleton="count" directive that shows count skeleton placeholder cards during loading avoids creating a wrapper component just to handle the loading state loop. The directive sits directly on the skeleton card component: <app-post-card-skeleton *appSkeleton="3" />. This keeps templates clean and purposeful.@if, @for, and @switch control flow covers most use cases for custom structural directives. Before writing a custom structural directive, verify whether the new built-in syntax can handle the requirement — it almost certainly can, and it has better TypeScript type narrowing. Reserve custom structural directives for genuinely novel control flow patterns (retry loops, permission gates, feature flags) that do not map to any existing control flow primitive.Common Mistakes
Mistake 1 — Not clearing ViewContainerRef before re-rendering (duplicate views)
❌ Wrong — calling createEmbeddedView() on every count change without viewContainer.clear() first; views accumulate.
✅ Correct — always call viewContainer.clear() in ngOnChanges before recreating views.
Mistake 2 — Missing ngTemplateContextGuard (TypeScript cannot infer context variable types)
❌ Wrong — no ngTemplateContextGuard; TypeScript types context variables as any inside the template.
✅ Correct — implement the static ngTemplateContextGuard method for proper type inference of template context variables.