# @isa/ui/empty-state

A standalone Angular component library providing consistent empty state displays for various scenarios (no results, no articles, all done, select action). Part of the ISA Design System.

## Overview

The Empty State component library delivers a reusable, accessible empty state component with multiple appearance variants and customizable content. It provides visual feedback to users when data is unavailable, actions are complete, or user input is required.

### Key Features

- **Multiple Appearance Variants**: 4 pre-configured visual styles (NoResults, NoArticles, AllDone, SelectAction)
- **Embedded SVG Icons**: Rich, contextual SVG illustrations with dynamic rendering
- **Content Projection**: Flexible action area for custom buttons/links via `<ng-content>`
- **Signal-Based Architecture**: Modern Angular signals for reactive icon rendering
- **Sanitized HTML**: Secure SVG rendering using Angular's DomSanitizer
- **OnPush Change Detection**: Optimized performance with minimal re-renders
- **Type-Safe API**: Strongly typed appearance options with TypeScript const assertions
- **SCSS Styling**: Component-scoped styles with CSS class hooks

## Installation

This library is part of the ISA monorepo and uses path aliases for imports:

```typescript
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
```

## Quick Start

### Basic Usage

```typescript
import { Component } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';

@Component({
  selector: 'app-search-results',
  standalone: true,
  imports: [EmptyStateComponent],
  template: `
    <ui-empty-state
      title="Keine Ergebnisse gefunden"
      description="Versuchen Sie es mit anderen Suchbegriffen oder Filtern."
      [appearance]="EmptyStateAppearance.NoResults"
    />
  `
})
export class SearchResultsComponent {
  EmptyStateAppearance = EmptyStateAppearance;
}
```

### With Custom Actions

```typescript
import { Component } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
import { ButtonComponent } from '@isa/ui/buttons';

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [EmptyStateComponent, ButtonComponent],
  template: `
    <ui-empty-state
      title="Keine Artikel verfügbar"
      description="Es wurden keine passenden Artikel gefunden."
      [appearance]="EmptyStateAppearance.NoArticles"
    >
      <ui-button (click)="resetFilters()">Filter zurücksetzen</ui-button>
      <ui-button variant="secondary" (click)="goBack()">Zurück</ui-button>
    </ui-empty-state>
  `
})
export class ProductListComponent {
  EmptyStateAppearance = EmptyStateAppearance;

  resetFilters(): void {
    // Reset filter logic
  }

  goBack(): void {
    // Navigation logic
  }
}
```

## API Reference

### EmptyStateComponent

**Selector**: `ui-empty-state`

#### Inputs

| Input | Type | Default | Required | Description |
|-------|------|---------|----------|-------------|
| `title` | `string` | - | ✅ | Main heading text for the empty state |
| `description` | `string` | - | ✅ | Supporting description text |
| `appearance` | `EmptyStateAppearance` | `EmptyStateAppearance.NoResults` | ❌ | Visual variant (determines icon) |

#### Content Projection

- **Default Slot**: Projects custom action buttons/links into the `.ui-empty-state-actions` container

#### Host Properties

- **CSS Class**: `.ui-empty-state` - Applied to the host element for styling

#### Component Configuration

- **Change Detection**: `OnPush` - Optimized for performance
- **View Encapsulation**: `None` - Allows global styles to cascade
- **Standalone**: `true` - Can be imported directly without NgModule

### EmptyStateAppearance Type

```typescript
export const EmptyStateAppearance = {
  NoResults: 'noResults',      // Search/filter returned no results
  NoArticles: 'noArticles',    // No articles/products available
  AllDone: 'allDone',          // Task completion state
  SelectAction: 'selectAction' // User needs to select an action
} as const;

export type EmptyStateAppearance =
  (typeof EmptyStateAppearance)[keyof typeof EmptyStateAppearance];
```

#### Appearance Variants

