Files
ISA-Frontend/libs/ui/empty-state
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/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:

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

Quick Start

Basic Usage

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

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

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

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

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

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

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

// 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:

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:

{
  "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

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

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:

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:

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:

@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:

// ✅ 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:

// ✅ 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:

// ✅ 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:

// ✅ 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:

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:

// ✅ 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:

// ✅ 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

// ❌ 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

// ❌ 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

// ❌ 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:

@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:

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