- Create comprehensive html-template skill with 3 reference files - E2E Testing Attributes (data-what, data-which patterns) - ARIA Accessibility Attributes (roles, properties, states, WCAG) - Combined Patterns (real-world examples with both) - Move E2E attribute guidance from command to skill - Add extensive ARIA accessibility documentation - Update angular-template skill with cross-references - Remove dev-add-e2e-attrs command (functionality now in skill) The new skill provides 3,322 lines of comprehensive documentation covering both testing and accessibility best practices for HTML templates, with practical examples for forms, navigation, tables, dialogs, and more. Benefits: - Clean separation: Angular syntax vs HTML attributes - Reusable: html-template works with any HTML context - Comprehensive: E2E + ARIA in one place - Integrated: Works seamlessly with angular-template skill
6.3 KiB
name, description
| name | description |
|---|---|
| angular-template | This skill should be used when writing or reviewing Angular component templates. It provides guidance on modern Angular 20+ template syntax including control flow (@if, @for, @switch, @defer), content projection (ng-content), template references (ng-template, ng-container), variable declarations (@let), and expression binding. Use when creating components, refactoring to modern syntax, implementing lazy loading, or reviewing template best practices. |
Angular Template
Guide for modern Angular 20+ template patterns: control flow, lazy loading, projection, and binding.
When to Use
- Creating/reviewing component templates
- Refactoring legacy
*ngIf/*ngFor/*ngSwitchto modern syntax - Implementing
@deferlazy loading - Designing reusable components with
ng-content - Template performance optimization
Related Skill: For E2E testing attributes (data-what, data-which) and ARIA accessibility attributes, see the html-template skill. Both skills work together when writing Angular templates.
Control Flow (Angular 17+)
@if / @else if / @else
@if (user.isAdmin()) {
<app-admin-dashboard />
} @else if (user.isEditor()) {
<app-editor-dashboard />
} @else {
<app-viewer-dashboard />
}
// Store result with 'as'
@if (user.profile?.settings; as settings) {
<p>Theme: {{settings.theme}}</p>
}
@for with @empty
@for (product of products(); track product.id) {
<app-product-card [product]="product" />
} @empty {
<p>No products available</p>
}
CRITICAL: Always provide track expression:
- Best:
track item.idortrack item.uuid - Static lists:
track $index - NEVER:
track identity(item)(causes full re-render)
Contextual variables: $count, $index, $first, $last, $even, $odd
@switch
@switch (viewMode()) {
@case ('grid') { <app-grid-view /> }
@case ('list') { <app-list-view /> }
@default { <app-grid-view /> }
}
@defer Lazy Loading
Basic Usage
@defer (on viewport) {
<app-heavy-chart />
} @placeholder (minimum 500ms) {
<div class="skeleton"></div>
} @loading (after 100ms; minimum 1s) {
<mat-spinner />
} @error {
<p>Failed to load</p>
}
Triggers
| Trigger | Use Case |
|---|---|
idle (default) |
Non-critical features |
viewport |
Below-the-fold content |
interaction |
User-initiated (click/keydown) |
hover |
Tooltips/popovers |
timer(Xs) |
Delayed content |
when(expr) |
Custom condition |
Multiple triggers: @defer (on interaction; on timer(5s))
Prefetching: @defer (on interaction; prefetch on idle)
Requirements
- Components MUST be standalone
- No
@ViewChild/@ContentChildreferences - Reserve space in
@placeholderto prevent layout shift
Best Practices
- ✅ Defer below-the-fold content
- ❌ Never defer above-the-fold (harms LCP)
- ❌ Avoid
immediate/timerduring initial render (harms TTI) - Test with network throttling
Content Projection
Single Slot
@Component({
selector: 'ui-card',
template: `<div class="card"><ng-content></ng-content></div>`
})
Multi-Slot with Selectors
@Component({
template: `
<header><ng-content select="card-header"></ng-content></header>
<main><ng-content select="card-body"></ng-content></main>
<footer><ng-content></ng-content></footer> <!-- default slot -->
`
})
Usage:
<ui-card>
<card-header><h3>Title</h3></card-header>
<card-body><p>Content</p></card-body>
<button>Action</button> <!-- goes to default slot -->
</ui-card>
Fallback content: <ng-content select="title">Default Title</ng-content>
Aliasing: <h3 ngProjectAs="card-header">Title</h3>
CRITICAL Constraint
ng-content always instantiates (even if hidden). For conditional projection, use ng-template + NgTemplateOutlet.
Template References
ng-template
<ng-template #userCard let-user="userData" let-index="i">
<div class="user">#{{index}}: {{user.name}}</div>
</ng-template>
<ng-container
*ngTemplateOutlet="userCard; context: {userData: currentUser(), i: 0}">
</ng-container>
Access in component:
myTemplate = viewChild<TemplateRef<unknown>>('myTemplate');
ng-container
Groups elements without DOM footprint:
<p>
Hero's name is
<ng-container @if="hero()">{{hero().name}}</ng-container>.
</p>
Variables
@let (Angular 18.1+)
@let userName = user().name;
@let greeting = 'Hello, ' + userName;
@let asyncData = data$ | async;
<h1>{{greeting}}</h1>
Scoped to current view (not hoisted to parent/sibling).
Template References (#)
<input #emailInput type="email" />
<button (click)="sendEmail(emailInput.value)">Send</button>
<app-datepicker #startDate />
<button (click)="startDate.open()">Open</button>
Binding Patterns
Property: [disabled]="!isValid()"
Attribute: [attr.aria-label]="label()" [attr.data-what]="'card'"
Event: (click)="save()" (input)="onInput($event)"
Two-way: [(ngModel)]="userName"
Class: [class.active]="isActive()" or [class]="{active: isActive()}"
Style: [style.width.px]="width()" or [style]="{color: textColor()}"
Best Practices
- Use signals:
isExpanded = signal(false) - Prefer control flow over directives: Use
@ifnot*ngIf - Keep expressions simple: Use
computed()for complex logic - Testing & Accessibility: Always add E2E and ARIA attributes (see html-template skill)
- Track expressions: Required in
@for, use unique IDs
Migration
| Legacy | Modern |
|---|---|
*ngIf="condition" |
@if (condition) { } |
*ngFor="let item of items" |
@for (item of items; track item.id) { } |
[ngSwitch] |
@switch (value) { @case ('a') { } } |
CLI migration: ng generate @angular/core:control-flow
Reference Files
For detailed examples and edge cases, see:
references/control-flow-reference.md- @if/@for/@switch patternsreferences/defer-patterns.md- Lazy loading strategiesreferences/projection-patterns.md- Advanced ng-contentreferences/template-reference.md- ng-template/ng-container
Search with: grep -r "pattern" references/