# @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](#features)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Styling and Customization](#styling-and-customization)
- [Architecture Notes](#architecture-notes)
- [Testing](#testing)
- [Dependencies](#dependencies)

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

```typescript
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

```html
<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

```html
<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

```html
<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

```typescript
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:

```html
<!-- 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

### Basic Search Bar

```typescript
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

```typescript
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

```typescript
@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

```typescript
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

```html
<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:

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

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

```typescript
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:

```typescript
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:

```typescript
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

```bash
# 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

```typescript
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:

```html
<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:

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

```typescript
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:

```html
<!-- 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:

```html
<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.
