Files
ISA-Frontend/libs/ui/skeleton-loader
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
..

@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

  • 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 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

<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 element
  • ui-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:

  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

# 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.