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

MemberTypeDescription
valueinput<TValue>()Cell value (fallback when no FormControl)
rowinput<TRow>()Row data object
columninput<ColumnConfig>()Column configuration
controlinput<AbstractControl>()Cell’s FormControl (when using FormArray)
commitoutput<TValue | null>()Emitted when value is committed (null clears the field)
canceloutput<void>()Emitted when edit is cancelled
currentValue()Signal<TValue | undefined>Resolved value (prefers control.value over value)
isInvalid()Signal<boolean>Mirrors control.invalidfalse if no control is bound
isDirty()Signal<boolean>Mirrors control.dirtyfalse if no control is bound
isTouched()Signal<boolean>Mirrors control.touchedfalse 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)MethodEmit commit event + DOM CustomEvent (accepts TValue | null)
cancelEdit()MethodEmit cancel event + DOM CustomEvent
isCellFocused()MethodWhether 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)
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 grid value, falls back to 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 provides these members (see FilterPanelParams for the full type):

PropertyTypeDescription
fieldstringColumn field name
columnColumnConfigFull column configuration
uniqueValuesunknown[]Distinct values in the column
excludedValuesSet<unknown>Currently excluded values (set filter)
searchTextstringCurrent search text
currentFilter?FilterModelCurrently active filter for this column, if any
applySetFilter(excludedValues, valueTo?)MethodApply include/exclude filter (pass an array of excluded values)
applyTextFilter(operator, value, valueTo?)MethodApply text/number filter
clearFilter()MethodClear column filter
closePanel()MethodClose filter panel
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';
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt