# Base Classes for Editors & Filter Panels

> BaseGridEditor, BaseGridEditorCVA, BaseOverlayEditor, and BaseFilterPanel — reusable base classes for custom Angular editors and filter panels.

The `@toolbox-web/grid-angular` package provides base classes that eliminate boilerplate when building custom editors and filter panels. Each class handles common infrastructure — lifecycle, cleanup, positioning, form integration — so you can focus on your component's UI and logic.

## Class Hierarchy

```
BaseGridEditor          ← common inputs/outputs, validation helpers
  ├── BaseGridEditorCVA  ← + ControlValueAccessor (dual grid/form use)
  └── BaseOverlayEditor  ← + floating overlay panel infrastructure
```

| Class | Purpose | Extend when… |
|-------|---------|--------------|
| `BaseGridEditor` | Inline cell editors | You need a simple input that renders inside the cell |
| `BaseGridEditorCVA` | Dual grid + form editors | The same component is used inside `<tbw-grid>` **and** standalone `<form>` |
| `BaseOverlayEditor` | Overlay/popup editors | You need a floating panel (date picker, autocomplete, dropdown) |
| `BaseFilterPanel` | Column filter panels | You need a custom filter UI for the FilteringPlugin |

---

## BaseGridEditor

The foundation class for all Angular cell editors. Provides common inputs, outputs, validation state, and automatic cleanup.

> **Full API:** [`BaseGridEditor`](/grid/angular/api/utilities/basegrideditor.md)

### API

| Member | Type | Description |
|--------|------|-------------|
| `value` | `input<TValue>()` | Cell value (fallback when no `FormControl`) |
| `row` | `input<TRow>()` | Row data object |
| `column` | `input<`[`ColumnConfig`](/grid/api/core/interfaces/columnconfig.md)`>()` | Column configuration |
| `control` | `input<AbstractControl>()` | Cell's `FormControl` (when using `FormArray`) |
| `commit` | `output<TValue \| null>()` | Emitted when value is committed (`null` clears the field) |
| `cancel` | `output<void>()` | Emitted when edit is cancelled |
| `currentValue()` | `Signal<TValue \| undefined>` | Resolved value (prefers `control.value` over `value`) |
| `isInvalid()` | `Signal<boolean>` | Mirrors `control.invalid` — `false` if no `control` is bound |
| `isDirty()` | `Signal<boolean>` | Mirrors `control.dirty` — `false` if no `control` is bound |
| `isTouched()` | `Signal<boolean>` | Mirrors `control.touched` — `false` if no `control` is bound |
| `hasErrors()` | `Signal<boolean>` | True when `control.errors` has any keys — `false` if no `control` is bound |
| `firstErrorMessage()` | `Signal<string>` | First validation error from `control.errors`, run through `getErrorMessage()` |
| `allErrorMessages()` | `Signal<string[]>` | Every validation error from `control.errors`, each run through `getErrorMessage()` |
| `commitValue(v)` | Method | Emit `commit` event + DOM `CustomEvent` (accepts `TValue \| null`) |
| `cancelEdit()` | Method | Emit `cancel` event + DOM `CustomEvent` |
| `isCellFocused()` | Method | Whether the parent cell has the `cell-focus` class |
| `getErrorMessage(key, value?)` | Method (override) | Customise validation error messages |
| `onBeforeEditClose()` | Hook (override) | Called before grid clears editing state — last chance to flush pending values |
| `onEditClose()` | Hook (override) | Called after the editing session ends — cleanup overlays/dropdowns |
| `onExternalValueChange(v)` | Hook (override) | Called when the cell value changes externally (cascade, undo/redo) |

### Example

```typescript
import { Component } from '@angular/core';
import { BaseGridEditor } from '@toolbox-web/grid-angular/features/editing';

@Component({
  selector: 'app-text-editor',
  template: `
    <input
      [value]="currentValue()"
      [class.is-invalid]="isInvalid()"
      (input)="commitValue($event.target.value)"
      (keydown.escape)="cancelEdit()"
    />
    @if (hasErrors()) {
      <div class="error">{{ firstErrorMessage() }}</div>
    }
  `
})
export class TextEditorComponent extends BaseGridEditor<MyRow, string> {
  protected override getErrorMessage(errorKey: string): string {
    if (errorKey === 'required') return 'This field is required';
    return super.getErrorMessage(errorKey);
  }
}
```

