angular-component by analogjs/angular-skills
npx skills add https://github.com/analogjs/angular-skills --skill 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 });
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
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。
每周安装量
4.5K
代码仓库
GitHub 星标
498
首次出现
2026年1月24日
安全审计
安装于
github-copilot3.7K
opencode3.7K
gemini-cli3.6K
codex3.6K
amp3.4K
kimi-cli3.4K
Create standalone components for Angular v20+. Components are standalone by default—do NOT set 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 {
// Required input
name = input.required<string>();
// Optional input with default
email = input<string>('');
showEmail = input(false);
// Input with transform
isActive = input(false, { transform: booleanAttribute });
// Computed from inputs
avatarUrl = computed(() => `https://api.example.com/avatar/${this.name()}`);
// Output
selected = output<string>();
handleClick() {
this.selected.emit(this.name());
}
}
// Required - must be provided by parent
name = input.required<string>();
// Optional with default value
count = input(0);
// Optional without default (undefined allowed)
label = input<string>();
// With alias for template binding
size = input('medium', { alias: 'buttonSize' });
// With transform function
disabled = input(false, { transform: booleanAttribute });
value = input(0, { transform: numberAttribute });
import { output, outputFromObservable } from '@angular/core';
// Basic output
clicked = output<void>();
selected = output<Item>();
// With alias
valueChange = output<number>({ alias: 'change' });
// From Observable (for RxJS interop)
scroll$ = new Subject<number>();
scrolled = outputFromObservable(this.scroll$);
// Emit values
this.clicked.emit();
this.selected.emit(item);
Use the host object in @Component—do NOT use @HostBinding or @HostListener decorators.
@Component({
selector: 'app-button',
host: {
// Static attributes
'role': 'button',
// Dynamic class bindings
'[class.primary]': 'variant() === "primary"',
'[class.disabled]': 'disabled()',
// Dynamic style bindings
'[style.--btn-color]': 'color()',
// Attribute bindings
'[attr.aria-disabled]': 'disabled()',
'[attr.tabindex]': 'disabled() ? -1 : 0',
// Event listeners
'(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 {}
// Usage:
// <app-card>
// <h2 card-header>Title</h2>
// <p>Main content</p>
// <button card-footer>Action</button>
// </app-card>
import { OnDestroy, OnInit, afterNextRender, afterRender } from '@angular/core';
export class My implements OnInit, OnDestroy {
constructor() {
// For DOM manipulation after render (SSR-safe)
afterNextRender(() => {
// Runs once after first render
});
afterRender(() => {
// Runs after every render
});
}
ngOnInit() { /* Component initialized */ }
ngOnDestroy() { /* Cleanup */ }
}
Components MUST:
Pass AXE accessibility checks
Meet WCAG AA standards
Include proper ARIA attributes for interactive elements
Support keyboard navigation
Maintain visible focus indicators
@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()); } }
Use native control flow—do NOT use *ngIf, *ngFor, *ngSwitch.
<!-- Conditionals -->
@if (isLoading()) {
<app-spinner />
} @else if (error()) {
<app-error [message]="error()" />
} @else {
<app-content [data]="data()" />
}
<!-- Loops -->
@for (item of items(); track item.id) {
<app-item [item]="item" />
} @empty {
<p>No items found</p>
}
<!-- Switch -->
@switch (status()) {
@case ('pending') { <span>Pending</span> }
@case ('active') { <span>Active</span> }
@default { <span>Unknown</span> }
}
Do NOT use ngClass or ngStyle. Use direct bindings:
<!-- Class bindings -->
<div [class.active]="isActive()">Single class</div>
<div [class]="classString()">Class string</div>
<!-- Style bindings -->
<div [style.color]="textColor()">Styled text</div>
<div [style.width.px]="width()">With unit</div>
Use NgOptimizedImage for static images:
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>();
}
For detailed patterns, see references/component-patterns.md.
Weekly Installs
4.5K
Repository
GitHub Stars
498
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot3.7K
opencode3.7K
gemini-cli3.6K
codex3.6K
amp3.4K
kimi-cli3.4K
97,600 周安装