angular-ui-patterns

安装量: 166
排名: #5188

安装

npx skills add https://github.com/sickn33/antigravity-awesome-skills --skill angular-ui-patterns

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 @defer to 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()) { <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

< app-header /> < app-hero-section />

@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 : `

{{ title() }}

{{ message() }}

@if (retry.observed) { }

` , } ) 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()) { 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 < 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 : `

{{ title() }}

{{ description() }}

@if (actionLabel()) { }

` , } ) 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 : `

@if (isFieldInvalid("name")) { {{ getFieldError("name") }} }
@if (isFieldInvalid("email")) { {{ getFieldError("email") }} }

` , } ) 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

< button (click) = " submit() "

Submit </ button

<
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.
返回排行榜