Base Classes for Editors & 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
Section titled “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
Section titled “BaseGridEditor”The foundation class for all Angular cell editors. Provides common inputs, outputs, validation state, and automatic cleanup.
Full API:
BaseGridEditor
The class exposes four inputs (value, row, column, control), two outputs (commit,
cancel), and a set of signals derived from the bound FormControl — currentValue(),
isInvalid(), isDirty(), isTouched(), hasErrors(), firstErrorMessage() and
allErrorMessages(). Every signal that mirrors control state returns false (or an empty
value) when no control is bound, so an editor works unchanged outside a FormArray.
Commit through commitValue(v) and abort through cancelEdit() — both emit the Angular output
and the DOM CustomEvent the grid listens for. Override getErrorMessage(key, value?) to
customise validation text, and the onBeforeEditClose() / onEditClose() /
onExternalValueChange(v) hooks to flush pending values, tear down overlays, or react to
cascading edits.
See BaseGridEditor for every member with its
exact signature.
Example
Section titled “Example”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
Section titled “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, orcontrol.touchedchanges — which Angular triggers on every value update, blur, or programmatic call tomarkAsTouched()/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
Section titled “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
Additional API (on top of BaseGridEditor)
Section titled “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 the last commitBoth() value, then the grid value, then the CVA value |
commitBoth(v) | Method | Commits via both CVA onChange and grid commitValue |
writeValue / registerOn* / setDisabledState | CVA methods | Full ControlValueAccessor implementation |
Example
Section titled “Example”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> {}Using in both contexts
Section titled “Using in both contexts”// 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
Section titled “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·OverlayPosition
Features
Section titled “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
| 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
Section titled “Example”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
Section titled “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
Section titled “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
Section titled “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·FilterPanelParams
| 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
Section titled “FilterPanelParams”The params input carries the field being filtered, its column config, the available values
(uniqueValues, excludedValues, searchText), the active currentFilter to restore UI state
from, and the callbacks that write back: applySetFilter(excludedValues, valueTo?) — note it
takes an array, not a Set — applyTextFilter(operator, value, valueTo?), clearFilter()
and closePanel().
See FilterPanelParams for the full
typed contract.
Example: Text Filter
Section titled “Example: Text Filter”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)
Section titled “Example: Set Filter (Checkbox List)”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
Section titled “Usage in Column Config”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
Section titled “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
Section titled “Imports”All base classes are exported from the main package:
import { BaseGridEditor, BaseGridEditorCVA, BaseOverlayEditor, type OverlayPosition } from '@toolbox-web/grid-angular/features/editing';import { BaseFilterPanel } from '@toolbox-web/grid-angular/features/filtering';