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
| Member | Type | Description |
|---|---|---|
value | input<TValue>() | Cell value (fallback when no FormControl) |
row | input<TRow>() | Row data object |
column | input<ColumnConfig>() | 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
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 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
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 provides these members (see FilterPanelParams for the full type):
| Property | Type | Description |
|---|---|---|
field | string | Column field name |
column | ColumnConfig | 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
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';