[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6JM5QSS3MZ3XRM8VQ2WN9RB":3,"skill-same:01M6JM5QSS3MZ3XRM8VQ2WN9RB":89},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":42,"linkOnly":43,"markdownContent":44,"markdownBody":45,"frontMatter":46,"refName":48,"commitSha":49,"skillDirectory":50,"forksCount":51,"repositoryHtmlUrl":52,"repositoryHomepageUrl":53,"ownerHtmlUrl":54,"ownerBlogUrl":55,"ownerDescription":6,"updatedAt":56,"install":57,"resources":70},"01M6JM5QSS3MZ3XRM8VQ2WN9RB","angular_ui_patterns",null,"Modern Angular UI patterns for loading states, error handling, and data display. Use when building UI components, handling async data, or managing component states.","LongLeo287\u002FOmniClaw","LongLeo287","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F141803367?v=4","01M6JK75C62VCNZKR5BRZ386YV","LongLeo","01M6JK75GQ78TSP1ZPXNKK3MCH",9,"MIT","https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fangular_ui_patterns\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41],"ui","angular","angular ui patterns","patterns","for","safe","display","loading","states","error","ui components","self","async","use","data","error handling","component","components","2026-11-10 10:12:30",false,"---\nname: angular_ui_patterns\n\ndescription: \"Modern Angular UI patterns for loading states, error handling, and data display. Use when building UI components, handling async data, or managing component states.\"\nrisk: safe\nsource: self\ndate_added: \"2026-02-27\"\n---\n\n# 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      \u003Capp-error-state [error]=\"error()\" (retry)=\"load()\" \u002F>\n    } @else if (loading() && !items().length) {\n      \u003Capp-skeleton-list \u002F>\n    } @else if (!items().length) {\n      \u003Capp-empty-state message=\"No items found\" \u002F>\n    } @else {\n      \u003Capp-item-list [items]=\"items()\" \u002F>\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\u002Fskeleton)\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\u002Fcard layouts    | Modal actions         |\n| Initial page load    | Button submissions    |\n| Content placeholders | Inline operations     |\n\n---\n\n## Control Flow Patterns\n\n### @if\u002F@else for Conditional Rendering\n\n```html\n@if (user(); as user) {\n\u003Cspan>Welcome, {{ user.name }}\u003C\u002Fspan>\n} @else if (loading()) {\n\u003Capp-spinner size=\"small\" \u002F>\n} @else {\n\u003Ca routerLink=\"\u002Flogin\">Sign In\u003C\u002Fa>\n}\n```\n\n### @for with Track\n\n```html\n@for (item of items(); track item.id) {\n\u003Capp-item-card [item]=\"item\" (delete)=\"remove(item.id)\" \u002F>\n} @empty {\n\u003Capp-empty-state\n  icon=\"inbox\"\n  message=\"No items yet\"\n  actionLabel=\"Create Item\"\n  (action)=\"create()\"\n\u002F>\n}\n```\n\n### @defer for Progressive Loading\n\n```html\n\u003C!-- Critical content loads immediately -->\n\u003Capp-header \u002F>\n\u003Capp-hero-section \u002F>\n\n\u003C!-- Non-critical content deferred -->\n@defer (on viewport) {\n\u003Capp-comments [postId]=\"postId()\" \u002F>\n} @placeholder {\n\u003Cdiv class=\"h-32 bg-gray-100 animate-pulse\">\u003C\u002Fdiv>\n} @loading (minimum 200ms) {\n\u003Capp-spinner \u002F>\n} @error {\n\u003Capp-error-state message=\"Failed to load comments\" \u002F>\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\u002F\u002F 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(['\u002Fitems']);\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\u002F\u002F WRONG - Error silently caught\nasync create(data: CreateItemDto) {\n  try {\n    await this.store.create(data);\n  } catch (error) {\n    console.error(error); \u002F\u002F 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    \u003Cdiv class=\"error-state\">\n      \u003Cimg ngSrc=\"\u002Fassets\u002Ferror-icon.svg\" width=\"64\" height=\"64\" alt=\"\" \u002F>\n      \u003Ch3>{{ title() }}\u003C\u002Fh3>\n      \u003Cp>{{ message() }}\u003C\u002Fp>\n      @if (retry.observed) {\n        \u003Cbutton (click)=\"retry.emit()\" class=\"btn-primary\">Try Again\u003C\u002Fbutton>\n      }\n    \u003C\u002Fdiv>\n  `,\n})\nexport class ErrorStateComponent {\n  title = input(\"Something went wrong\");\n  message = input(\"An unexpected error occurred\");\n  retry = output\u003Cvoid>();\n}\n```\n\n---\n\n## Button State Patterns\n\n### Button Loading State\n\n```html\n\u003Cbutton\n  (click)=\"handleSubmit()\"\n  [disabled]=\"isSubmitting() || !form.valid\"\n  class=\"btn-primary\"\n>\n  @if (isSubmitting()) {\n  \u003Capp-spinner size=\"small\" class=\"mr-2\" \u002F>\n  Saving... } @else { Save Changes }\n\u003C\u002Fbutton>\n```\n\n### Disable During Operations\n\n**CRITICAL: Always disable triggers during async operations.**\n\n```typescript\n\u002F\u002F CORRECT - Button disabled while loading\n@Component({\n  template: `\n    \u003Cbutton\n      [disabled]=\"saving()\"\n      (click)=\"save()\"\n    >\n      @if (saving()) {\n        \u003Capp-spinner size=\"sm\" \u002F> Saving...\n      } @else {\n        Save\n      }\n    \u003C\u002Fbutton>\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\u002F\u002F WRONG - User can click multiple times\n\u003Cbutton (click)=\"save()\">\n  {{ saving() ? 'Saving...' : 'Save' }}\n\u003C\u002Fbutton>\n```\n\n---\n\n## Empty States\n\n### Empty State Requirements\n\nEvery list\u002Fcollection MUST have an empty state:\n\n```html\n@for (item of items(); track item.id) {\n\u003Capp-item-card [item]=\"item\" \u002F>\n} @empty {\n\u003Capp-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\u002F>\n}\n```\n\n### Contextual Empty States\n\n```typescript\n@Component({\n  selector: \"app-empty-state\",\n  template: `\n    \u003Cdiv class=\"empty-state\">\n      \u003Cspan class=\"icon\" [class]=\"icon()\">\u003C\u002Fspan>\n      \u003Ch3>{{ title() }}\u003C\u002Fh3>\n      \u003Cp>{{ description() }}\u003C\u002Fp>\n      @if (actionLabel()) {\n        \u003Cbutton (click)=\"action.emit()\" class=\"btn-primary\">\n          {{ actionLabel() }}\n        \u003C\u002Fbutton>\n      }\n    \u003C\u002Fdiv>\n  `,\n})\nexport class EmptyStateComponent {\n  icon = input(\"inbox\");\n  title = input.required\u003Cstring>();\n  description = input(\"\");\n  actionLabel = input\u003Cstring | null>(null);\n  action = output\u003Cvoid>();\n}\n```\n\n---\n\n## Form Patterns\n\n### Form with Loading and Validation\n\n```typescript\n@Component({\n  template: `\n    \u003Cform [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n      \u003Cdiv class=\"form-field\">\n        \u003Clabel for=\"name\">Name\u003C\u002Flabel>\n        \u003Cinput\n          id=\"name\"\n          formControlName=\"name\"\n          [class.error]=\"isFieldInvalid('name')\"\n        \u002F>\n        @if (isFieldInvalid(\"name\")) {\n          \u003Cspan class=\"error-text\">\n            {{ getFieldError(\"name\") }}\n          \u003C\u002Fspan>\n        }\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"form-field\">\n        \u003Clabel for=\"email\">Email\u003C\u002Flabel>\n        \u003Cinput id=\"email\" type=\"email\" formControlName=\"email\" \u002F>\n        @if (isFieldInvalid(\"email\")) {\n          \u003Cspan class=\"error-text\">\n            {{ getFieldError(\"email\") }}\n          \u003C\u002Fspan>\n        }\n      \u003C\u002Fdiv>\n\n      \u003Cbutton type=\"submit\" [disabled]=\"form.invalid || submitting()\">\n        @if (submitting()) {\n          \u003Capp-spinner size=\"sm\" \u002F> Submitting...\n        } @else {\n          Submit\n        }\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\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\u002FModal Patterns\n\n### Confirmation Dialog\n\n```typescript\n\u002F\u002F dialog.service.ts\n@Injectable({ providedIn: 'root' })\nexport class DialogService {\n  private dialog = inject(Dialog); \u002F\u002F CDK Dialog or custom\n\n  async confirm(options: {\n    title: string;\n    message: string;\n    confirmText?: string;\n    cancelText?: string;\n  }): Promise\u003Cboolean> {\n    const dialogRef = this.dialog.open(ConfirmDialogComponent, {\n      data: options,\n    });\n\n    return await firstValueFrom(dialogRef.closed) ?? false;\n  }\n}\n\n\u002F\u002F 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\u002F\u002F WRONG - Spinner when data exists (causes flash on refetch)\n@if (loading()) {\n  \u003Capp-spinner \u002F>\n}\n\n\u002F\u002F CORRECT - Only show loading without data\n@if (loading() && !items().length) {\n  \u003Capp-spinner \u002F>\n}\n```\n\n### Error Handling\n\n```typescript\n\u002F\u002F WRONG - Error swallowed\ntry {\n  await this.service.save();\n} catch (e) {\n  console.log(e); \u002F\u002F User has no idea!\n}\n\n\u002F\u002F 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\u003C!-- WRONG - Button not disabled during submission -->\n\u003Cbutton (click)=\"submit()\">Submit\u003C\u002Fbutton>\n\n\u003C!-- CORRECT - Disabled and shows loading -->\n\u003Cbutton (click)=\"submit()\" [disabled]=\"loading()\">\n  @if (loading()) {\n  \u003Capp-spinner size=\"sm\" \u002F>\n  } Submit\n\u003C\u002Fbutton>\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\u002Fvisual)\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.\n","# 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      \u003Capp-error-state [error]=\"error()\" (retry)=\"load()\" \u002F>\n    } @else if (loading() && !items().length) {\n      \u003Capp-skeleton-list \u002F>\n    } @else if (!items().length) {\n      \u003Capp-empty-state message=\"No items found\" \u002F>\n    } @else {\n      \u003Capp-item-list [items]=\"items()\" \u002F>\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\u002Fskeleton)\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\u002Fcard layouts    | Modal actions         |\n| Initial page load    | Button submissions    |\n| Content placeholders | Inline operations     |\n\n---\n\n## Control Flow Patterns\n\n### @if\u002F@else for Conditional Rendering\n\n```html\n@if (user(); as user) {\n\u003Cspan>Welcome, {{ user.name }}\u003C\u002Fspan>\n} @else if (loading()) {\n\u003Capp-spinner size=\"small\" \u002F>\n} @else {\n\u003Ca routerLink=\"\u002Flogin\">Sign In\u003C\u002Fa>\n}\n```\n\n### @for with Track\n\n```html\n@for (item of items(); track item.id) {\n\u003Capp-item-card [item]=\"item\" (delete)=\"remove(item.id)\" \u002F>\n} @empty {\n\u003Capp-empty-state\n  icon=\"inbox\"\n  message=\"No items yet\"\n  actionLabel=\"Create Item\"\n  (action)=\"create()\"\n\u002F>\n}\n```\n\n### @defer for Progressive Loading\n\n```html\n\u003C!-- Critical content loads immediately -->\n\u003Capp-header \u002F>\n\u003Capp-hero-section \u002F>\n\n\u003C!-- Non-critical content deferred -->\n@defer (on viewport) {\n\u003Capp-comments [postId]=\"postId()\" \u002F>\n} @placeholder {\n\u003Cdiv class=\"h-32 bg-gray-100 animate-pulse\">\u003C\u002Fdiv>\n} @loading (minimum 200ms) {\n\u003Capp-spinner \u002F>\n} @error {\n\u003Capp-error-state message=\"Failed to load comments\" \u002F>\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\u002F\u002F 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(['\u002Fitems']);\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\u002F\u002F WRONG - Error silently caught\nasync create(data: CreateItemDto) {\n  try {\n    await this.store.create(data);\n  } catch (error) {\n    console.error(error); \u002F\u002F 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    \u003Cdiv class=\"error-state\">\n      \u003Cimg ngSrc=\"\u002Fassets\u002Ferror-icon.svg\" width=\"64\" height=\"64\" alt=\"\" \u002F>\n      \u003Ch3>{{ title() }}\u003C\u002Fh3>\n      \u003Cp>{{ message() }}\u003C\u002Fp>\n      @if (retry.observed) {\n        \u003Cbutton (click)=\"retry.emit()\" class=\"btn-primary\">Try Again\u003C\u002Fbutton>\n      }\n    \u003C\u002Fdiv>\n  `,\n})\nexport class ErrorStateComponent {\n  title = input(\"Something went wrong\");\n  message = input(\"An unexpected error occurred\");\n  retry = output\u003Cvoid>();\n}\n```\n\n---\n\n## Button State Patterns\n\n### Button Loading State\n\n```html\n\u003Cbutton\n  (click)=\"handleSubmit()\"\n  [disabled]=\"isSubmitting() || !form.valid\"\n  class=\"btn-primary\"\n>\n  @if (isSubmitting()) {\n  \u003Capp-spinner size=\"small\" class=\"mr-2\" \u002F>\n  Saving... } @else { Save Changes }\n\u003C\u002Fbutton>\n```\n\n### Disable During Operations\n\n**CRITICAL: Always disable triggers during async operations.**\n\n```typescript\n\u002F\u002F CORRECT - Button disabled while loading\n@Component({\n  template: `\n    \u003Cbutton\n      [disabled]=\"saving()\"\n      (click)=\"save()\"\n    >\n      @if (saving()) {\n        \u003Capp-spinner size=\"sm\" \u002F> Saving...\n      } @else {\n        Save\n      }\n    \u003C\u002Fbutton>\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\u002F\u002F WRONG - User can click multiple times\n\u003Cbutton (click)=\"save()\">\n  {{ saving() ? 'Saving...' : 'Save' }}\n\u003C\u002Fbutton>\n```\n\n---\n\n## Empty States\n\n### Empty State Requirements\n\nEvery list\u002Fcollection MUST have an empty state:\n\n```html\n@for (item of items(); track item.id) {\n\u003Capp-item-card [item]=\"item\" \u002F>\n} @empty {\n\u003Capp-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\u002F>\n}\n```\n\n### Contextual Empty States\n\n```typescript\n@Component({\n  selector: \"app-empty-state\",\n  template: `\n    \u003Cdiv class=\"empty-state\">\n      \u003Cspan class=\"icon\" [class]=\"icon()\">\u003C\u002Fspan>\n      \u003Ch3>{{ title() }}\u003C\u002Fh3>\n      \u003Cp>{{ description() }}\u003C\u002Fp>\n      @if (actionLabel()) {\n        \u003Cbutton (click)=\"action.emit()\" class=\"btn-primary\">\n          {{ actionLabel() }}\n        \u003C\u002Fbutton>\n      }\n    \u003C\u002Fdiv>\n  `,\n})\nexport class EmptyStateComponent {\n  icon = input(\"inbox\");\n  title = input.required\u003Cstring>();\n  description = input(\"\");\n  actionLabel = input\u003Cstring | null>(null);\n  action = output\u003Cvoid>();\n}\n```\n\n---\n\n## Form Patterns\n\n### Form with Loading and Validation\n\n```typescript\n@Component({\n  template: `\n    \u003Cform [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n      \u003Cdiv class=\"form-field\">\n        \u003Clabel for=\"name\">Name\u003C\u002Flabel>\n        \u003Cinput\n          id=\"name\"\n          formControlName=\"name\"\n          [class.error]=\"isFieldInvalid('name')\"\n        \u002F>\n        @if (isFieldInvalid(\"name\")) {\n          \u003Cspan class=\"error-text\">\n            {{ getFieldError(\"name\") }}\n          \u003C\u002Fspan>\n        }\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"form-field\">\n        \u003Clabel for=\"email\">Email\u003C\u002Flabel>\n        \u003Cinput id=\"email\" type=\"email\" formControlName=\"email\" \u002F>\n        @if (isFieldInvalid(\"email\")) {\n          \u003Cspan class=\"error-text\">\n            {{ getFieldError(\"email\") }}\n          \u003C\u002Fspan>\n        }\n      \u003C\u002Fdiv>\n\n      \u003Cbutton type=\"submit\" [disabled]=\"form.invalid || submitting()\">\n        @if (submitting()) {\n          \u003Capp-spinner size=\"sm\" \u002F> Submitting...\n        } @else {\n          Submit\n        }\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\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\u002FModal Patterns\n\n### Confirmation Dialog\n\n```typescript\n\u002F\u002F dialog.service.ts\n@Injectable({ providedIn: 'root' })\nexport class DialogService {\n  private dialog = inject(Dialog); \u002F\u002F CDK Dialog or custom\n\n  async confirm(options: {\n    title: string;\n    message: string;\n    confirmText?: string;\n    cancelText?: string;\n  }): Promise\u003Cboolean> {\n    const dialogRef = this.dialog.open(ConfirmDialogComponent, {\n      data: options,\n    });\n\n    return await firstValueFrom(dialogRef.closed) ?? false;\n  }\n}\n\n\u002F\u002F 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\u002F\u002F WRONG - Spinner when data exists (causes flash on refetch)\n@if (loading()) {\n  \u003Capp-spinner \u002F>\n}\n\n\u002F\u002F CORRECT - Only show loading without data\n@if (loading() && !items().length) {\n  \u003Capp-spinner \u002F>\n}\n```\n\n### Error Handling\n\n```typescript\n\u002F\u002F WRONG - Error swallowed\ntry {\n  await this.service.save();\n} catch (e) {\n  console.log(e); \u002F\u002F User has no idea!\n}\n\n\u002F\u002F 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\u003C!-- WRONG - Button not disabled during submission -->\n\u003Cbutton (click)=\"submit()\">Submit\u003C\u002Fbutton>\n\n\u003C!-- CORRECT - Disabled and shows loading -->\n\u003Cbutton (click)=\"submit()\" [disabled]=\"loading()\">\n  @if (loading()) {\n  \u003Capp-spinner size=\"sm\" \u002F>\n  } Submit\n\u003C\u002Fbutton>\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\u002Fvisual)\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.\n",{"name":5,"risk":29,"source":35,"date_added":47,"description":7},"2026-02-27","main","c173cdffe08e788a99b91928a831b476b8434838","ecosystem\u002Fskills\u002Fangular_ui_patterns",0,"https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw","https:\u002F\u002Fportfolio-longleo.vercel.app\u002Flabs\u002Fomniclaw.html","https:\u002F\u002Fgithub.com\u002FLongLeo287","https:\u002F\u002Fwww.facebook.com\u002FResourceRookie2023","2026-09-30 00:58:05",{"source":8,"skillName":5,"commands":58,"prompt":68,"zipAvailable":69},[59,62,65],{"tool":60,"command":61},"npx","npx skills add LongLeo287\u002FOmniClaw --skill angular_ui_patterns",{"tool":63,"command":64},"bunx","bunx skills add LongLeo287\u002FOmniClaw --skill angular_ui_patterns",{"tool":66,"command":67},"pnpm","pnpm dlx skills add LongLeo287\u002FOmniClaw --skill angular_ui_patterns","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fangular_ui_patterns\u002FSKILL.md",true,[71,77,81,85],{"path":72,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":75,"sourceHtmlUrl":76},"_DIR_IDENTITY.md","REFERENCE","参考文档",160,"https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fangular_ui_patterns\u002F_DIR_IDENTITY.md",{"path":78,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":79,"sourceHtmlUrl":80},"metadata.json",632,"https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fangular_ui_patterns\u002Fmetadata.json",{"path":82,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":83,"sourceHtmlUrl":84},"README.md",1666,"https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fangular_ui_patterns\u002FREADME.md",{"path":86,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":87,"sourceHtmlUrl":88},"schema.json",459,"https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fangular_ui_patterns\u002Fschema.json",[90,127,146,172,188,215],{"skillNo":91,"name":92,"nameZh":6,"description":93,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":108,"publishedAt":42},"01M6JR0KQP4MHT32TN40D9CBFX","UI-UX Pro Max","Advanced UI\u002FUX analysis and generation skills with data-driven patterns. Python scripts (core.py, search.py) for systematic UI pattern extraction, design system generation, and component search from curated data sources.","https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fui_components\u002Fui_ux_pro_max\u002FSKILL.md",[96,101,104,105],{"categoryNo":97,"categoryName":98,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000015","视觉设计","SCENARIO","使用场景",{"categoryNo":102,"categoryName":103,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":106,"categoryName":107,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000033","数据分析师",[109,24,110,111,112,113,114,40,115,116,117,118,119,120,121,122,123,124,125,126],"ux","ui ux pro max","max","pro","ui ux","design","search","analysis","team","rd","ui-ux","active","systematic","scripts","pattern extraction","sources","advanced","data-driven",{"skillNo":128,"name":129,"nameZh":6,"description":130,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":131,"categories":132,"keywords":134,"publishedAt":42},"01M6JR0EF5AEQHP2FSTB57H05P","shadcn-ui","Expert guidance for integrating and building applications with shadcn\u002Fui components, including component discovery, installation, customization, and best practices.","https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fui_components\u002Fshadcn_ui\u002FSKILL.md",[133],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[135,24,136,41,40,137,138,139,140,117,141,142,143,120,144,34,145,28],"shadcn","shadcn ui","core","best practices","applications","discovery","customization","guidance","expert","installation","with",{"skillNo":147,"name":148,"nameZh":6,"description":149,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":150,"categories":151,"keywords":155,"publishedAt":42},"01M6JR08F51BEN17RHFYX65WVE","zustand_store_ts","Create Zustand stores with TypeScript, subscribeWithSelector middleware, and proper state\u002Faction separation. Use when building React state management, creating global stores, or implementing reactive state patterns with Zustand.","https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fzustand_store_ts\u002FSKILL.md",[152],{"categoryNo":153,"categoryName":154,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000009","前端开发",[156,157,158,159,160,161,162,163,164,165,166,167,37,27,168,169,145,170,171],"zustand store ts","zustand","store","typescript","microsoft","react","create","state management","state","react state management","management","global","separation","middleware","action","implementing",{"skillNo":173,"name":157,"nameZh":6,"description":174,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":175,"categories":176,"keywords":177,"publishedAt":42},"01M6JR00TDMVAFFCZXZ04AN7Q7","Zustand state management guide. Use when working with store code (src\u002Fstore\u002F**), implementing actions, managing state, or creating slices. Triggers on Zustand store development, state management questions, or action implementation.","https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fzustand\u002FSKILL.md",[],[157,178,163,179,164,180,181,166,182,37,183,184,185,158,145,186,187,170,171],"development","code","actions","src","guide","implementation","zustand state","triggers","questions","zustand state management",{"skillNo":189,"name":190,"nameZh":6,"description":191,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":192,"categories":193,"keywords":198,"publishedAt":42},"01M6JQZSWV4KWJP112G6XHEP44","youtube_summarizer","Automatically fetch YouTube video transcripts, generate structured summaries, and send full transcripts to messaging platforms. Detects YouTube URLs and provides metadata, key insights, and downloadable transcripts.","https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fyoutube_summarizer\u002FSKILL.md",[194,197],{"categoryNo":195,"categoryName":196,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000016","图像与多媒体",{"categoryNo":102,"categoryName":103,"taxonomyType":99,"taxonomyTypeDesc":100},[199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214],"youtube","youtube summarizer","summarizer","generate","metadata","messaging","Video","fetch","telegram","transcription","summarization","structured","full","insights","transcripts","summaries",{"skillNo":216,"name":217,"nameZh":6,"description":218,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":219,"categories":220,"keywords":221,"publishedAt":42},"01M6JQZN3QJB6B77JGKJD62DQT","youtube_clipper","YouTube 视频智能剪辑工具。下载视频和字幕，AI 分析生成精细章节（几分钟级别）， 用户选择片段后自动剪辑、翻译字幕为中英双语、烧录字幕到视频，并生成总结文案。 使用场景：当用户需要剪辑 YouTube 视频、生成短视频片段、制作双语字幕版本时。 关键词：视频剪辑、YouTube、字幕翻译、双语字幕、视频下载、clip video","https:\u002F\u002Fgithub.com\u002FLongLeo287\u002FOmniClaw\u002Fblob\u002Fmain\u002Fecosystem\u002Fskills\u002Fyoutube_clipper\u002FSKILL.md",[],[199,222,223,205,224,225,226,227,228,229,230,231,232,233,234,235,236],"clipper","youtube clipper","Bash","下载","视频","生成","关键词","翻译","read","write","clip","总结","工具","文案","sonnet"]