| Variant | Icon | Use Case |
|---------|------|----------|
| `NoResults` | Document with magnifying glass and X | Search returned no matches, filters excluded all items |
| `NoArticles` | Document with X mark | Product catalog is empty, no items in category |
| `AllDone` | Coffee cup with steam | All tasks completed, inbox zero state |
| `SelectAction` | Hand pointer with dropdown | User needs to choose from dropdown/menu |

## Usage Examples

### Example 1: Search Results Empty State

```typescript
import { Component, computed, signal } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
import { ButtonComponent } from '@isa/ui/buttons';

@Component({
  selector: 'app-customer-search',
  standalone: true,
  imports: [EmptyStateComponent, ButtonComponent],
  template: `
    @if (hasResults()) {
      <!-- Display results -->
    } @else {
      <ui-empty-state
        title="Keine Kunden gefunden"
        description="Ihre Suche ergab keine Treffer. Überprüfen Sie Ihre Suchkriterien oder versuchen Sie es mit anderen Begriffen."
        [appearance]="EmptyStateAppearance.NoResults"
      >
        <ui-button (click)="clearSearch()">Suche zurücksetzen</ui-button>
      </ui-empty-state>
    }
  `
})
export class CustomerSearchComponent {
  EmptyStateAppearance = EmptyStateAppearance;

  searchResults = signal<Customer[]>([]);
  hasResults = computed(() => this.searchResults().length > 0);

  clearSearch(): void {
    this.searchResults.set([]);
  }
}
```

### Example 2: Task Completion State

```typescript
import { Component } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
import { Router } from '@angular/router';

@Component({
  selector: 'app-return-process',
  standalone: true,
  imports: [EmptyStateComponent],
  template: `
    <ui-empty-state
      title="Alle Rücksendungen bearbeitet"
      description="Großartig! Sie haben alle ausstehenden Rücksendungen abgeschlossen."
      [appearance]="EmptyStateAppearance.AllDone"
    >
      <ui-button (click)="goToDashboard()">Zum Dashboard</ui-button>
    </ui-empty-state>
  `
})
export class ReturnProcessComponent {
  EmptyStateAppearance = EmptyStateAppearance;

  constructor(private router: Router) {}

  goToDashboard(): void {
    this.router.navigate(['/dashboard']);
  }
}
```

### Example 3: Action Selection Required

```typescript
import { Component } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';

@Component({
  selector: 'app-workflow-start',
  standalone: true,
  imports: [EmptyStateComponent],
  template: `
    <ui-empty-state
      title="Wählen Sie eine Aktion aus"
      description="Bitte wählen Sie aus dem Dropdown-Menü oben eine Aktion aus, um fortzufahren."
      [appearance]="EmptyStateAppearance.SelectAction"
    />
  `
})
export class WorkflowStartComponent {
  EmptyStateAppearance = EmptyStateAppearance;
}
```

### Example 4: Conditional Appearance Based on Context

```typescript
import { Component, computed, signal } from '@angular/core';
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';

type EmptyReason = 'no-results' | 'no-articles' | 'completed' | 'select-action';

@Component({
  selector: 'app-dynamic-empty-state',
  standalone: true,
  imports: [EmptyStateComponent],
  template: `
    <ui-empty-state
      [title]="emptyTitle()"
      [description]="emptyDescription()"
      [appearance]="emptyAppearance()"
    >
      @if (reason() === 'no-results') {
        <ui-button (click)="resetFilters()">Filter zurücksetzen</ui-button>
      }
    </ui-empty-state>
  `
})
export class DynamicEmptyStateComponent {
  reason = signal<EmptyReason>('no-results');

  emptyAppearance = computed(() => {
    switch (this.reason()) {
      case 'no-results':
        return EmptyStateAppearance.NoResults;
      case 'no-articles':
        return EmptyStateAppearance.NoArticles;
      case 'completed':
        return EmptyStateAppearance.AllDone;
      case 'select-action':
        return EmptyStateAppearance.SelectAction;
      default:
        return EmptyStateAppearance.NoResults;
    }
  });

  emptyTitle = computed(() => {
    switch (this.reason()) {
      case 'no-results':
        return 'Keine Ergebnisse';
      case 'no-articles':
        return 'Keine Artikel';
      case 'completed':
        return 'Alles erledigt';
      case 'select-action':
        return 'Aktion auswählen';
      default:
        return 'Leer';
    }
  });

  emptyDescription = computed(() => {
    // Description logic based on reason
    return 'Beschreibung basierend auf dem Kontext';
  });

  resetFilters(): void {
    // Reset logic
  }
}
```

