- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
12 KiB
@isa/ui/skeleton-loader
A lightweight Angular structural directive and component for displaying skeleton loading states during asynchronous operations.
Overview
The Skeleton Loader library provides a simple yet effective way to show loading placeholders while content is being fetched. It uses a structural directive pattern that conditionally replaces content with an animated skeleton loader, improving perceived performance and user experience during data loading.
Table of Contents
- Features
- Quick Start
- API Reference
- Usage Examples
- Styling and Customization
- Architecture Notes
- Testing
- Dependencies
Features
- Structural directive pattern - Seamlessly replaces content with skeleton loader
- Signal-based reactivity - Uses Angular signals for efficient change detection
- Customizable dimensions - Configure width and height via directive inputs
- OnPush change detection - Optimized for performance
- Minimal footprint - Lightweight implementation with no external dependencies
- Smooth transitions - Automatically handles view switching between loading and content states
- Standalone component - Can be used independently of the directive
Quick Start
1. Import the Directive
import { Component } from '@angular/core';
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [SkeletonLoaderDirective],
template: '...'
})
export class ProductListComponent {
isLoading = true;
}
2. Basic Usage
<!-- Replace content with skeleton while loading -->
<div *uiSkeletonLoader="isLoading">
<h2>Product Name</h2>
<p>Product description goes here...</p>
</div>
3. Custom Dimensions
<!-- Specify exact dimensions for the skeleton -->
<div *uiSkeletonLoader="isLoading; width: '200px'; height: '50px'">
Content to hide while loading
</div>
4. Using the Component Directly
import { SkeletonLoaderComponent } from '@isa/ui/skeleton-loader';
@Component({
selector: 'app-card',
standalone: true,
imports: [SkeletonLoaderComponent],
template: `
@if (isLoading) {
<ui-skeleton-loader style="width: 100%; height: 200px;"></ui-skeleton-loader>
} @else {
<div class="card-content">{{ content }}</div>
}
`
})
export class CardComponent {
isLoading = true;
content = '';
}
API Reference
SkeletonLoaderDirective
Structural directive that conditionally replaces its content with a skeleton loader.
Selector: [uiSkeletonLoader]
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
uiSkeletonLoader |
boolean |
false |
When true, shows skeleton loader; when false, shows original content |
uiSkeletonLoaderWidth |
string | undefined |
'inherit' |
CSS width value for the skeleton loader (e.g., '100px', '50%', 'inherit') |
uiSkeletonLoaderHeight |
string | undefined |
'inherit' |
CSS height value for the skeleton loader (e.g., '20px', '100%', 'inherit') |
Implementation Details
- Uses
ViewContainerRefto dynamically create/destroy views - Employs
effect()for reactive rendering based on input signals - Automatically clears previous view before creating new one
- Applies dimension styles using
untracked()to prevent circular updates
SkeletonLoaderComponent
Standalone component that renders a skeleton loading animation.
Selector: ui-skeleton-loader
Features
- OnPush change detection strategy
- ViewEncapsulation.None for global styling
- Host class:
ui-skeleton-loader - Single animated bar element with class
ui-skeleton-loader-bar
Template Structure
<div class="ui-skeleton-loader-bar"></div>
Usage Examples
Basic Loading State
import { Component, signal } from '@angular/core';
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [SkeletonLoaderDirective],
template: `
<div *uiSkeletonLoader="isLoading()">
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
</div>
`
})
export class UserProfileComponent {
isLoading = signal(true);
user = { name: '', email: '' };
ngOnInit() {
// Simulate API call
setTimeout(() => {
this.user = { name: 'John Doe', email: 'john@example.com' };
this.isLoading.set(false);
}, 2000);
}
}
List with Multiple Skeletons
<div class="product-list">
@for (item of items; track item.id) {
<div
class="product-card"
*uiSkeletonLoader="isLoading; width: '100%'; height: '120px'"
>
<img [src]="item.image" alt="{{ item.name }}">
<h3>{{ item.name }}</h3>
<p>{{ item.price | currency }}</p>
</div>
}
</div>
Table Row Skeletons
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (row of tableData; track row.id) {
<tr *uiSkeletonLoader="isLoadingRow(row.id); height: '48px'">
<td>{{ row.name }}</td>
<td>{{ row.email }}</td>
<td>{{ row.status }}</td>
</tr>
}
</tbody>
</table>
Using with RxJS Observables
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
@Component({
selector: 'app-data-viewer',
standalone: true,
imports: [SkeletonLoaderDirective],
template: `
<div *uiSkeletonLoader="isLoading(); width: '100%'; height: '400px'">
@if (data(); as dataValue) {
<pre>{{ dataValue | json }}</pre>
}
</div>
`
})
export class DataViewerComponent {
#dataService = inject(DataService);
data$ = this.#dataService.getData();
data = toSignal(this.data$);
isLoading = toSignal(
this.data$.pipe(map(data => !data)),
{ initialValue: true }
);
}
Standalone Component Usage
<!-- When you need direct control over skeleton rendering -->
<div class="custom-container">
@if (isLoading) {
<ui-skeleton-loader
style="width: 100%; height: 200px; margin-bottom: 1rem;"
></ui-skeleton-loader>
<ui-skeleton-loader
style="width: 80%; height: 100px;"
></ui-skeleton-loader>
} @else {
<div class="actual-content">
{{ content }}
</div>
}
</div>
Styling and Customization
Default Styling
The skeleton loader applies the following host classes:
ui-skeleton-loader- Applied to the component host elementui-skeleton-loader-bar- Applied to the animated bar element
Custom Styles
You can customize the skeleton appearance using CSS:
// Custom skeleton colors and animation
.ui-skeleton-loader {
background-color: #f0f0f0;
border-radius: 4px;
}
.ui-skeleton-loader-bar {
background: linear-gradient(
90deg,
#f0f0f0 0%,
#e0e0e0 50%,
#f0f0f0 100%
);
animation: skeleton-loading 1.5s ease-in-out infinite;
}
@keyframes skeleton-loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
Dimension Inheritance
When width or height are not specified (or set to 'inherit'), the skeleton loader inherits dimensions from its parent container:
<!-- Skeleton inherits parent's dimensions -->
<div style="width: 300px; height: 150px;">
<div *uiSkeletonLoader="isLoading">
Content
</div>
</div>
ViewEncapsulation
The component uses ViewEncapsulation.None, allowing global styles to apply. This enables:
- Consistent styling across the application
- Easy theming via global CSS variables
- Override styles from parent components
Architecture Notes
Design Pattern
The library follows the Structural Directive + Presentation Component pattern:
SkeletonLoaderDirective (Structural Logic)
↓
Creates/Destroys views dynamically
↓
SkeletonLoaderComponent (Presentation)
Signal-Based Reactivity
The directive uses Angular signals for optimal performance:
// Reactive rendering
render = effect(() => {
const condition = this.uiSkeletonLoader();
if (condition && !this.componentRef) {
// Show skeleton
} else if (!condition && !this.embeddedViewRef) {
// Show content
}
});
// Reactive style updates
updateStyles = effect(() => {
this.applyStyles();
});
View Management
The directive efficiently manages view lifecycle:
-
Loading State (
condition = true):- Clears existing views
- Creates
SkeletonLoaderComponentinstance - Applies dimension styles
- Clears embedded view reference
-
Loaded State (
condition = false):- Clears existing views
- Creates embedded view from template
- Clears component reference
Memory Management
- Component/view references are properly cleaned up
- Uses
untracked()to prevent circular signal updates - OnPush change detection reduces unnecessary checks
Performance Considerations
- Minimal DOM Operations - Only creates/destroys views when state changes
- Signal Efficiency - Uses computed values and effects for reactive updates
- OnPush Strategy - Component only checks when inputs change
- Untracked Styles - Style application doesn't trigger additional effects
Testing
The library uses Jest for testing.
Running Tests
# Run tests for skeleton-loader
npx nx test ui-skeleton-loader --skip-nx-cache
# Run tests with coverage
npx nx test ui-skeleton-loader --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-skeleton-loader --watch
Test Coverage
The library includes comprehensive tests covering:
- Directive behavior - View creation/destruction based on condition
- Dimension application - Width/height style application
- Signal reactivity - Proper effect execution
- View lifecycle - Component and template ref management
- Edge cases - Undefined dimensions, rapid state changes
Dependencies
Required Libraries
@angular/core- Angular framework (v20.1.2)
Path Alias
Import from: @isa/ui/skeleton-loader
Peer Dependencies
- Angular 20.1.2 or higher
- TypeScript 5.8.3 or higher
Best Practices
1. Use for Async Operations
Always tie the skeleton loader to actual loading states:
// Good: Tied to actual data loading
isLoading = toSignal(this.data$.pipe(map(data => !data)));
// Bad: Arbitrary timeout
setTimeout(() => this.isLoading = false, 1000);
2. Match Skeleton to Content
Ensure skeleton dimensions approximate actual content:
<!-- Good: Skeleton matches content dimensions -->
<div *uiSkeletonLoader="isLoading; width: '100%'; height: '48px'">
<h2 class="text-xl">{{ title }}</h2> <!-- ~48px height -->
</div>
<!-- Bad: Skeleton much smaller than content -->
<div *uiSkeletonLoader="isLoading; height: '20px'">
<div class="large-card">...</div> <!-- 300px height -->
</div>
3. Avoid Nested Skeletons
Don't nest skeleton loader directives:
<!-- Bad: Nested skeletons -->
<div *uiSkeletonLoader="isLoadingOuter">
<div *uiSkeletonLoader="isLoadingInner">
Content
</div>
</div>
<!-- Good: Single skeleton or separate skeletons -->
<div *uiSkeletonLoader="isLoading">
<div>Content</div>
</div>
4. Use Inherit for Flexible Layouts
Let the skeleton inherit dimensions for responsive layouts:
<!-- Skeleton automatically adapts to container -->
<div class="responsive-container">
<div *uiSkeletonLoader="isLoading">
{{ content }}
</div>
</div>
License
Internal ISA Frontend library - not for external distribution.