Skip to main content Skills Marketplace Découvrez et explorez les compétences IA créées par la communauté.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Copier le promptAfficher les détails du prompt Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
npx skills add https://github.com/foolhardy45/portfolio --skill angular-ui-patternsLa commande reste sur une seule ligne. Faites défiler horizontalement pour la vérifier avant de la copier.
Vous préférez une copie locale ? Téléchargez les fichiers actuellement disponibles dans SkillsMP.
Télécharger Zip Téléchargement... Explorateur de fichiers
3 fichiers Métiers associés SOC
Basé sur la classification professionnelle SOC
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. risk safe source self date_added 2026-02-27
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()" />
}
` ,
})
{
store = ( );
items = . . ;
loading = . . ;
error = . . ;
}
export
class
ItemListComponent
private
inject
ItemStore
this
store
items
this
store
loading
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.
@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.' );
}
}
}
async create (data : CreateItemDto ) {
try {
await this .store .create (data);
} catch (error) {
console .error (error);
}
}
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.
@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 );
}
}
}
<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
@Injectable ({ providedIn : 'root' })
export class DialogService {
private dialog = inject (Dialog );
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 ;
}
}
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
@if (loading ()) {
<app-spinner />
}
@if (loading () && !items ().length ) {
<app-spinner />
}
Error Handling
try {
await this .service .save ();
} catch (e) {
console .log (e);
}
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
Data & Mutations
Accessibility
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.