Skip to content

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.

BaseGridEditor ← common inputs/outputs, validation helpers
├── BaseGridEditorCVA ← + ControlValueAccessor (dual grid/form use)
└── BaseOverlayEditor ← + floating overlay panel infrastructure
ClassPurposeExtend when…
BaseGridEditorInline cell editorsYou need a simple input that renders inside the cell
BaseGridEditorCVADual grid + form editorsThe same component is used inside <tbw-grid> and standalone <form>
BaseOverlayEditorOverlay/popup editorsYou need a floating panel (date picker, autocomplete, dropdown)
BaseFilterPanelColumn filter panelsYou need a custom filter UI for the FilteringPlugin

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 FormControlcurrentValue(), 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.

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);
}
}

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 pipelinefirstErrorMessage() 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.

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

MemberTypeDescription
cvaValueSignal<TValue | null>Value written by the form control
disabledStateSignal<boolean>Tracks setDisabledState() calls
displayValueComputed<TValue | null>Prefers the last commitBoth() value, then the grid value, then the CVA value
commitBoth(v)MethodCommits via both CVA onChange and grid commitValue
writeValue / registerOn* / setDisabledStateCVA methodsFull ControlValueAccessor implementation
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> {}
// 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" />

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

  • 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
MemberTypeDescription
overlayPositionOverlayPositionPosition relative to cell: 'below' (default), 'above', 'below-right', 'over-top-left', 'over-bottom-left'
initOverlay(panel)MethodInitialize with the panel element. Call in an effect() or afterNextRender().
showOverlay()MethodShow the overlay panel
hideOverlay(suppressTab?)MethodHide the overlay panel
reopenOverlay()MethodClose and re-open (repositions after content change)
teardownOverlay()MethodRemove panel from DOM + cleanup (auto-called on destroy)
onInlineKeydown(e)MethodKeydown handler for the inline input
onInlineClick()MethodClick handler for the inline input (toggle overlay)
handleEscape(e)MethodClose overlay or cancel edit
advanceGridFocus(backward?)MethodDispatch synthetic Tab to move to next cell
getInlineInput()AbstractReturn the inline <input> element (for focus return)
onOverlayOutsideClick()AbstractCalled on click outside — typically call hideOverlay()
onOverlayOpened()HookCalled after overlay opens (focus inner element, etc.)
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();
}
}
ValueDescription
'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.

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

VariableDefaultDescription
--tbw-overlay-bg#fffPanel background
--tbw-overlay-border#cccPanel border color
--tbw-overlay-radius4pxPanel border radius
--tbw-overlay-shadow0 4px 12px rgba(0,0,0,0.15)Panel box shadow

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

MemberTypeDescription
paramsinput.required<FilterPanelParams>()Injected by the grid’s filtering infrastructure
applyFilter()AbstractImplement your filter logic here
applyAndClose()MethodCalls applyFilter() then closePanel()
clearAndClose()MethodCalls clearFilter() then closePanel()

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 SetapplyTextFilter(operator, value, valueTo?), clearFilter() and closePanel().

See FilterPanelParams for the full typed contract.

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);
}
}
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()]);
}
}
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 },
};

ScenarioBase Class
Simple text/number input in cellBaseGridEditor
Date picker input used in both grid and formsBaseGridEditorCVA
Dropdown/autocomplete with floating panelBaseOverlayEditor
Custom column filter UIBaseFilterPanel
Date picker overlay + form controlCompose: extend BaseOverlayEditor and implement ControlValueAccessor manually

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';