### Validation State & Reactivity

The validation signals — `isInvalid`, `isDirty`, `isTouched`, `hasErrors`,
`firstErrorMessage`, `allErrorMessages` — are **derived from the `control` input**.
They only carry meaning when the grid is bound to a `FormArray` and an
`AbstractControl` has been passed in via `[control]="control"`. Without a control,
they default to `false` / `''` / `[]`.

Because they are Angular `computed()` signals:

- They **don't run on a schedule** — they re-evaluate lazily, only when read.
- They re-fire automatically whenever `control.errors`, `control.invalid`,
  `control.dirty`, or `control.touched` changes — which Angular triggers on every
  value update, blur, or programmatic call to `markAsTouched()` /
  `updateValueAndValidity()`.
- If your template never reads them, they never compute.

**Error pipeline** — `firstErrorMessage()` and `allErrorMessages()` walk the
`control.errors` map and pipe each `(key, value)` pair through your
`getErrorMessage()` override. The first-vs-all distinction:

- **`firstErrorMessage()`** — the most common case; show one inline message
  (`{{ firstErrorMessage() }}` under the input).
- **`allErrorMessages()`** — when a single field can have several simultaneous
  errors you want to surface together (password complexity, multi-rule
  validators) rendered as a list.

---

## BaseGridEditorCVA

Combines `BaseGridEditor` with Angular's `ControlValueAccessor` interface. Use this when a single component needs to work both as a grid cell editor **and** as a standalone form control.

> **Full API:** [`BaseGridEditorCVA`](/grid/angular/api/utilities/basegrideditorcva.md)

### Additional API (on top of BaseGridEditor)

| Member | Type | Description |
|--------|------|-------------|
| `cvaValue` | `Signal<TValue \| null>` | Value written by the form control |
| `disabledState` | `Signal<boolean>` | Tracks `setDisabledState()` calls |
| `displayValue` | `Computed<TValue \| null>` | Prefers grid value, falls back to CVA value |
| `commitBoth(v)` | Method | Commits via both CVA `onChange` **and** grid `commitValue` |
| `writeValue` / `registerOn*` / `setDisabledState` | CVA methods | Full `ControlValueAccessor` implementation |

### Example

```typescript
import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { BaseGridEditorCVA } from '@toolbox-web/grid-angular/features/editing';

@Component({
  selector: 'app-date-picker',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => DatePickerComponent),
    multi: true,
  }],
  template: `
    <input
      type="date"
      [value]="displayValue()"
      [disabled]="disabledState()"
      (change)="commitBoth($event.target.value)"
      (keydown.escape)="cancelEdit()"
    />
  `
})
export class DatePickerComponent extends BaseGridEditorCVA<MyRow, string> {}
```

:::note
Subclasses must still provide `NG_VALUE_ACCESSOR` themselves because
`forwardRef(() => ConcreteClass)` must reference the concrete component — this
is an Angular limitation.
:::

### Using in both contexts

```typescript
// Inside a grid
<tbw-grid-column field="startDate" editable>
  <app-date-picker *tbwEditor="let value" [value]="value" />
</tbw-grid-column>

// Inside a standalone form
<app-date-picker formControlName="startDate" />
```

---

## BaseOverlayEditor

Base class for editors that display a **floating overlay panel** (date pickers, autocomplete dropdowns, color pickers, etc.). Handles all the positioning, focus gating, click-outside detection, and cleanup infrastructure.

> **Full API:** [`BaseOverlayEditor`](/grid/angular/api/utilities/baseoverlayeditor.md) · [`OverlayPosition`](/grid/angular/api/types/overlayposition.md)

### Features

- **CSS Anchor Positioning** with JS `getBoundingClientRect()` fallback for Firefox/Safari
- **Focus gating** — in row editing mode, only the focused cell's overlay is shown
- **Click-outside detection** — configurable via `onOverlayOutsideClick()`
- **Keyboard routing** — Enter/Space/ArrowDown/F2 open, Escape closes
- **Automatic teardown** — panel removed from `<body>` + all listeners cleaned up on destroy

### API

