{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/angular-ui-patterns",
  "version": "1.0.0",
  "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.",
  "system_prompt_fragment": "# Angular UI Patterns\n\n## Core Principles\n\n1. **Never show stale UI** - Loading states only when actually loading\n2. **Always surface errors** - Users must know when something fails\n3. **Optimistic updates** - Make the UI feel instant\n4. **Progressive disclosure** - Use `@defer` to show content as available\n5. **Graceful degradation** - Partial data is better than no data\n\n---\n\n## Loading State Patterns\n\n### The Golden Rule\n\n**Show loading indicator ONLY when there's no data to display.**\n\n```typescript\n@Component({\n  template: `\n    @if (error()) {\n      <app-error-state [error]=\"error()\" (retry)=\"load()\" />\n    } @else if (loading() && !items().length) {\n      <app-skeleton-list />\n    } @else if (!items().length) {\n      <app-empty-state message=\"No items found\" />\n    } @else {\n      <app-item-list [items]=\"items()\" />\n    }\n  `,\n})\nexport class ItemListComponent {\n  private store = inject(ItemStore);\n\n  items = this.store.items;\n  loading = this.store.loading;\n  error = this.store.error;\n}\n```\n\n### Loading State Decision Tree\n\n```\nIs there an error?\n  → Yes: Show error state with retry option\n  → No: Continue\n\nIs it loading AND we have no data?\n  → Yes: Show loading indicator (spinner/skeleton)\n  → No: Continue\n\nDo we have data?\n  → Yes, with items: Show the data\n  → Yes, but empty: Show empty state\n  → No: Show loading (fallback)\n```\n\n### Skeleton vs Spinner\n\n| Use Skeleton When    | Use Spinner When      |\n| -------------------- | --------------------- |\n| Known content shape  | Unknown content shape |\n| List/card layouts    | Modal actions         |\n| Initial page load    | Button submissions    |\n| Content placeholders | Inline operations     |\n\n---\n\n## Control Flow Patterns\n\n### @if/@else for Conditional Rendering\n\n```html\n@if (user(); as user) {\n<span>Welcome, {{ user.name }}</span>\n} @else if (loading()) {\n<app-spinner size=\"small\" />\n} @else {\n<a routerLink=\"/login\">Sign In</a>\n}\n```\n\n### @for with Track\n\n```html\n@for (item of items(); track item.id) {\n<app-item-card [item]=\"item\" (delete)=\"remove(item.id)\" />\n} @empty {\n<app-empty-state\n  icon=\"inbox\"\n  message=\"No items yet\"\n  actionLabel=\"Create Item\"\n  (action)=\"create()\"\n/>\n}\n```\n\n### @defer for Progressive Loading\n\n```html\n<!-- Critical content loads immediately -->\n<app-header />\n<app-hero-section />\n\n<!-- Non-critical content deferred -->\n@defer (on viewport) {\n<app-comments [postId]=\"postId()\" />\n} @placeholder {\n<div class=\"h-32 bg-gray-100 animate-pulse\"></div>\n} @loading (minimum 200ms) {\n<app-spinner />\n} @error {\n<app-error-state message=\"Failed to load comments\" />\n}\n```\n\n---\n\n## Error Handling Patterns\n\n### Error Handling Hierarchy\n\n```\n1. Inline error (field-level) → Form validation errors\n2. Toast notification → Recoverable errors, user can retry\n3. Error banner → Page-level errors, data still partially usable\n4. Full error screen → Unrecoverable, needs user action\n```\n\n### Always Show Errors\n\n**CRITICAL: Never swallow errors silently.**\n\n```typescript\n// CORRECT - Error always surfaced to user\n@Component({...})\nexport class CreateItemComponent {\n  private store = inject(ItemStore);\n  private toast = inject(ToastService);\n\n  async create(data: CreateItemDto) {\n    try {\n      await this.store.create(data);\n      this.toast.success('Item created successfully');\n      this.router.navigate(['/items']);\n    } catch (error) {\n      console.error('createItem failed:', error);\n      this.toast.error('Failed to create item. Please try again.');\n    }\n  }\n}\n\n// WRONG - Error silently caught\nasync create(data: CreateItemDto) {\n  try {\n    await this.store.create(data);\n  } catch (error) {\n    console.error(error); // User sees nothing!\n  }\n}\n```\n\n### Error State Component Pattern\n\n```typescript\n@Component({\n  selector: \"app-error-state\",\n  standalone: true,\n  imports: [NgOptimizedImage],\n  template: `\n    <div class=\"error-state\">\n      <img ngSrc=\"/assets/error-icon.svg\" width=\"64\" height=\"64\" alt=\"\" />\n      <h3>{{ title() }}</h3>\n      <p>{{ message() }}</p>\n      @if (retry.observed) {\n        <button (click)=\"retry.emit()\" class=\"btn-primary\">Try Again</button>\n      }\n    </div>\n  `,\n})\nexport class ErrorStateComponent {\n  title = input(\"Something went wrong\");\n  message = input(\"An unexpected error occurred\");\n  retry = output<void>();\n}\n```\n\n---\n\n## Button State Patterns\n\n### Button Loading State\n\n```html\n<button\n  (click)=\"handleSubmit()\"\n  [disabled]=\"isSubmitting() || !form.valid\"\n  class=\"btn-primary\"\n>\n  @if (isSubmitting()) {\n  <app-spinner size=\"small\" class=\"mr-2\" />\n  Saving... } @else { Save Changes }\n</button>\n```\n\n### Disable During Operations\n\n**CRITICAL: Always disable triggers during async operations.**\n\n```typescript\n// CORRECT - Button disabled while loading\n@Component({\n  template: `\n    <button\n      [disabled]=\"saving()\"\n      (click)=\"save()\"\n    >\n      @if (saving()) {\n        <app-spinner size=\"sm\" /> Saving...\n      } @else {\n        Save\n      }\n    </button>\n  `\n})\nexport class SaveButtonComponent {\n  saving = signal(false);\n\n  async save() {\n    this.saving.set(true);\n    try {\n      await this.service.save();\n    } finally {\n      this.saving.set(false);\n    }\n  }\n}\n\n// WRONG - User can click multiple times\n<button (click)=\"save()\">\n  {{ saving() ? 'Saving...' : 'Save' }}\n</button>\n```\n\n---\n\n## Empty States\n\n### Empty State Requirements\n\nEvery list/collection MUST have an empty state:\n\n```html\n@for (item of items(); track item.id) {\n<app-item-card [item]=\"item\" />\n} @empty {\n<app-empty-state\n  icon=\"folder-open\"\n  title=\"No items yet\"\n  description=\"Create your first item to get started\"\n  actionLabel=\"Create Item\"\n  (action)=\"openCreateDialog()\"\n/>\n}\n```\n\n### Contextual Empty States\n\n```typescript\n@Component({\n  selector: \"app-empty-state\",\n  template: `\n    <div class=\"empty-state\">\n      <span class=\"icon\" [class]=\"icon()\"></span>\n      <h3>{{ title() }}</h3>\n      <p>{{ description() }}</p>\n      @if (actionLabel()) {\n        <button (click)=\"action.emit()\" class=\"btn-primary\">\n          {{ actionLabel() }}\n        </button>\n      }\n    </div>\n  `,\n})\nexport class EmptyStateComponent {\n  icon = input(\"inbox\");\n  title = input.required<string>();\n  description = input(\"\");\n  actionLabel = input<string | null>(null);\n  action = output<void>();\n}\n```\n\n---\n\n## Form Patterns\n\n### Form with Loading and Validation\n\n```typescript\n@Component({\n  template: `\n    <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n      <div class=\"form-field\">\n        <label for=\"name\">Name</label>\n        <input\n          id=\"name\"\n          formControlName=\"name\"\n          [class.error]=\"isFieldInvalid('name')\"\n        />\n        @if (isFieldInvalid(\"name\")) {\n          <span class=\"error-text\">\n            {{ getFieldError(\"name\") }}\n          </span>\n        }\n      </div>\n\n      <div class=\"form-field\">\n        <label for=\"email\">Email</label>\n        <input id=\"email\" type=\"email\" formControlName=\"email\" />\n        @if (isFieldInvalid(\"email\")) {\n          <span class=\"error-text\">\n            {{ getFieldError(\"email\") }}\n          </span>\n        }\n      </div>\n\n      <button type=\"submit\" [disabled]=\"form.invalid || submitting()\">\n        @if (submitting()) {\n          <app-spinner size=\"sm\" /> Submitting...\n        } @else {\n          Submit\n        }\n      </button>\n    </form>\n  `,\n})\nexport class UserFormComponent {\n  private fb = inject(FormBuilder);\n\n  submitting = signal(false);\n\n  form = this.fb.group({\n    name: [\"\", [Validators.required, Validators.minLength(2)]],\n    email: [\"\", [Validators.required, Validators.email]],\n  });\n\n  isFieldInvalid(field: string): boolean {\n    const control = this.form.get(field);\n    return control ? control.invalid && control.touched : false;\n  }\n\n  getFieldError(field: string): string {\n    const control = this.form.get(field);\n    if (control?.hasError(\"required\")) return \"This field is required\";\n    if (control?.hasError(\"email\")) return \"Invalid email format\";\n    if (control?.hasError(\"minlength\")) return \"Too short\";\n    return \"\";\n  }\n\n  async onSubmit() {\n    if (this.form.invalid) return;\n\n    this.submitting.set(true);\n    try {\n      await this.service.submit(this.form.value);\n      this.toast.success(\"Submitted successfully\");\n    } catch {\n      this.toast.error(\"Submission failed\");\n    } finally {\n      this.submitting.set(false);\n    }\n  }\n}\n```\n\n---\n\n## Dialog/Modal Patterns\n\n### Confirmation Dialog\n\n```typescript\n// dialog.service.ts\n@Injectable({ providedIn: 'root' })\nexport class DialogService {\n  private dialog = inject(Dialog); // CDK Dialog or custom\n\n  async confirm(options: {\n    title: string;\n    message: string;\n    confirmText?: string;\n    cancelText?: string;\n  }): Promise<boolean> {\n    const dialogRef = this.dialog.open(ConfirmDialogComponent, {\n      data: options,\n    });\n\n    return await firstValueFrom(dialogRef.closed) ?? false;\n  }\n}\n\n// Usage\nasync deleteItem(item: Item) {\n  const confirmed = await this.dialog.confirm({\n    title: 'Delete Item',\n    message: `Are you sure you want to delete \"${item.name}\"?`,\n    confirmText: 'Delete',\n  });\n\n  if (confirmed) {\n    await this.store.delete(item.id);\n  }\n}\n```\n\n---\n\n## Anti-Patterns\n\n### Loading States\n\n```typescript\n// WRONG - Spinner when data exists (causes flash on refetch)\n@if (loading()) {\n  <app-spinner />\n}\n\n// CORRECT - Only show loading without data\n@if (loading() && !items().length) {\n  <app-spinner />\n}\n```\n\n### Error Handling\n\n```typescript\n// WRONG - Error swallowed\ntry {\n  await this.service.save();\n} catch (e) {\n  console.log(e); // User has no idea!\n}\n\n// CORRECT - Error surfaced\ntry {\n  await this.service.save();\n} catch (e) {\n  console.error(\"Save failed:\", e);\n  this.toast.error(\"Failed to save. Please try again.\");\n}\n```\n\n### Button States\n\n```html\n<!-- WRONG - Button not disabled during submission -->\n<button (click)=\"submit()\">Submit</button>\n\n<!-- CORRECT - Disabled and shows loading -->\n<button (click)=\"submit()\" [disabled]=\"loading()\">\n  @if (loading()) {\n  <app-spinner size=\"sm\" />\n  } Submit\n</button>\n```\n\n---\n\n## UI State Checklist\n\nBefore completing any UI component:\n\n### UI States\n\n- [ ] Error state handled and shown to user\n- [ ] Loading state shown only when no data exists\n- [ ] Empty state provided for collections (`@empty` block)\n- [ ] Buttons disabled during async operations\n- [ ] Buttons show loading indicator when appropriate\n\n### Data & Mutations\n\n- [ ] All async operations have error handling\n- [ ] All user actions have feedback (toast/visual)\n- [ ] Optimistic updates rollback on failure\n\n### Accessibility\n\n- [ ] Loading states announced to screen readers\n- [ ] Error messages linked to form fields\n- [ ] Focus management after state changes\n\n---\n\n## Integration with Other Skills\n\n- **angular-state-management**: Use Signal stores for state\n- **angular**: Apply modern patterns (Signals, @defer)\n- **testing-patterns**: Test all UI states\n\n## When to Use\nThis skill is applicable to execute the workflow or actions described in the overview.",
  "applicable_domains": [
    "frontend"
  ],
  "category": "frontend",
  "invocation": [
    "/angular-ui-patterns"
  ],
  "authored_by": "claudeskills.in community",
  "source_url": "https://claudeskills.in/skill/angular-ui-patterns",
  "provenance": {
    "source": "claudeskills.in",
    "source_url": "https://claudeskills.in/skill/angular-ui-patterns",
    "license": "unknown",
    "imported_at": "2026-09-03",
    "notes": "Aggregated by claudeskills.in from community GitHub lists."
  },
  "tags": [
    "claudeskills",
    "frontend",
    "risk-reviewed"
  ],
  "lifecycle": "draft"
}