## Architecture Notes

### Component Structure

```
EmptyStateComponent
├── Host Element (.ui-empty-state)
│   ├── Sub-Icon Container (@if sanitizedSubIcon())
│   │   └── [innerHTML] (AllDone steam, SelectAction hand)
│   ├── Main Icon Circle (.ui-empty-state-circle)
│   │   └── [innerHTML] (Primary SVG icon)
│   ├── Title (.ui-empty-state-title)
│   │   └── {{ title() }}
│   ├── Description (.ui-empty-state-description)
│   │   └── {{ description() }}
│   └── Actions Container (.ui-empty-state-actions)
│       └── <ng-content> (Projected buttons/links)
```

### Signal Computation Flow

```typescript
// 1. Input signals (set by parent component)
appearance: InputSignal<EmptyStateAppearance>

// 2. Derived computed signals
icon: Signal<string> = computed(() => {
  // Maps appearance to primary SVG constant
})

subIcon: Signal<string> = computed(() => {
  // Returns secondary SVG for AllDone/SelectAction, empty string otherwise
})

// 3. Sanitized computed signals
sanitizedIcon: Signal<SafeHtml> = computed(() => {
  // Bypasses security for safe HTML rendering
})

sanitizedSubIcon: Signal<SafeHtml | undefined> = computed(() => {
  // Conditionally sanitizes sub-icon if present
})
```

### SVG Icon Management

**Constants Module** (`constants.ts`):
- Stores complete SVG markup as string constants
- Each SVG includes viewBox, dimensions, and semantic path data
- Icons designed for ISA color palette (`#CED4DA`, `#6C757D`)

**Icon Selection Logic**:
```typescript
icon = computed(() => {
  const appearance = this.appearance();
  switch (appearance) {
    case EmptyStateAppearance.NoArticles:
      return NO_ARTICLES;
    case EmptyStateAppearance.AllDone:
      return ALL_DONE_CUP;
    case EmptyStateAppearance.SelectAction:
      return SELECT_ACTION_OBJECT_DROPDOWN;
    case EmptyStateAppearance.NoResults:
    default:
      return NO_RESULTS;
  }
});
```

### Security Considerations

**DomSanitizer Usage**:
- All SVG content sanitized via `bypassSecurityTrustHtml()`
- Safe because SVG constants are internal, trusted sources
- Prevents XSS attacks from user-supplied content
- Template binding via `[innerHTML]` for dynamic rendering

**Why Sanitization is Safe Here**:
1. SVG strings are hardcoded constants (not user input)
2. No dynamic interpolation of external data into SVGs
3. Icons are static, design-system-approved assets
4. DomSanitizer explicitly bypasses only for known-safe content

### Styling Architecture

**Component SCSS** (`empty-state.component.scss`):
- Scoped to `.ui-empty-state` class
- Defines layout, spacing, and typography
- Uses ISA design tokens for consistency
- CSS classes target: `.ui-empty-state-circle`, `.ui-empty-state-title`, `.ui-empty-state-description`, `.ui-empty-state-actions`

**Global Style Integration**:
- `ViewEncapsulation.None` allows external styles to cascade
- Tailwind utilities can style projected content
- Maintains design system consistency

### Performance Characteristics

**Optimization Strategies**:
- **OnPush Change Detection**: Component only re-renders when inputs change
- **Computed Signals**: Icon selection memoized, recalculates only on appearance change
- **Conditional Rendering**: Sub-icons only render when present (`@if sanitizedSubIcon()`)
- **Static Constants**: SVG strings stored in memory once, reused across instances

**Bundle Impact**:
- Standalone component with minimal dependencies
- SVG icons increase bundle size (~4KB total for all icons)
- No external image assets required
- Tree-shakeable via ES modules

## Dependencies

### Angular Dependencies

| Package | Version | Purpose |
|---------|---------|---------|
| `@angular/core` | 20.1.2 | Component framework, signals, dependency injection |
| `@angular/platform-browser` | 20.1.2 | DomSanitizer for secure HTML rendering |

### Internal Dependencies

None - this library is fully self-contained with no dependencies on other `@isa/*` libraries.

### Peer Dependencies

This library expects the following packages in the consuming application:

```json
{
  "peerDependencies": {
    "@angular/core": "^20.0.0",
    "@angular/platform-browser": "^20.0.0"
  }
}
```

## Testing

### Test Configuration

**Framework**: Jest (legacy, migrating to Vitest)

**Test File**: `empty-state.component.spec.ts`

**Configuration**: `jest.config.ts` (extends workspace defaults)

### Running Tests

```bash
# Run tests with fresh results (skip Nx cache)
npx nx test ui-empty-state --skip-nx-cache

# Run tests in watch mode
npx nx test ui-empty-state --watch

# Run tests with coverage
npx nx test ui-empty-state --code-coverage --skip-nx-cache
```

### Testing Recommendations

#### Unit Test Coverage

**Test appearance variants**:
```typescript
it('should display NoResults icon by default', () => {
  const fixture = TestBed.createComponent(EmptyStateComponent);
  fixture.componentRef.setInput('title', 'No Results');
  fixture.componentRef.setInput('description', 'Try again');
  fixture.detectChanges();

  const compiled = fixture.nativeElement;
  const iconElement = compiled.querySelector('.ui-empty-state-circle');
  expect(iconElement.innerHTML).toContain('M75.5 43.794V30.1846');
});
```

**Test content projection**:
```typescript
it('should project custom actions into actions container', () => {
  const fixture = TestBed.createComponent(EmptyStateComponent);
  fixture.componentRef.setInput('title', 'Test');
  fixture.componentRef.setInput('description', 'Test');

  const compiled = fixture.nativeElement;
  const actionsContainer = compiled.querySelector('.ui-empty-state-actions');
  const projectedButton = actionsContainer.querySelector('button');

  expect(projectedButton).toBeTruthy();
  expect(projectedButton.textContent).toContain('Custom Action');
});
```

**Test sanitization**:
```typescript
it('should sanitize icon HTML for security', () => {
  const component = new EmptyStateComponent();
  const sanitizer = TestBed.inject(DomSanitizer);

  // Verify sanitizer is injected
  expect(component['#sanitizer']).toBeDefined();

  // Verify sanitized output
  const sanitizedIcon = component.sanitizedIcon();
  expect(sanitizedIcon).toBeTruthy();
});
```

#### Integration Test Scenarios

**Test with routing actions**:
```typescript
@Component({
  template: `
    <ui-empty-state
      title="Test"
      description="Test description"
      [appearance]="EmptyStateAppearance.NoResults"
    >
      <button (click)="navigate()">Go Home</button>
    </ui-empty-state>
  `
})
class TestHostComponent {
  EmptyStateAppearance = EmptyStateAppearance;
  navigated = false;

  navigate(): void {
    this.navigated = true;
  }
}

it('should trigger navigation when action button clicked', () => {
  const fixture = TestBed.createComponent(TestHostComponent);
  fixture.detectChanges();

  const button = fixture.nativeElement.querySelector('button');
  button.click();

  expect(fixture.componentInstance.navigated).toBe(true);
});
```

