现代Angular UI模式,用于加载状态、错误处理和数据展示。适用于构建UI组件、处理异步数据或管理组件状态。
技能概述
angular-ui-patterns 技能提供现代Angular UI模式,专注于加载状态、错误处理和数据展示。该技能帮助开发者构建用户友好的界面,正确处理异步操作和错误情况。
下载地址:https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/angular-ui-patterns
核心原则
- 永不显示过时UI:仅在真正加载时显示加载状态
- 始终展示错误:用户必须知道何时出现问题
- 乐观更新:让UI感觉即时响应
- 渐进式披露:使用@defer在内容可用时显示
- 优雅降级:部分数据优于无数据
加载状态模式
黄金法则
仅当没有数据可显示时才显示加载指示器。
@Component({
template: `
@if (error()) {
<app-error-state [error]="error()" (retry)="load()" />
} @else if (loading() && !items().length) {
<app-skeleton-list />
} @else if (!items().length) {
<app-empty-state message="No items found" />
} @else {
<app-item-list [items]="items()" />
}
`,
})
export class ItemListComponent {
private store = inject(ItemStore);
items = this.store.items;
loading = this.store.loading;
error = this.store.error;
}
加载状态决策树
决策流程
1. 是否有错误?→ 是:显示错误状态并提供重试选项
2. 是否正在加载且没有数据?→ 是:显示加载指示器(骨架屏/旋转器)
3. 是否有数据?→ 有数据:显示数据;空数据:显示空状态
骨架屏 vs 旋转器
| 使用骨架屏场景 | 使用旋转器场景 |
|---|---|
| 已知内容形状 | 未知内容形状 |
| 列表/卡片布局 | 模态操作 |
| 初始页面加载 | 按钮提交 |
| 内容占位符 | 内联操作 |
控制流模式
@if/@else条件渲染
@if (user(); as user) {
<span>Welcome, {{ user.name }}</span>
} @else if (loading()) {
<app-spinner size="small" />
} @else {
<a routerLink="/login">Sign In</a>
}
@for与track
@for (item of items(); track item.id) {
<app-item-card [item]="item" (delete)="remove(item.id)" />
} @empty {
<app-empty-state icon="inbox" message="No items yet" actionLabel="Create Item" (action)="create()" />
}
@defer渐进式加载
<!-- 关键内容立即加载 -->
<app-header />
<app-hero-section /><!-- 非关键内容延迟加载 -->
@defer (on viewport) {
<app-comments [postId]="postId()" />
} @placeholder {
<div class="h-32 bg-gray-100 animate-pulse"></div>
} @loading (minimum 200ms) {
<app-spinner />
} @error {
<app-error-state message="Failed to load comments" />
}
错误处理模式
错误处理层次
- 内联错误(字段级):表单验证错误
- Toast通知:可恢复错误,用户可重试
- 错误横幅:页面级错误,数据仍部分可用
- 全屏错误:不可恢复,需要用户操作
始终显示错误
// 正确 - 错误始终向用户显示
@Component({...})
export class CreateItemComponent {
private store = inject(ItemStore);
private toast = inject(ToastService);async create(data: CreateItemDto) {
try {
await this.store.create(data);
this.toast.success('Item created successfully');
this.router.navigate(['/items']);
} catch (error) {
console.error('createItem failed:', error);
this.toast.error('Failed to create item. Please try again.');
}
}
}// 错误 - 错误被静默捕获
async create(data: CreateItemDto) {
try {
await this.store.create(data);
} catch (error) {
console.error(error); // 用户看不到任何信息!
}
}
按钮状态模式
按钮加载状态
<button (click)="handleSubmit()" [disabled]="isSubmitting() || !form.valid" class="btn-primary">
@if (isSubmitting()) {
<app-spinner size="small" class="mr-2" />
Saving...
} @else {
Save Changes
}
</button>
空状态
空状态要求
每个列表/集合都必须有空状态:
@for (item of items(); track item.id) {
<app-item-card [item]="item" />
} @empty {
<app-empty-state icon="folder-open" title="No items yet" description="Create your first item to get started" actionLabel="Create Item" (action)="openCreateDialog()" />
}
最佳实践
- 加载状态:仅在无数据时显示加载指示器
- 错误显示:永不静默吞掉错误,始终向用户展示
- 按钮禁用:异步操作期间始终禁用触发器
- 空状态:每个列表都应有友好的空状态
- 渐进式加载:使用@defer延迟非关键内容
- 骨架屏:已知形状时优先使用骨架屏
- 乐观更新:让UI感觉即时响应
注意事项
限制:
- 仅在任务明确匹配上述范围时使用此技能
- 不要将输出视为环境特定验证、测试或专家审查的替代品
- 如果缺少必需的输入、权限、安全边界或成功标准,请停止并请求澄清