Files
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/search-bar

A feature-rich Angular search bar component with integrated clear functionality and customizable appearance modes.

Overview

The Search Bar library provides a flexible and accessible search input component designed for the ISA application. It supports two appearance modes (main and results), integrates seamlessly with Angular Forms using NgControl, and includes a dedicated clear button component with automatic focus restoration.

Table of Contents

Features

  • Two appearance modes - Main and results views with distinct styling
  • Angular Forms integration - Works with FormControl and NgControl via content projection
  • Integrated clear button - Dedicated component with automatic focus restoration
  • Icon support - Prefix and suffix icon slots using ng-icons
  • Signal-based reactivity - Computed appearance classes for efficient updates
  • OnPush change detection - Optimized performance
  • Accessibility - Proper focus management and ARIA attributes
  • Content projection - Flexible composition with multiple slot patterns

Quick Start

1. Import the Components

import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';
import { IconButtonComponent } from '@isa/ui/buttons';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaActionSearch } from '@isa/icons';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    UiSearchBarComponent,
    UiSearchBarClearComponent,
    IconButtonComponent,
    NgIconComponent
  ],
  providers: [provideIcons({ isaActionSearch })],
  template: '...'
})
export class SearchComponent {
  searchControl = new FormControl('');
}

2. Basic Usage

<ui-search-bar>
  <input
    type="text"
    [formControl]="searchControl"
    placeholder="Search..."
    data-what="search-input"
  />
  <ui-search-bar-clear></ui-search-bar-clear>
  <ui-icon-button name="isaActionSearch"></ui-icon-button>
</ui-search-bar>

3. With Prefix Icon

<ui-search-bar appearance="results">
  <ui-icon-button prefix name="isaActionSearch"></ui-icon-button>
  <input
    type="text"
    [formControl]="searchControl"
    placeholder="Filter results..."
  />
  <ui-search-bar-clear></ui-search-bar-clear>
</ui-search-bar>

4. Custom Reset Value

<ui-search-bar>
  <input type="text" [formControl]="searchControl" />
  <!-- Reset to specific value instead of null -->
  <ui-search-bar-clear [value]="defaultSearchTerm"></ui-search-bar-clear>
  <ui-icon-button name="isaActionSearch"></ui-icon-button>
</ui-search-bar>

API Reference

UiSearchBarComponent

Container component that provides structure and styling for search inputs.

Selector: ui-search-bar

Inputs

Input Type Default Description
appearance SearchbarAppearance 'main' Visual appearance mode: 'main' or 'results'

SearchbarAppearance Type

export const SearchbarAppearance = {
  Main: 'main',      // Primary search bar appearance
  Results: 'results' // Results filtering appearance
} as const;

export type SearchbarAppearance =
  (typeof SearchbarAppearance)[keyof typeof SearchbarAppearance];

Content Projection Slots

The component uses content projection with specific selectors:

<!-- Prefix icon slot -->
<ng-content select="ui-icon-button[prefix]"></ng-content>

<!-- Input field slot (required) -->
<ng-content select="input[type=text]"></ng-content>

<!-- Actions container -->
<div class="ui-search-bar__actions">
  <!-- Clear button slot -->
  <ng-content select="ui-search-bar-clear"></ng-content>

  <!-- Suffix icon slot -->
  <ng-content select="ui-icon-button"></ng-content>
</div>

Host Classes

  • ui-search-bar - Always applied
  • ui-search-bar__main - When appearance is 'main'
  • ui-search-bar__results - When appearance is 'results'

Implementation Details

  • Uses contentChild.required() to access the projected NgControl
  • Automatically retrieves the input's ElementRef for focus management
  • Computed appearanceClass updates reactively based on input signal

UiSearchBarClearComponent

Standalone component that provides clear/reset functionality for the search input.

Selector: ui-search-bar-clear

Inputs

Input Type Default Description
value unknown undefined Value to set when clearing (defaults to null if not provided)

Features

  • Displays close icon (isaActionClose) from @isa/icons
  • Automatically resets the parent search bar's form control
  • Restores focus to the input field after reset (using asapScheduler)
  • Standalone component with ViewEncapsulation.None