## Best Practices

### 1. Choose Appropriate Appearance

Match the appearance variant to the user's context:

```typescript
// ✅ GOOD: Context-appropriate variant
<ui-empty-state
  title="Keine Suchergebnisse"
  description="..."
  [appearance]="EmptyStateAppearance.NoResults"
/>

// ❌ BAD: Misleading variant
<ui-empty-state
  title="Keine Suchergebnisse"
  description="..."
  [appearance]="EmptyStateAppearance.AllDone"  // Wrong icon!
/>
```

### 2. Provide Actionable Guidance

Always include helpful descriptions and recovery actions:

```typescript
// ✅ GOOD: Clear guidance with actions
<ui-empty-state
  title="Keine Artikel gefunden"
  description="Versuchen Sie, Ihre Suchkriterien anzupassen oder Filter zurückzusetzen."
>
  <ui-button (click)="resetFilters()">Filter zurücksetzen</ui-button>
  <ui-button variant="secondary" (click)="clearSearch()">Suche löschen</ui-button>
</ui-empty-state>

// ❌ BAD: Vague, no actions
<ui-empty-state
  title="Nichts gefunden"
  description="Keine Daten"
/>
```

### 3. Use Computed Signals for Dynamic Content

Leverage Angular signals for reactive empty states:

```typescript
// ✅ GOOD: Reactive, computed approach
export class ProductListComponent {
  products = signal<Product[]>([]);
  isLoading = signal(false);

  emptyTitle = computed(() => {
    if (this.isLoading()) return 'Laden...';
    return 'Keine Produkte gefunden';
  });

  emptyDescription = computed(() => {
    if (this.isLoading()) return 'Bitte warten...';
    return 'Versuchen Sie es mit anderen Filtern.';
  });
}

// ❌ BAD: Static, non-reactive
export class ProductListComponent {
  emptyTitle = 'Keine Produkte';  // Won't update dynamically
}
```

### 4. Maintain Consistent Messaging

Follow ISA language guidelines for title/description text:

```typescript
// ✅ GOOD: Professional, helpful tone
title="Keine Rücksendungen vorhanden"
description="Es liegen derzeit keine Rücksendungen vor. Neue Rücksendungen erscheinen automatisch hier."

// ❌ BAD: Casual, unhelpful
title="Ups, nichts da!"
description="Schade."
```

### 5. Test All Appearance Variants

Ensure all visual variants render correctly:

```typescript
describe('EmptyStateComponent Appearances', () => {
  const appearances = [
    EmptyStateAppearance.NoResults,
    EmptyStateAppearance.NoArticles,
    EmptyStateAppearance.AllDone,
    EmptyStateAppearance.SelectAction
  ];

  appearances.forEach(appearance => {
    it(`should render ${appearance} appearance correctly`, () => {
      const fixture = TestBed.createComponent(EmptyStateComponent);
      fixture.componentRef.setInput('title', 'Test');
      fixture.componentRef.setInput('description', 'Test');
      fixture.componentRef.setInput('appearance', appearance);
      fixture.detectChanges();

      const iconElement = fixture.nativeElement.querySelector('.ui-empty-state-circle');
      expect(iconElement).toBeTruthy();
      expect(iconElement.innerHTML).toContain('svg');
    });
  });
});
```

### 6. Accessibility Considerations

Ensure empty states are accessible:

```typescript
// ✅ GOOD: Semantic HTML with ARIA
<ui-empty-state
  title="Keine Ergebnisse"
  description="Ihre Suche ergab keine Treffer"
  role="status"
  aria-live="polite"
>
  <ui-button>Filter zurücksetzen</ui-button>
</ui-empty-state>

// Consider adding to template:
// <div role="status" aria-live="polite">
//   <ui-empty-state .../>
// </div>
```