| Member | Type | Description |
|--------|------|-------------|
| `overlayPosition` | `OverlayPosition` | Position relative to cell: `'below'` (default), `'above'`, `'below-right'`, `'over-top-left'`, `'over-bottom-left'` |
| `initOverlay(panel)` | Method | Initialize with the panel element. Call in an `effect()` or `afterNextRender()`. |
| `showOverlay()` | Method | Show the overlay panel |
| `hideOverlay(suppressTab?)` | Method | Hide the overlay panel |
| `reopenOverlay()` | Method | Close and re-open (repositions after content change) |
| `teardownOverlay()` | Method | Remove panel from DOM + cleanup (auto-called on destroy) |
| `onInlineKeydown(e)` | Method | Keydown handler for the inline input |
| `onInlineClick()` | Method | Click handler for the inline input (toggle overlay) |
| `handleEscape(e)` | Method | Close overlay or cancel edit |
| `advanceGridFocus(backward?)` | Method | Dispatch synthetic Tab to move to next cell |
| `getInlineInput()` | **Abstract** | Return the inline `<input>` element (for focus return) |
| `onOverlayOutsideClick()` | **Abstract** | Called on click outside — typically call `hideOverlay()` |
| `onOverlayOpened()` | Hook | Called after overlay opens (focus inner element, etc.) |

### Example

```typescript
import { Component, viewChild, ElementRef, effect } from '@angular/core';
import { BaseOverlayEditor } from '@toolbox-web/grid-angular/features/editing';

@Component({
  selector: 'app-date-editor',
  template: `
    <input
      #inlineInput
      readonly
      [value]="currentValue()"
      (click)="onInlineClick()"
      (keydown)="onInlineKeydown($event)"
    />
    <div #panel class="tbw-overlay-panel" style="width: 280px; padding: 12px;">
      <input
        type="date"
        [value]="currentValue()"
        (change)="selectAndClose($event.target.value)"
      />
      <div class="actions">
        <button (click)="hideOverlay()">Cancel</button>
      </div>
    </div>
  `
})
export class DateEditorComponent extends BaseOverlayEditor<MyRow, string> {
  panelRef = viewChild.required<ElementRef<HTMLElement>>('panel');
  inputRef = viewChild.required<ElementRef<HTMLInputElement>>('inlineInput');

  protected override overlayPosition = 'below' as const;

  constructor() {
    super();
    effect(() => {
      const panel = this.panelRef().nativeElement;
      this.initOverlay(panel);
      if (this.isCellFocused()) this.showOverlay();
    });
  }

  protected getInlineInput(): HTMLInputElement | null {
    return this.inputRef()?.nativeElement ?? null;
  }

  protected onOverlayOutsideClick(): void {
    this.hideOverlay();
  }

  selectAndClose(date: string): void {
    this.commitValue(date);
    this.hideOverlay();
  }
}
```

### Overlay Position Options

| Value | Description |
|-------|-------------|
| `'below'` | Panel below the cell, left-aligned (default). Flips above if viewport overflows. |
| `'above'` | Panel above the cell, left-aligned. Flips below if off-screen. |
| `'below-right'` | Panel below the cell, right-aligned. Flips above if viewport overflows. |
| `'over-top-left'` | Panel top-left aligns with cell top-left (opens downward). No flip. |
| `'over-bottom-left'` | Panel bottom-left aligns with cell bottom-left (opens upward). No flip. |

### CSS Customization

The overlay panel uses CSS custom properties from the grid theme:

| Variable | Default | Description |
|----------|---------|-------------|
| `--tbw-overlay-bg` | `#fff` | Panel background |
| `--tbw-overlay-border` | `#ccc` | Panel border color |
| `--tbw-overlay-radius` | `4px` | Panel border radius |
| `--tbw-overlay-shadow` | `0 4px 12px rgba(0,0,0,0.15)` | Panel box shadow |

---

## BaseFilterPanel

Base class for custom column filter panels used with the `FilteringPlugin`. Provides the `params` input and lifecycle helpers so you only need to implement your filter logic.

> **Full API:** [`BaseFilterPanel`](/grid/angular/api/utilities/basefilterpanel.md) · [`FilterPanelParams`](/grid/plugins/filtering/interfaces/filterpanelparams.md)

### API

| Member | Type | Description |
|--------|------|-------------|
| `params` | `input.required<FilterPanelParams>()` | Injected by the grid's filtering infrastructure |
| `applyFilter()` | **Abstract** | Implement your filter logic here |
| `applyAndClose()` | Method | Calls `applyFilter()` then `closePanel()` |
| `clearAndClose()` | Method | Calls `clearFilter()` then `closePanel()` |

