mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
- 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
483 lines
12 KiB
Markdown
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.
|