$npx -y skills add SamarthaKV29/antigravity-god-mode --skill angular-ui-patternsModern Angular UI patterns for loading states, error handling, and data display. Use when building UI components, handling async data, or managing component states.
| 1 | # Angular UI Patterns |
| 2 | |
| 3 | ## Core Principles |
| 4 | |
| 5 | 1. **Never show stale UI** - Loading states only when actually loading |
| 6 | 2. **Always surface errors** - Users must know when something fails |
| 7 | 3. **Optimistic updates** - Make the UI feel instant |
| 8 | 4. **Progressive disclosure** - Use `@defer` to show content as available |
| 9 | 5. **Graceful degradation** - Partial data is better than no data |
| 10 | |
| 11 | --- |
| 12 | |
| 13 | ## Loading State Patterns |
| 14 | |
| 15 | ### The Golden Rule |
| 16 | |
| 17 | **Show loading indicator ONLY when there's no data to display.** |
| 18 | |
| 19 | ```typescript |
| 20 | @Component({ |
| 21 | template: ` |
| 22 | @if (error()) { |
| 23 | <app-error-state [error]="error()" (retry)="load()" /> |
| 24 | } @else if (loading() && !items().length) { |
| 25 | <app-skeleton-list /> |
| 26 | } @else if (!items().length) { |
| 27 | <app-empty-state message="No items found" /> |
| 28 | } @else { |
| 29 | <app-item-list [items]="items()" /> |
| 30 | } |
| 31 | `, |
| 32 | }) |
| 33 | export class ItemListComponent { |
| 34 | private store = inject(ItemStore); |
| 35 | |
| 36 | items = this.store.items; |
| 37 | loading = this.store.loading; |
| 38 | error = this.store.error; |
| 39 | } |
| 40 | ``` |
| 41 | |
| 42 | ### Loading State Decision Tree |
| 43 | |
| 44 | ``` |
| 45 | Is there an error? |
| 46 | → Yes: Show error state with retry option |
| 47 | → No: Continue |
| 48 | |
| 49 | Is it loading AND we have no data? |
| 50 | → Yes: Show loading indicator (spinner/skeleton) |
| 51 | → No: Continue |
| 52 | |
| 53 | Do we have data? |
| 54 | → Yes, with items: Show the data |
| 55 | → Yes, but empty: Show empty state |
| 56 | → No: Show loading (fallback) |
| 57 | ``` |
| 58 | |
| 59 | ### Skeleton vs Spinner |
| 60 | |
| 61 | | Use Skeleton When | Use Spinner When | |
| 62 | | -------------------- | --------------------- | |
| 63 | | Known content shape | Unknown content shape | |
| 64 | | List/card layouts | Modal actions | |
| 65 | | Initial page load | Button submissions | |
| 66 | | Content placeholders | Inline operations | |
| 67 | |
| 68 | --- |
| 69 | |
| 70 | ## Control Flow Patterns |
| 71 | |
| 72 | ### @if/@else for Conditional Rendering |
| 73 | |
| 74 | ```html |
| 75 | @if (user(); as user) { |
| 76 | <span>Welcome, {{ user.name }}</span> |
| 77 | } @else if (loading()) { |
| 78 | <app-spinner size="small" /> |
| 79 | } @else { |
| 80 | <a routerLink="/login">Sign In</a> |
| 81 | } |
| 82 | ``` |
| 83 | |
| 84 | ### @for with Track |
| 85 | |
| 86 | ```html |
| 87 | @for (item of items(); track item.id) { |
| 88 | <app-item-card [item]="item" (delete)="remove(item.id)" /> |
| 89 | } @empty { |
| 90 | <app-empty-state |
| 91 | icon="inbox" |
| 92 | message="No items yet" |
| 93 | actionLabel="Create Item" |
| 94 | (action)="create()" |
| 95 | /> |
| 96 | } |
| 97 | ``` |
| 98 | |
| 99 | ### @defer for Progressive Loading |
| 100 | |
| 101 | ```html |
| 102 | <!-- Critical content loads immediately --> |
| 103 | <app-header /> |
| 104 | <app-hero-section /> |
| 105 | |
| 106 | <!-- Non-critical content deferred --> |
| 107 | @defer (on viewport) { |
| 108 | <app-comments [postId]="postId()" /> |
| 109 | } @placeholder { |
| 110 | <div class="h-32 bg-gray-100 animate-pulse"></div> |
| 111 | } @loading (minimum 200ms) { |
| 112 | <app-spinner /> |
| 113 | } @error { |
| 114 | <app-error-state message="Failed to load comments" /> |
| 115 | } |
| 116 | ``` |
| 117 | |
| 118 | --- |
| 119 | |
| 120 | ## Error Handling Patterns |
| 121 | |
| 122 | ### Error Handling Hierarchy |
| 123 | |
| 124 | ``` |
| 125 | 1. Inline error (field-level) → Form validation errors |
| 126 | 2. Toast notification → Recoverable errors, user can retry |
| 127 | 3. Error banner → Page-level errors, data still partially usable |
| 128 | 4. Full error screen → Unrecoverable, needs user action |
| 129 | ``` |
| 130 | |
| 131 | ### Always Show Errors |
| 132 | |
| 133 | **CRITICAL: Never swallow errors silently.** |
| 134 | |
| 135 | ```typescript |
| 136 | // CORRECT - Error always surfaced to user |
| 137 | @Component({...}) |
| 138 | export class CreateItemComponent { |
| 139 | private store = inject(ItemStore); |
| 140 | private toast = inject(ToastService); |
| 141 | |
| 142 | async create(data: CreateItemDto) { |
| 143 | try { |
| 144 | await this.store.create(data); |
| 145 | this.toast.success('Item created successfully'); |
| 146 | this.router.navigate(['/items']); |
| 147 | } catch (error) { |
| 148 | console.error('createItem failed:', error); |
| 149 | this.toast.error('Failed to create item. Please try again.'); |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | // WRONG - Error silently caught |
| 155 | async create(data: CreateItemDto) { |
| 156 | try { |
| 157 | await this.store.create(data); |
| 158 | } catch (error) { |
| 159 | console.error(error); // User sees nothing! |
| 160 | } |
| 161 | } |
| 162 | ``` |
| 163 | |
| 164 | ### Error State Component Pattern |
| 165 | |
| 166 | ```typescript |
| 167 | @Component({ |
| 168 | selector: "app-error-state", |
| 169 | standalone: true, |
| 170 | imports: [NgOptimizedImage], |
| 171 | template: ` |
| 172 | <div class="error-state"> |
| 173 | <img ngSrc="/assets/error-icon.svg" width="64" height="64" alt="" /> |
| 174 | <h3>{{ title() }}</h3> |
| 175 | <p>{{ message() }}</p> |
| 176 | @if (retry.observed) { |
| 177 | <button (click)="retry.emit()" class="btn-primary">Try Again</button> |
| 178 | } |
| 179 | </div> |
| 180 | `, |
| 181 | }) |
| 182 | export class ErrorStateComponent { |
| 183 | title = input("Something went wrong"); |
| 184 | message = input("An unexpected error occurred"); |
| 185 | retry = output<void>(); |
| 186 | } |
| 187 | ``` |
| 188 | |
| 189 | --- |
| 190 | |
| 191 | ## Button State Patterns |
| 192 | |
| 193 | ### Button Loading State |
| 194 | |
| 195 | ```html |
| 196 | <button |
| 197 | (click)="handleSubmit()" |
| 198 | [disabled]="isSubmitting() || !form.valid" |
| 199 | class="btn-primary" |
| 200 | > |
| 201 | @if (isSubmitting()) { |
| 202 | <app-spinner size="small" class="mr-2" /> |
| 203 | Saving... } @else { Save Changes } |
| 204 | </button> |
| 205 | ``` |
| 206 | |
| 207 | ### Disable During Operations |
| 208 | |
| 209 | **CRITICAL: Always disable triggers during async operations.** |
| 210 | |
| 211 | ```typescript |
| 212 | // CORRECT - Button disabled while loading |
| 213 | @Component({ |
| 214 | templa |