### FilterPanelParams

The `params` input provides these members (see [`FilterPanelParams`](/grid/plugins/filtering/interfaces/filterpanelparams.md) for the full type):

| Property | Type | Description |
|----------|------|-------------|
| `field` | `string` | Column field name |
| `column` | [`ColumnConfig`](/grid/api/core/interfaces/columnconfig.md) | Full column configuration |
| `uniqueValues` | `unknown[]` | Distinct values in the column |
| `excludedValues` | `Set<unknown>` | Currently excluded values (set filter) |
| `searchText` | `string` | Current search text |
| `currentFilter?` | `FilterModel` | Currently active filter for this column, if any |
| `applySetFilter(excludedValues, valueTo?)` | Method | Apply include/exclude filter (pass an **array** of excluded values) |
| `applyTextFilter(operator, value, valueTo?)` | Method | Apply text/number filter |
| `clearFilter()` | Method | Clear column filter |
| `closePanel()` | Method | Close filter panel |

### Example: Text Filter

```typescript
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
import { BaseFilterPanel } from '@toolbox-web/grid-angular/features/filtering';

@Component({
  selector: 'app-text-filter',
  template: `
    <div class="filter-panel">
      <input
        #input
        placeholder="Search..."
        (keydown.enter)="applyAndClose()"
      />
      <div class="actions">
        <button (click)="applyAndClose()">Apply</button>
        <button (click)="clearAndClose()">Clear</button>
      </div>
    </div>
  `
})
export class TextFilterComponent extends BaseFilterPanel {
  input = viewChild.required<ElementRef<HTMLInputElement>>('input');

  constructor() {
    super();
    afterNextRender(() => {
      this.input().nativeElement.focus();
    });
  }

  applyFilter(): void {
    this.params().applyTextFilter('contains', this.input().nativeElement.value);
  }
}
```

### Example: Set Filter (Checkbox List)

```typescript
import { Component, signal } from '@angular/core';
import { BaseFilterPanel } from '@toolbox-web/grid-angular/features/filtering';

@Component({
  selector: 'app-set-filter',
  template: `
    <div class="filter-panel">
      @for (val of params().uniqueValues; track val) {
        <label>
          <input
            type="checkbox"
            [checked]="!excluded().has(val)"
            (change)="toggle(val)"
          />
          {{ val }}
        </label>
      }
      <div class="actions">
        <button (click)="applyAndClose()">Apply</button>
        <button (click)="clearAndClose()">Clear</button>
      </div>
    </div>
  `
})
export class SetFilterComponent extends BaseFilterPanel {
  excluded = signal(new Set<unknown>());

  toggle(value: unknown): void {
    this.excluded.update(set => {
      const next = new Set(set);
      next.has(value) ? next.delete(value) : next.add(value);
      return next;
    });
  }

  applyFilter(): void {
    // applySetFilter takes an array, not a Set
    this.params().applySetFilter([...this.excluded()]);
  }
}
```

### Usage in Column Config

```typescript
import type { GridConfig } from '@toolbox-web/grid-angular';
import { GridFilteringDirective } from '@toolbox-web/grid-angular/features/filtering';
import { TextFilterComponent } from './text-filter.component';
import { SetFilterComponent } from './set-filter.component';

const gridConfig: GridConfig = {
  columns: [
    { field: 'name', filterable: true, filterPanel: TextFilterComponent },
    { field: 'status', filterable: true, filterPanel: SetFilterComponent },
  ],
  features: { filtering: true },
};
```

---

## When to Use Which Class

| Scenario | Base Class |
|----------|-----------|
| Simple text/number input in cell | `BaseGridEditor` |
| Date picker input used in both grid and forms | `BaseGridEditorCVA` |
| Dropdown/autocomplete with floating panel | `BaseOverlayEditor` |
| Custom column filter UI | `BaseFilterPanel` |
| Date picker overlay + form control | Compose: extend `BaseOverlayEditor` and implement `ControlValueAccessor` manually |

## Imports

All base classes are exported from the main package:

```typescript
import { BaseGridEditor, BaseGridEditorCVA, BaseOverlayEditor, type OverlayPosition } from '@toolbox-web/grid-angular/features/editing';
import { BaseFilterPanel } from '@toolbox-web/grid-angular/features/filtering';

```
