Skip to main content 首页 创作者 beko2210 firstbrain angular-ui-patterns
angular-ui-patterns Modern Angular UI patterns for loading states, error handling, and data display. Use when building UI components, handling async data, or managing component states.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/BEKO2210/Firstbrain --skill angular-ui-patterns命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... name angular-ui-patterns description Modern Angular UI patterns for loading states, error handling, and data display. Use when building UI components, handling async data, or managing component states. type skill created 2026-02-27T00:00:00.000Z domain software-development category frontend risk safe source self tags ["skill","software-development","frontend","angular"]
Angular UI Patterns
Core Principles
Never show stale UI - Loading states only when actually loading
Always surface errors - Users must know when something fails
Optimistic updates - Make the UI feel instant
Progressive disclosure - Use @defer to show content as available
Graceful degradation - Partial data is better than no data
Loading State Patterns
The Golden Rule
Show loading indicator ONLY when there's no data to display.
@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 ;
}
Loading State Decision Tree
Is there an error?
→ Yes: Show error state with retry option
→ No: Continue
Is it loading AND we have no data?
→ Yes: Show loading indicator (spinner/skeleton)
→ No: Continue
Do we have data?
→ Yes, with items: Show the data
→ Yes, but empty: Show empty state
→ No: Show loading (fallback)
Skeleton vs Spinner
Use Skeleton When Use Spinner When Known content shape Unknown content shape List/card layouts Modal actions Initial page load
Content placeholders Inline operations
Control Flow Patterns
@if/@else for Conditional Rendering @if (user(); as user) {
<span > Welcome, {{ user.name }}</span >
} @else if (loading()) {
<app-spinner size ="small" />
} @else {
<a routerLink ="/login" > Sign In</a >
}
@for with 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 for Progressive Loading
<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" />
}
Error Handling Patterns
Error Handling Hierarchy 1. Inline error (field-level) → Form validation errors
2. Toast notification → Recoverable errors, user can retry
3. Error banner → Page-level errors, data still partially usable
4. Full error screen → Unrecoverable, needs user action
Always Show Errors CRITICAL: Never swallow errors silently.
@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);
}
}
Error State Component Pattern @Component ({
selector : "app-error-state" ,
standalone : true ,
imports : [NgOptimizedImage ],
template : `
<div class="error-state">
<img ngSrc="/assets/error-icon.svg" width="64" height="64" alt="" />
<h3>{{ title() }}</h3>
<p>{{ message() }}</p>
@if (retry.observed) {
<button (click)="retry.emit()" class="btn-primary">Try Again</button>
}
</div>
` ,
})
export class ErrorStateComponent {
title = input ("Something went wrong" );
message = input ("An unexpected error occurred" );
retry = output<void >();
}
Button State Patterns
Button Loading State <button
(click )="handleSubmit()"
[disabled ]="isSubmitting() || !form.valid"
class ="btn-primary"
>
@if (isSubmitting()) {
<app-spinner size ="small" class ="mr-2" />
Saving... } @else { Save Changes }
</button >
Disable During Operations CRITICAL: Always disable triggers during async operations.
@Component ({
template : `
<button
[disabled]="saving()"
(click)="save()"
>
@if (saving()) {
<app-spinner size="sm" /> Saving...
} @else {
Save
}
</button>
`
})
export class SaveButtonComponent {
saving = signal (false );
async save ( ) {
this .saving .set (true );
try {
await this .service .save ();
} finally {
this .saving .set (false );
}
}
}
<button (click)="save()" >
{{ saving () ? 'Saving...' : 'Save' }}
</button>
Empty States
Empty State Requirements Every list/collection MUST have an empty state:
@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()"
/>
}
Contextual Empty States @Component ({
selector : "app-empty-state" ,
template : `
<div class="empty-state">
<span class="icon" [class]="icon()"></span>
<h3>{{ title() }}</h3>
<p>{{ description() }}</p>
@if (actionLabel()) {
<button (click)="action.emit()" class="btn-primary">
{{ actionLabel() }}
</button>
}
</div>
` ,
})
export class EmptyStateComponent {
icon = input ("inbox" );
title = input.required <string >();
description = input ("" );
actionLabel = input<string | null >(null );
action = output<void >();
}
Form Patterns
Form with Loading and Validation @Component ({
template : `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-field">
<label for="name">Name</label>
<input
id="name"
formControlName="name"
[class.error]="isFieldInvalid('name')"
/>
@if (isFieldInvalid("name")) {
<span class="error-text">
{{ getFieldError("name") }}
</span>
}
</div>
<div class="form-field">
<label for="email">Email</label>
<input id="email" type="email" formControlName="email" />
@if (isFieldInvalid("email")) {
<span class="error-text">
{{ getFieldError("email") }}
</span>
}
</div>
<button type="submit" [disabled]="form.invalid || submitting()">
@if (submitting()) {
<app-spinner size="sm" /> Submitting...
} @else {
Submit
}
</button>
</form>
` ,
})
export class UserFormComponent {
private fb = inject (FormBuilder );
submitting = signal (false );
form = this .fb .group ({
name : ["" , [Validators .required , Validators .minLength (2 )]],
email : ["" , [Validators .required , Validators .email ]],
});
isFieldInvalid (field : string ): boolean {
const control = this .form .get (field);
return control ? control.invalid && control.touched : false ;
}
getFieldError (field : string ): string {
const control = this .form .get (field);
if (control?.hasError ("required" )) return "This field is required" ;
if (control?.hasError ("email" )) return "Invalid email format" ;
if (control?.hasError ("minlength" )) return "Too short" ;
return "" ;
}
async onSubmit ( ) {
if (this .form .invalid ) return ;
this .submitting .set (true );
try {
await this .service .submit (this .form .value );
this .toast .success ("Submitted successfully" );
} catch {
this .toast .error ("Submission failed" );
} finally {
this .submitting .set (false );
}
}
}
Dialog/Modal Patterns
Confirmation Dialog
@Injectable ({ providedIn : 'root' })
export class DialogService {
private dialog = inject (Dialog );
async confirm (options : {
title : string ;
message : string ;
confirmText ?: string ;
cancelText ?: string ;
}): Promise <boolean > {
const dialogRef = this .dialog .open (ConfirmDialogComponent , {
data : options,
});
return await firstValueFrom (dialogRef.closed ) ?? false ;
}
}
async deleteItem (item : Item ) {
const confirmed = await this .dialog .confirm ({
title : 'Delete Item' ,
message : `Are you sure you want to delete "${item.name} "?` ,
confirmText : 'Delete' ,
});
if (confirmed) {
await this .store .delete (item.id );
}
}
Anti-Patterns
Loading States
@if (loading ()) {
<app-spinner />
}
@if (loading () && !items ().length ) {
<app-spinner />
}
Error Handling
try {
await this .service .save ();
} catch (e) {
console .log (e);
}
try {
await this .service .save ();
} catch (e) {
console .error ("Save failed:" , e);
this .toast .error ("Failed to save. Please try again." );
}
Button States
<button (click )="submit()" > Submit</button >
<button (click )="submit()" [disabled ]="loading()" >
@if (loading()) {
<app-spinner size ="sm" />
} Submit
</button >
UI State Checklist Before completing any UI component:
UI States
Data & Mutations
Accessibility
Integration with Other Skills
angular-state-management : Use Signal stores for state
angular : Apply modern patterns (Signals, @defer)
testing-patterns : Test all UI states
When to Use This skill is applicable to execute the workflow or actions described in the overview.
Connections
Domain: [[Software Entwicklung]]
Kategorie: [[Frontend Entwicklung]]
Navigation: [[Skills Uebersicht]], [[Home]]