Install
$ agentstack add skill-agent-skills-hub-agent-skills-hub-angular-ui-patterns ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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()) {
} @else if (loading() && !items().length) {
} @else if (!items().length) {
} @else {
}
`,
})
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) {
Welcome, {{ user.name }}
} @else if (loading()) {
} @else {
Sign In
}
@for with Track
@for (item of items(); track item.id) {
} @empty {
}
@defer for Progressive Loading
@defer (on viewport) {
} @placeholder {
} @loading (minimum 200ms) {
} @error {
}
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: `
{{ title() }}
{{ message() }}
@if (retry.observed) {
Try Again
}
`,
})
export class ErrorStateComponent {
title = input("Something went wrong");
message = input("An unexpected error occurred");
retry = output();
}
Button State Patterns
Button Loading State
@if (isSubmitting()) {
Saving... } @else { Save Changes }
Disable During Operations
CRITICAL: Always disable triggers during async operations.
// CORRECT - Button disabled while loading
@Component({
template: `
@if (saving()) {
Saving...
} @else {
Save
}
`
})
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
{{ saving() ? 'Saving...' : 'Save' }}
Empty States
Empty State Requirements
Every list/collection MUST have an empty state:
@for (item of items(); track item.id) {
} @empty {
}
Contextual Empty States
@Component({
selector: "app-empty-state",
template: `
{{ title() }}
{{ description() }}
@if (actionLabel()) {
{{ actionLabel() }}
}
`,
})
export class EmptyStateComponent {
icon = input("inbox");
title = input.required();
description = input("");
actionLabel = input(null);
action = output();
}
Form Patterns
Form with Loading and Validation
@Component({
template: `
Name
@if (isFieldInvalid("name")) {
{{ getFieldError("name") }}
}
Email
@if (isFieldInvalid("email")) {
{{ getFieldError("email") }}
}
@if (submitting()) {
Submitting...
} @else {
Submit
}
`,
})
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 {
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()) {
}
// CORRECT - Only show loading without data
@if (loading() && !items().length) {
}
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
Submit
@if (loading()) {
} Submit
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
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: agent-skills-hub
- Source: agent-skills-hub/agent-skills-hub
- License: MIT
- Homepage: https://agent-skills-hub.github.io/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.