Angular custom filter types
filterTypes(specs) registers a type’s predicate, operators, state keys,
chip labels and filter-tree projection. Use widget to choose the Angular
kit control that edits its values, or return an Angular template or component
from render to supply the field. This keeps the same custom semantics in
the frontend predicate, header filters and Advanced builder.
Here is a case-insensitive choice filter with native Angular renderers.
The definition supplies an explicit getValue. A host checkbox switches
between a template and a component in both the filter form and the compact
header-filter row:
import { Component, input, signal, type TemplateRef, viewChild,} from "@angular/core";import type { ColumnDef, FilterDef, FilterFormSource, FilterTypeSpec, TableLabels,} from "@adapttable/angular";import type { FilterWidgetRenderProps } from "@adapttable/angular/adapter";import { AdaptDataTable } from "@adapttable/angular-unstyled";import { filters, filterTypes } from "@adapttable/angular-unstyled/filters";import { headerFilters } from "@adapttable/angular-unstyled/header-filters";
const caseInsensitiveChoice: FilterTypeSpec = { type: "caseInsensitiveChoice", widget: "select", ops: ["eq"], defaultOp: "eq", stateKeys: (def) => [def.key], match: (def, extra, row) => { const selected = extra[def.key]; if (selected == null || selected === "") return true; return ( String(def.getValue?.(row) ?? "").toLowerCase() === String(selected).toLowerCase() ); }, chips: (def) => ({ [def.key]: (value) => `${def.label ?? def.key}: ${value}`, }), conditionToExtra: (def, condition) => ({ [def.key]: condition.value == null ? undefined : String(condition.value), }),};
interface Person { id: string; team: string;}
@Component({ selector: "app-team-filter", template: ` <label> {{ def().label ?? def().key }} <select [class]="className()" [value]="source().extra[def().key] ?? ''" (change)=" source().setExtra(def().key, $any($event.target).value || undefined) " > <option value="">{{ labels().filterAll }}</option> <option value="core">Core</option> <option value="platform">Platform</option> </select> </label> `,})export class TeamFilter { readonly def = input.required<FilterDef>(); readonly source = input.required<FilterFormSource<unknown>>(); readonly labels = input.required<Required<TableLabels>>(); readonly className = input<string>();}
@Component({ selector: "app-custom-filter", imports: [AdaptDataTable], template: ` <label> <input type="checkbox" [checked]="renderAsComponent()" (change)="renderAsComponent.set($any($event.target).checked)" /> Use the component renderer </label> <ng-template #teamFilter let-props let-source="source" let-labels="labels" let-className="className" > <label> {{ props.def.label ?? props.def.key }} <select [class]="className" [value]="source.extra[props.def.key] ?? ''" (change)=" source.setExtra( props.def.key, $any($event.target).value || undefined ) " > <option value="">{{ labels.filterAll }}</option> <option value="core">Core</option> <option value="platform">Platform</option> </select> </label> </ng-template> <adapt-data-table [data]="rows" [columns]="columns" [rowKey]="rowKey" [features]="features" urlKey="teams" /> `,})export class CustomFilter { readonly renderAsComponent = signal(false); readonly teamFilter = viewChild<TemplateRef<FilterWidgetRenderProps<Person>>>("teamFilter"); readonly rows: Person[] = [ { id: "a", team: "CORE" }, { id: "b", team: "Platform" }, ]; readonly columns: ColumnDef<Person>[] = [{ key: "team", header: "Team" }]; readonly defs: FilterDef<Person>[] = [ { key: "team", type: "caseInsensitiveChoice", label: "Team", getValue: (row) => row.team, options: [ { value: "core", label: "Core" }, { value: "platform", label: "Platform" }, ], }, ]; readonly rowKey = (row: Person) => row.id; readonly features = [ filterTypes([ { ...caseInsensitiveChoice, render: () => this.renderAsComponent() ? TeamFilter : (this.teamFilter() ?? null), }, ]), filters(this.defs), headerFilters(), ];}Both factories also exist at @adapttable/ng-zorro/filters; use its root
AdaptDataTable together with them and its /header-filters feature.
The custom controls above belong to the native kit; an NG-ZORRO custom
renderer should use NG-ZORRO controls. Both example kits are published on npm;
see getting started for installation.
The template query is read inside render, after the feature is created.
While it is unavailable, the callback returns null, a valid empty display
value that falls back to the registered select widget. For only the built-in
kit control, register filterTypes([caseInsensitiveChoice]) without a render
callback.
Keep the whole contract consistent
Section titled “Keep the whole contract consistent”typemust match the definition’s type name;widgetmust name a built-in widget kindstateKeyslists every key the type owns, including operator or range keys, so clearing and persistence address the right valuesmatchshould pass an inactive filter and evaluate active valueschipsdescribes the active state;conditionToExtramaps an Advanced condition into the same statematchevaluatesurlArrayandurlNumberKeysopt into the registry’s list/range-number parsing conventions; they do not introduce an arbitrary serialization format
Register types before the table mounts. The feature host also exposes
registerFilterType and extendFilterType for plugins; registrations follow
the feature lifecycle. A custom server must implement these semantics itself:
JavaScript predicates are not sent to an endpoint or stored in a URL.
Component renderers and live props
Section titled “Component renderers and live props”FilterWidgetRenderProps and renderRegisteredFilter are public exports
from @adapttable/angular. The helper calls the registered renderer with
{ def, source, labels, className }. source is a FilterFormSource: read
current values from source.extra and write through source.setExtra.
The labels are fully resolved. The automatic form passes className as
undefined; the compact header control passes its input class hook.
Templates receive all four named fields and the entire props object as
$implicit, which is the example’s let-props. A component receives only
the matching inputs it declares. The example’s TeamFilter declares all
four as signal inputs. To always use it, replace the callback with
render: () => TeamFilter. Return the component class (Type<unknown>),
not a component instance or a factory that creates one. A renderer uses
filter props, not a column’s CellContext.
The native and NG-ZORRO AdaptAutoFilterForm components and their
AdaptFilterHeaderRow / AdaptFilterHeaderControl surfaces use this same
order:
- A
TemplateRefis stamped with the live filter context - An Angular component type is mounted with its declared matching inputs
- A truthy string or number is displayed as text
- Anything else falls back to the registered built-in
widget
For example, "Choose a team" and 3 display text; undefined, null,
false, true, "", 0, plain objects and non-component functions use
the kit widget. Return "0" if zero should be visible text. This callback
does not accept React elements or JSX. A custom renderer takes the place
of the whole form field, so it supplies its own visible label, accessible
name, focus behavior and kit controls. Use logical spacing for RTL, and
make the control usable in the mobile filter panel as well as a compact
desktop header.
Source, definition, label and class changes update the mounted template or component inputs. Keeping the same component type preserves its instance; changing the type, closing the form or destroying its host tears it down with normal Angular lifecycle handling. Rendering does not change the predicate, chips, URL keys or Advanced tree projection: keep those in the registered spec. See the adapter slot contract for building a complete custom form.
See filtering, filter trees and URL state.
Updating predicate meaning
Section titled “Updating predicate meaning”Replacing declarative filter definitions or registry extensions refreshes the matching rows even when the data array and active filter value stay the same. The table controller supplies the semantic key used by the neutral frontend source, so unrelated feature changes and row updates do not force a predicate reset.
For a custom injectFrontendData predicate whose meaning changes without new
rows or query state, update its filterKey value or signal. Changing a callback
reference alone keeps the existing no-restaging behavior; an explicit key
makes the intended invalidation clear.