Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    sickn33

    angular-ui-patterns

    sickn33/angular-ui-patterns
    Design
    8,021
    6 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Modern Angular UI patterns for loading states, error handling, and data display. Use when building UI components, handling async data, or managing component states.

    SKILL.md

    Angular UI Patterns

    Core Principles

    1. Never show stale UI - Loading states only when actually loading
    2. Always surface errors - Users must know when something fails
    3. Optimistic updates - Make the UI feel instant
    4. Progressive disclosure - Use @defer to show content as available
    5. 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 (@empty block)
    • 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

    When to Use

    This skill is applicable to execute the workflow or actions described in the overview.

    Limitations

    • Use this skill only when the task clearly matches the scope described above.
    • Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
    • Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
    Recommended Servers
    Svelte
    Svelte
    Vercel Grep
    Vercel Grep
    Browser tool
    Browser tool
    Repository
    sickn33/antigravity-awesome-skills
    Files