SKILL.md
readonlyread-only
name
angular-component
description
Create modern Angular standalone components following v20+ best practices. Use for building UI components with signal-based inputs/outputs, OnPush change detection, host bindings, content projection, and lifecycle hooks. Triggers on component creation, refactoring class-based inputs to signals, adding host bindings, or implementing accessible interactive components.
Angular Component
為 Angular v20+ 建立獨立元件。元件預設為獨立,請勿設定 standalone: true。
元件結構
import { Component, ChangeDetectionStrategy, input, output, computed } from '@angular/core';
@Component({
selector: 'app-user-card',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'user-card',
'[class.active]': 'isActive()',
'(click)': 'handleClick()',
},
template: `
<img [src]="avatarUrl()" [alt]="name() + ' avatar'" />
<h2>{{ name() }}</h2>
@if (showEmail()) {
<p>{{ email() }}</p>
}
`,
styles: `
:host { display: block; }
:host.active { border: 2px solid blue; }
`,
})
export class UserCard {
// 必要輸入
name = input.required<string>();
// 選擇性輸入,含預設值
email = input<string>('');
showEmail = input(false);
// 含轉換函式的輸入
isActive = input(false, { transform: booleanAttribute });
// 從輸入計算而來
avatarUrl = computed(() => `https://api.example.com/avatar/${this.name()}`);
// 輸出
selected = output<string>();
handleClick() {
this.selected.emit(this.name());
}
}
信號輸入
// 必要 - 必須由父元件提供
name = input.required<string>();
// 選擇性,含預設值
count = input(0);
// 選擇性,無預設值(允許 undefined)
label = input<string>();
// 含別名,用於模板綁定
size = input('medium', { alias: 'buttonSize' });
// 含轉換函式
disabled = input(false, { transform: booleanAttribute });
value = input(0, { transform: numberAttribute });
信號輸出
import { output, outputFromObservable } from '@angular/core';
// 基本輸出
clicked = output<void>();
selected = output<Item>();
// 含別名
valueChange = output<number>({ alias: 'change' });
// 從 Observable(用於 RxJS 互通)
scroll$ = new Subject<number>();
scrolled = outputFromObservable(this.scroll$);
// 發射值
this.clicked.emit();
this.selected.emit(item);
宿主綁定
在 @Component 中使用 host 物件,請勿使用 @HostBinding 或 @HostListener 裝飾器。
@Component({
selector: 'app-button',
host: {
// 靜態屬性
'role': 'button',
// 動態類別綁定
'[class.primary]': 'variant() === "primary"',
'[class.disabled]': 'disabled()',
// 動態樣式綁定
'[style.--btn-color]': 'color()',
// 屬性綁定
'[attr.aria-disabled]': 'disabled()',
'[attr.tabindex]': 'disabled() ? -1 : 0',
// 事件監聽
'(click)': 'onClick($event)',
'(keydown.enter)': 'onClick($event)',
'(keydown.space)': 'onClick($event)',
},
template: `<ng-content />`,
})
export class Button {
variant = input<'primary' | 'secondary'>('primary');
disabled = input(false, { transform: booleanAttribute });
color = input('#007bff');
clicked = output<void>();
onClick(event: Event) {
if (!this.disabled()) {
this.clicked.emit();
}
}
}
內容投影
@Component({
selector: 'app-card',
template: `
<header>
<ng-content select="[card-header]" />
</header>
<main>
<ng-content />
</main>
<footer>
<ng-content select="[card-footer]" />
</footer>
`,
})
export class Card {}
// 使用方式:
// <app-card>
// <h2 card-header>標題</h2>
// <p>主要內容</p>
// <button card-footer>動作</button>
// </app-card>
生命週期鉤子
import { OnDestroy, OnInit, afterNextRender, afterRender } from '@angular/core';
export class My implements OnInit, OnDestroy {
constructor() {
// 用於渲染後的 DOM 操作(SSR 安全)
afterNextRender(() => {
// 首次渲染後執行一次
});
afterRender(() => {
// 每次渲染後執行
});
}
ngOnInit() { /* 元件初始化 */ }
ngOnDestroy() { /* 清理 */ }
}
無障礙需求
元件必須:
- 通過 AXE 無障礙檢查
- 符合 WCAG AA 標準
- 為互動元素包含適當的 ARIA 屬性
- 支援鍵盤導航
- 維持可見的焦點指示器
@Component({
selector: 'app-toggle',
host: {
'role': 'switch',
'[attr.aria-checked]': 'checked()',
'[attr.aria-label]': 'label()',
'tabindex': '0',
'(click)': 'toggle()',
'(keydown.enter)': 'toggle()',
'(keydown.space)': 'toggle(); $event.preventDefault()',
},
template: `<span class="toggle-track"><span class="toggle-thumb"></span></span>`,
})
export class Toggle {
label = input.required<string>();
checked = input(false, { transform: booleanAttribute });
checkedChange = output<boolean>();
toggle() {
this.checkedChange.emit(!this.checked());
}
}
模板語法
使用原生控制流程,請勿使用 *ngIf、*ngFor、*ngSwitch。
<!-- 條件判斷 -->
@if (isLoading()) {
<app-spinner />
} @else if (error()) {
<app-error [message]="error()" />
} @else {
<app-content [data]="data()" />
}
<!-- 迴圈 -->
@for (item of items(); track item.id) {
<app-item [item]="item" />
} @empty {
<p>沒有找到項目</p>
}
<!-- 切換 -->
@switch (status()) {
@case ('pending') { <span>待處理</span> }
@case ('active') { <span>啟用中</span> }
@default { <span>未知</span> }
}
類別與樣式綁定
請勿使用 ngClass 或 ngStyle。使用直接綁定:
<!-- 類別綁定 -->
<div [class.active]="isActive()">單一類別</div>
<div [class]="classString()">類別字串</div>
<!-- 樣式綁定 -->
<div [style.color]="textColor()">樣式文字</div>
<div [style.width.px]="width()">含單位</div>
圖片
對於靜態圖片,使用 NgOptimizedImage:
import { NgOptimizedImage } from '@angular/common';
@Component({
imports: [NgOptimizedImage],
template: `
<img ngSrc="/assets/hero.jpg" width="800" height="600" priority />
<img [ngSrc]="imageUrl()" width="200" height="200" />
`,
})
export class Hero {
imageUrl = input.required<string>();
}
如需詳細模式,請參閱 references/component-patterns.md。






