- 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
@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-actionscontainer
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:
- SVG strings are hardcoded constants (not user input)
- No dynamic interpolation of external data into SVGs
- Icons are static, design-system-approved assets
- DomSanitizer explicitly bypasses only for known-safe content
Styling Architecture
Component SCSS (empty-state.component.scss):
- Scoped to
.ui-empty-stateclass - 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.Noneallows 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
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:
- Check existing tests in
empty-state.component.spec.tsfor usage examples - Review SCSS styles for customization guidance
- Consult ISA Design System for visual/UX standards
- 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