System Prompt / Instructions
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
@deferto 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 | Button submissions | | 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
<!-- Critical content loads immediately -->
<app-header />
<app-hero-section />
<!-- Non-critical content deferred -->
@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.
// CORRECT - Error always surfaced to user
@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.');
}
}
}
// WRONG - Error silently caught
async create(data: CreateItemDto) {
try {
await this.store.create(data);
} catch (error) {
console.error(error); // User sees nothing!
}
}
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.
// CORRECT - Button disabled while loading
@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);
}
}
}
// WRONG - User can click multiple times
<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
// dialog.service.ts
@Injectable({ providedIn: 'root' })
export class DialogService {
private dialog = inject(Dialog); // CDK Dialog or custom
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;
}
}
// Usage
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
// WRONG - Spinner when data exists (causes flash on refetch)
@if (loading()) {
<app-spinner />
}
// CORRECT - Only show loading without data
@if (loading() && !items().length) {
<app-spinner />
}
Error Handling
// WRONG - Error swallowed
try {
await this.service.save();
} catch (e) {
console.log(e); // User has no idea!
}
// CORRECT - Error surfaced
try {
await this.service.save();
} catch (e) {
console.error("Save failed:", e);
this.toast.error("Failed to save. Please try again.");
}
Button States
<!-- WRONG - Button not disabled during submission -->
<button (click)="submit()">Submit</button>
<!-- CORRECT - Disabled and shows loading -->
<button (click)="submit()" [disabled]="loading()">
@if (loading()) {
<app-spinner size="sm" />
} Submit
</button>
UI State Checklist
Before completing any UI component:
UI States
- [ ] Error state handled and shown to user
- [ ] Loading state shown only when no data exists
- [ ] Empty state provided for collections (
@emptyblock) - [ ] Buttons disabled during async operations
- [ ] Buttons show loading indicator when appropriate
Data & Mutations
- [ ] All async operations have error handling
- [ ] All user actions have feedback (toast/visual)
- [ ] Optimistic updates rollback on failure
Accessibility
- [ ] Loading states announced to screen readers
- [ ] Error messages linked to form fields
- [ ] Focus management after state changes
Integration with Other Skills
- angular-state-management: Use Signal stores for state
- angular: Apply modern patterns (Signals, @defer)
- testing-patterns: Test all UI states
Frequently Asked Questions
What is angular-ui-patterns?
angular-ui-patterns is an expert AI persona designed to improve your coding workflow. Modern Angular UI patterns for loading states, error handling, and data display. Use when building UI components, handling async data, or managing component states. It provides senior-level context directly within your IDE.
How do I install the angular-ui-patterns skill in Cursor or Windsurf?
To install the angular-ui-patterns skill, download the package, extract the files to your project's .cursor/skills directory, and type @angular-ui-patterns in your editor chat to activate the expert instructions.
Is angular-ui-patterns free to download?
Yes, the angular-ui-patterns AI persona is completely free to download and integrate into compatible Agentic IDEs like Cursor, Windsurf, Github Copilot, and Anthropic MCP servers.
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.
Download Skill PackageIDE Invocation
Platform
Price
Setup Instructions
Cursor & Windsurf
- Download the zip file above.
- Extract to
.cursor/skills - Type
@angular-ui-patternsin editor chat.
Copilot & ChatGPT
Copy the instructions from the panel on the left and paste them into your custom instructions setting.
"Adding this angular-ui-patterns persona to my Cursor workspace completely changed the quality of code my AI generates. Saves me hours every week."
Level up further
Developers who downloaded angular-ui-patterns also use these elite AI personas.
3d-web-experience
Expert in building 3D experiences for the web - Three.js, React Three Fiber, Spline, WebGL, and interactive 3D scenes. Covers product configurators, 3D portfolios, immersive websites, and bringing depth to web experiences. Use when: 3D website, three.js, WebGL, react three fiber, 3D experience.
ab-test-setup
Structured guide for setting up A/B tests with mandatory gates for hypothesis, metrics, and execution readiness.
accessibility-compliance-accessibility-audit
You are an accessibility expert specializing in WCAG compliance, inclusive design, and assistive technology compatibility. Conduct audits, identify barriers, and provide remediation guidance.