Host Attributes

  • Class: ui-search-bar__action__close
  • Click Handler: Triggers reset() method
  • Data Attribute: data-which="clear-search-icon" for E2E testing

Methods

reset(value?: unknown): void

Resets the search input to the specified value and restores focus.

Parameters:

  • value?: unknown - Optional override value (uses value input if not provided)

Behavior:

  1. Calls inputControl().reset(resetValue) on the parent search bar's form control
  2. Schedules focus restoration using asapScheduler
  3. Focuses the input element via inputControlElementRef()

Usage Examples

import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';
import { IconButtonComponent } from '@isa/ui/buttons';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaActionSearch } from '@isa/icons';

@Component({
  selector: 'app-product-search',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    UiSearchBarComponent,
    UiSearchBarClearComponent,
    IconButtonComponent
  ],
  providers: [provideIcons({ isaActionSearch })],
  template: `
    <ui-search-bar>
      <input
        type="text"
        [formControl]="searchControl"
        placeholder="Search products..."
        data-what="product-search-input"
        data-which="main-search"
      />
      <ui-search-bar-clear></ui-search-bar-clear>
      <ui-icon-button
        name="isaActionSearch"
        (click)="performSearch()"
        data-what="search-button"
      ></ui-icon-button>
    </ui-search-bar>
  `
})
export class ProductSearchComponent {
  searchControl = new FormControl('');

  performSearch(): void {
    const query = this.searchControl.value;
    console.log('Searching for:', query);
  }
}

Results Filter Bar

import { Component, computed, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';
import { IconButtonComponent } from '@isa/ui/buttons';

@Component({
  selector: 'app-results-filter',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    UiSearchBarComponent,
    UiSearchBarClearComponent,
    IconButtonComponent
  ],
  template: `
    <ui-search-bar appearance="results">
      <ui-icon-button prefix name="isaActionSearch"></ui-icon-button>
      <input
        type="text"
        [formControl]="filterControl"
        placeholder="Filter {{ totalResults() }} results..."
        data-what="results-filter-input"
      />
      <ui-search-bar-clear></ui-search-bar-clear>
    </ui-search-bar>

    <div class="results">
      @for (item of filteredResults(); track item.id) {
        <div class="result-item">{{ item.name }}</div>
      }
    </div>
  `
})
export class ResultsFilterComponent {
  filterControl = new FormControl('');
  filterValue = toSignal(this.filterControl.valueChanges, { initialValue: '' });

  allResults = signal([
    { id: 1, name: 'Product A' },
    { id: 2, name: 'Product B' },
    { id: 3, name: 'Product C' }
  ]);

  filteredResults = computed(() => {
    const filter = this.filterValue()?.toLowerCase() || '';
    return this.allResults().filter(item =>
      item.name.toLowerCase().includes(filter)
    );
  });

  totalResults = computed(() => this.allResults().length);
}

With Custom Reset Value

@Component({
  selector: 'app-category-search',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    UiSearchBarComponent,
    UiSearchBarClearComponent,
    IconButtonComponent
  ],
  template: `
    <ui-search-bar>
      <input
        type="text"
        [formControl]="searchControl"
        placeholder="Search in {{ currentCategory }}..."
      />
      <!-- Reset to current category instead of empty string -->
      <ui-search-bar-clear [value]="currentCategory"></ui-search-bar-clear>
      <ui-icon-button name="isaActionSearch"></ui-icon-button>
    </ui-search-bar>
  `
})
export class CategorySearchComponent {
  currentCategory = 'Electronics';
  searchControl = new FormControl(this.currentCategory);
}

Reactive Search with Debouncing

import { Component, inject, OnInit } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';

@Component({
  selector: 'app-live-search',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    UiSearchBarComponent,
    UiSearchBarClearComponent,
    IconButtonComponent
  ],
  template: `
    <ui-search-bar>
      <input
        type="text"
        [formControl]="searchControl"
        placeholder="Type to search..."
      />
      <ui-search-bar-clear></ui-search-bar-clear>
      <ui-icon-button name="isaActionSearch"></ui-icon-button>
    </ui-search-bar>

    @if (isSearching()) {
      <div class="loading">Searching...</div>
    }

    <div class="search-results">
      @for (result of searchResults(); track result.id) {
        <div>{{ result.title }}</div>
      }
    </div>
  `
})
export class LiveSearchComponent implements OnInit {
  #searchService = inject(SearchService);

