Files
ISA-Frontend/libs/ui/skeleton-loader/README.md
Lorenz Hilpert 2b5da00249 feat(checkout): add reward order confirmation feature with schema migrations
- 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
2025-10-21 14:28:52 +02:00

483 lines
12 KiB
Markdown

# @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](#features)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Architecture Notes](#architecture-notes)
- [Testing](#testing)
- [Dependencies](#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
```typescript
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
```html
<!-- Replace content with skeleton while loading -->
<div *uiSkeletonLoader="isLoading">
<h2>Product Name</h2>
<p>Product description goes here...</p>
</div>
```
### 3. Custom Dimensions
```html
<!-- Specify exact dimensions for the skeleton -->
<div *uiSkeletonLoader="isLoading; width: '200px'; height: '50px'">
Content to hide while loading
</div>
```
### 4. Using the Component Directly
```typescript
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 `ViewContainerRef` to 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
```html
<div class="ui-skeleton-loader-bar"></div>
```
## Usage Examples
### Basic Loading State
```typescript
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
```html
<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
```html
<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
```typescript
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
```html
<!-- 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 element
- `ui-skeleton-loader-bar` - Applied to the animated bar element
### Custom Styles
You can customize the skeleton appearance using CSS:
```scss
// 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:
```html
<!-- 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:
```typescript
// 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:
1. **Loading State** (`condition = true`):
- Clears existing views
- Creates `SkeletonLoaderComponent` instance
- Applies dimension styles
- Clears embedded view reference
2. **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
1. **Minimal DOM Operations** - Only creates/destroys views when state changes
2. **Signal Efficiency** - Uses computed values and effects for reactive updates
3. **OnPush Strategy** - Component only checks when inputs change
4. **Untracked Styles** - Style application doesn't trigger additional effects
## Testing
The library uses **Jest** for testing.
### Running Tests
```bash
# 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:
```typescript
// 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:
```html
<!-- 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:
```html
<!-- 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:
```html
<!-- Skeleton automatically adapts to container -->
<div class="responsive-container">
<div *uiSkeletonLoader="isLoading">
{{ content }}
</div>
</div>
```
## License
Internal ISA Frontend library - not for external distribution.