- 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
20 KiB
@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
- Quick Start
- API Reference
- Usage Examples
- Styling and Customization
- Architecture Notes
- Testing
- Dependencies
Features
- Two appearance modes - Main and results views with distinct styling
- Angular Forms integration - Works with
FormControlandNgControlvia 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 appliedui-search-bar__main- Whenappearanceis'main'ui-search-bar__results- Whenappearanceis'results'
Implementation Details
- Uses
contentChild.required()to access the projectedNgControl - Automatically retrieves the input's
ElementReffor focus management - Computed
appearanceClassupdates 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 (usesvalueinput if not provided)
Behavior:
- Calls
inputControl().reset(resetValue)on the parent search bar's form control - Schedules focus restoration using
asapScheduler - Focuses the input element via
inputControlElementRef()
Usage Examples
Basic Search Bar
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
- OnPush Change Detection - Both components use OnPush strategy
- Computed Appearance Class - Reactively updates without manual tracking
- Signal-Based Inputs - Efficient change propagation
- 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-whatanddata-whichattributes 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 libraryrxjs- RxJS forasapScheduler
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.