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