### 7. Avoid Overuse

Don't use empty states for loading or error conditions:

```typescript
// ✅ GOOD: Empty state for actual empty data
@if (products().length === 0 && !isLoading()) {
  <ui-empty-state .../>
}

// ❌ BAD: Empty state for loading
@if (isLoading()) {
  <ui-empty-state title="Laden..." description="Bitte warten"/>
}
// Use a loading spinner instead!
```

## Common Pitfalls

### 1. Forgetting Required Inputs

```typescript
// ❌ ERROR: Missing required inputs
<ui-empty-state [appearance]="EmptyStateAppearance.NoResults" />
// Angular will throw an error for missing title/description

// ✅ CORRECT: All required inputs provided
<ui-empty-state
  title="Erforderlich"
  description="Erforderlich"
  [appearance]="EmptyStateAppearance.NoResults"
/>
```

### 2. Incorrect Appearance Type

```typescript
// ❌ ERROR: Invalid string literal
<ui-empty-state
  title="Test"
  description="Test"
  appearance="no-results"  // TypeScript error!
/>

// ✅ CORRECT: Use EmptyStateAppearance enum
<ui-empty-state
  title="Test"
  description="Test"
  [appearance]="EmptyStateAppearance.NoResults"
/>
```

### 3. Projecting Non-Action Content

```typescript
// ❌ BAD: Projecting large content blocks
<ui-empty-state title="Test" description="Test">
  <div>
    <p>Long paragraph...</p>
    <table>...</table>
  </div>
</ui-empty-state>

// ✅ GOOD: Project only action buttons
<ui-empty-state title="Test" description="Test">
  <ui-button>Action 1</ui-button>
  <ui-button variant="secondary">Action 2</ui-button>
</ui-empty-state>
```

## Migration Guide

### From Custom Empty States to @isa/ui/empty-state

**Before**:
```typescript
@Component({
  template: `
    <div class="empty-state">
      <img src="/assets/no-results.svg" alt="No Results">
      <h2>Keine Ergebnisse</h2>
      <p>Versuchen Sie es erneut.</p>
      <button (click)="retry()">Erneut versuchen</button>
    </div>
  `
})
```

**After**:
```typescript
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';

@Component({
  standalone: true,
  imports: [EmptyStateComponent],
  template: `
    <ui-empty-state
      title="Keine Ergebnisse"
      description="Versuchen Sie es erneut."
      [appearance]="EmptyStateAppearance.NoResults"
    >
      <ui-button (click)="retry()">Erneut versuchen</ui-button>
    </ui-empty-state>
  `
})
```

**Benefits**:
- Consistent design across application
- Built-in icons, no asset management
- Accessible, semantic HTML structure
- Optimized rendering performance

## Related Documentation

- **ISA Design System**: Design guidelines for empty states
- **@isa/ui/buttons**: Button components for empty state actions
- **Angular Signals Guide**: Understanding reactive signal patterns
- **Security Best Practices**: DomSanitizer usage guidelines

## Support and Contributing

For questions, issues, or contributions related to the Empty State component:

1. Check existing tests in `empty-state.component.spec.ts` for usage examples
2. Review SCSS styles for customization guidance
3. Consult ISA Design System for visual/UX standards
4. Follow Angular standalone component best practices

## Changelog

### Current Version

**Features**:
- Standalone component architecture
- 4 appearance variants with embedded SVG icons
- Signal-based reactive rendering
- Content projection for custom actions
- OnPush change detection optimization
- Secure HTML sanitization

**Testing**:
- Jest configuration (migrating to Vitest)
- Component unit tests with Spectator (migrating to Angular Testing Library)

---

**Package**: `@isa/ui/empty-state`
**Path Alias**: `@isa/ui/empty-state`
**Entry Point**: `libs/ui/empty-state/src/index.ts`
**Selector**: `ui-empty-state`
**Type**: Standalone Angular Component Library
