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.

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>()Emitted when value is committed
canceloutput<void>()Emitted when edit is cancelled
currentValue()Signal<TValue>Resolved value (prefers control.value over value)
isInvalid()Signal<boolean>Whether control has validation errors
hasErrors()Signal<boolean>Alias for isInvalid()
firstErrorMessage()Signal<string>Human-readable first error message
commitValue(v)MethodEmit commit event + DOM CustomEvent
cancelEdit()MethodEmit cancel event + DOM CustomEvent
isCellFocused()MethodWhether the parent cell has the cell-focus class
import { Component } from '@angular/core';
import { BaseGridEditor } from '@toolbox-web/grid-angular';
@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);
}
}

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.

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

  • 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';
@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.

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:

PropertyTypeDescription
fieldstringColumn field name
columnColumnConfigFull column configuration
uniqueValuesunknown[]Distinct values in the column
excludedValuesSet<unknown>Currently excluded values (set filter)
searchTextstringCurrent search text
applySetFilter(excluded)MethodApply include/exclude filter
applyTextFilter(op, 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';
@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';
@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 {
this.params().applySetFilter(this.excluded());
}
}
import type { GridConfig } from '@toolbox-web/grid-angular';
import '@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,
BaseFilterPanel,
type OverlayPosition,
} from '@toolbox-web/grid-angular';
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