  searchControl = new FormControl('');
  isSearching = signal(false);
  searchResults = signal<SearchResult[]>([]);

  ngOnInit() {
    this.searchControl.valueChanges
      .pipe(
        debounceTime(300),
        distinctUntilChanged()
      )
      .subscribe(query => {
        if (query) {
          this.performSearch(query);
        } else {
          this.searchResults.set([]);
        }
      });
  }

  async performSearch(query: string): Promise<void> {
    this.isSearching.set(true);
    try {
      const results = await this.#searchService.search(query);
      this.searchResults.set(results);
    } finally {
      this.isSearching.set(false);
    }
  }
}

Advanced Multi-Slot Layout

<ui-search-bar appearance="main">
  <!-- Prefix icon for visual context -->
  <ui-icon-button
    prefix
    name="isaActionSearch"
    color="neutral"
    data-what="search-prefix-icon"
  ></ui-icon-button>

  <!-- Main input field -->
  <input
    type="text"
    [formControl]="searchControl"
    placeholder="Search by name, SKU, or barcode..."
    (keydown.enter)="performSearch()"
    data-what="advanced-search-input"
  />

  <!-- Clear button (only visible when input has value) -->
  @if (searchControl.value) {
    <ui-search-bar-clear data-what="clear-search"></ui-search-bar-clear>
  }

  <!-- Search action button -->
  <ui-icon-button
    name="isaActionSearch"
    color="brand"
    [pending]="isSearching()"
    (click)="performSearch()"
    data-what="search-submit-button"
  ></ui-icon-button>
</ui-search-bar>

Styling and Customization

Host Classes

The component applies dynamic CSS classes based on appearance:

// Main appearance
<ui-search-bar class="ui-search-bar ui-search-bar__main">

// Results appearance
<ui-search-bar class="ui-search-bar ui-search-bar__results">

Custom Styles

Override the default styles using CSS:

// Customize main search bar
.ui-search-bar__main {
  background-color: #ffffff;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  padding: 0.5rem;

  input {
    font-size: 1rem;
    color: #333;
  }
}

// Customize results filter bar
.ui-search-bar__results {
  background-color: #f5f5f5;
  border: 1px solid #d0d0d0;
  border-radius: 4px;

  input {
    font-size: 0.875rem;
    color: #666;
  }
}

// Customize actions container
.ui-search-bar__actions {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

// Customize clear button
.ui-search-bar__action__close {
  cursor: pointer;
  color: #999;
  transition: color 0.2s;

  &:hover {
    color: #333;
  }
}

ViewEncapsulation

Both components use ViewEncapsulation.None, allowing for:

  • Global theme customization
  • Consistent styling across the application
  • Easy CSS variable integration

Architecture Notes

Design Pattern

The library uses a Container + Action Component pattern:

UiSearchBarComponent (Container)
       ↓
Content Projection (Composition)
       ↓
UiSearchBarClearComponent (Action)

Content Child Pattern

The search bar accesses projected content using contentChild:

inputControl = contentChild.required(NgControl, { read: NgControl });
inputControlElementRef = contentChild.required(NgControl, { read: ElementRef });

This provides:

  • Type-safe access to the form control
  • Direct DOM element reference for focus management
  • Compile-time validation that required content is projected

Focus Management

The clear button uses asapScheduler to restore focus asynchronously:

reset(value?: unknown): void {
  const resetValue = value ?? this.value();
  this.searchBar.inputControl().reset(resetValue);

  // Schedule focus after change detection
  asapScheduler.schedule(() => {
    this.searchBar.inputControlElementRef().nativeElement.focus();
  });
}

This ensures:

  • DOM updates complete before focus
  • Smooth user experience
  • No timing race conditions

Parent-Child Communication

UiSearchBarClearComponent injects its parent:

private searchBar = inject(UiSearchBarComponent);

This enables:

  • Direct access to form control
  • Focus restoration on input element
  • Tight coupling between clear button and search bar

Performance Considerations

  1. OnPush Change Detection - Both components use OnPush strategy
  2. Computed Appearance Class - Reactively updates without manual tracking
  3. Signal-Based Inputs - Efficient change propagation
  4. Minimal DOM Operations - Focus management only when needed

Testing

The library uses Jest for testing.

Running Tests

# Run tests for search-bar
npx nx test ui-search-bar --skip-nx-cache

# Run tests with coverage
npx nx test ui-search-bar --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test ui-search-bar --watch

Test Coverage

Tests should cover:

  • Appearance modes - Correct class application for main and results
  • Content projection - Input and icon slots work correctly
  • Clear functionality - Reset and focus restoration
  • Form control integration - NgControl binding and updates
  • Focus management - Input receives focus after clear
  • E2E attributes - data-what and data-which attributes present

Example Test

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Component } from '@angular/core';
import { UiSearchBarComponent, UiSearchBarClearComponent } from '@isa/ui/search-bar';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule, UiSearchBarComponent, UiSearchBarClearComponent],
  template: `
    <ui-search-bar [appearance]="appearance">
      <input type="text" [formControl]="searchControl" />
      <ui-search-bar-clear></ui-search-bar-clear>
    </ui-search-bar>
  `
})
class TestComponent {
  searchControl = new FormControl('test value');
  appearance: SearchbarAppearance = 'main';
}

describe('UiSearchBarComponent', () => {
  let fixture: ComponentFixture<TestComponent>;
  let component: TestComponent;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [TestComponent]
    });
    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should apply main appearance class', () => {
    const element = fixture.nativeElement.querySelector('.ui-search-bar');
    expect(element.classList.contains('ui-search-bar__main')).toBe(true);
  });

  it('should clear input when clear button is clicked', () => {
    const clearButton = fixture.nativeElement.querySelector('.ui-search-bar__action__close');
    clearButton.click();
    fixture.detectChanges();

    expect(component.searchControl.value).toBeNull();
  });

  it('should restore focus after clear', async () => {
    const input = fixture.nativeElement.querySelector('input');
    const clearButton = fixture.nativeElement.querySelector('.ui-search-bar__action__close');

    clearButton.click();
    await fixture.whenStable();

    expect(document.activeElement).toBe(input);
  });
});

Dependencies

Required Libraries

  • @angular/core - Angular framework (v20.1.2)
  • @angular/forms - Angular Forms for NgControl integration
  • @ng-icons/core - Icon component system
  • @isa/icons - ISA icon library
  • rxjs - RxJS for asapScheduler

Optional Dependencies

  • @isa/ui/buttons - For icon button components (recommended)

Path Alias

Import from: @isa/ui/search-bar

Peer Dependencies

  • Angular 20.1.2 or higher
  • TypeScript 5.8.3 or higher

Best Practices

1. Always Provide E2E Attributes

Include data-what and data-which attributes for testing:

<ui-search-bar>
  <input
    type="text"
    [formControl]="searchControl"
    data-what="search-input"
    data-which="product-search"
  />
  <ui-search-bar-clear data-what="clear-button"></ui-search-bar-clear>
</ui-search-bar>

2. Use Reactive Forms

Always use FormControl for proper two-way binding:

// Good: Reactive form control
searchControl = new FormControl('');

// Bad: ngModel or direct value binding
searchValue = '';

3. Debounce Search Operations

For live search, always debounce user input:

ngOnInit() {
  this.searchControl.valueChanges
    .pipe(
      debounceTime(300),
      distinctUntilChanged()
    )
    .subscribe(query => this.search(query));
}

4. Choose Appropriate Appearance

Use 'main' for primary search, 'results' for filtering:

<!-- Primary search interface -->
<ui-search-bar appearance="main">...</ui-search-bar>

<!-- Results filtering -->
<ui-search-bar appearance="results">...</ui-search-bar>

5. Handle Empty States

Provide visual feedback when search returns no results:

<ui-search-bar>
  <input [formControl]="searchControl" />
  <ui-search-bar-clear></ui-search-bar-clear>
</ui-search-bar>

@if (searchControl.value && results().length === 0) {
  <div class="empty-state">No results found</div>
}

License

Internal ISA Frontend library - not for external distribution.