External filtering allows custom filtering logic to be mixed with the grid's inbuilt filtering.
This form of filtering is only compatible with the Client-Side Row Model, see Row Models for more details.
import { Component, type OnInit, computed, signal } from "@angular/core";
import { AgGridAngular } from "ag-grid-angular";
import type { ColDef, IDateFilterParams, IRowNode } from "ag-grid-community";
import {
ClientSideRowModelModule,
DateFilterModule,
ExternalFilterModule,
ModuleRegistry,
NumberFilterModule,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
SetFilterModule,
} from "ag-grid-enterprise";
import type { IOlympicData } from "./interfaces";
import "./styles.css";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
ExternalFilterModule,
ClientSideRowModelModule,
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
SetFilterModule,
NumberFilterModule,
DateFilterModule,
]);
@Component({
standalone: true,
imports: [AgGridAngular],
selector: "my-app",
template: `
<div class="test-container">
<div class="test-header">
<label>
<input
type="radio"
name="filter"
id="everyone"
checked
(change)="onAgeTypeChanged('everyone')"
/>
Everyone
</label>
<label>
<input
type="radio"
name="filter"
id="below25"
(change)="onAgeTypeChanged('below25')"
/>
Below 25
</label>
<label>
<input
type="radio"
name="filter"
id="between25and50"
(change)="onAgeTypeChanged('between25and50')"
/>
Between 25 and 50
</label>
<label>
<input
type="radio"
name="filter"
id="above50"
(change)="onAgeTypeChanged('above50')"
/>
Above 50
</label>
<label>
<input
type="radio"
name="filter"
id="dateAfter2008"
(change)="onAgeTypeChanged('dateAfter2008')"
/>
After 01/01/2008
</label>
</div>
<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[rowData]="rowData()"
[isExternalFilterPresent]="isExternalFilterPresent()"
[doesExternalFilterPass]="doesExternalFilterPass()"
/>
</div>
`,
})
export class AppComponent implements OnInit {
public ageType = signal("everyone");
public rowData = signal<IOlympicData[] | null>(null);
public dateFilterParams: IDateFilterParams = {
comparator: (filterLocalDateAtMidnight: Date, cellValue: string) => {
const cellDate = this.asDate(cellValue);
if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) {
return 0;
}
if (cellDate < filterLocalDateAtMidnight) {
return -1;
}
if (cellDate > filterLocalDateAtMidnight) {
return 1;
}
return 0;
},
};
public columnDefs: ColDef<IOlympicData>[] = [
{ field: "athlete", minWidth: 180 },
{ field: "age", filter: "agNumberColumnFilter" },
{ field: "country" },
{ field: "year" },
{
field: "date",
filter: "agDateColumnFilter",
filterParams: this.dateFilterParams,
},
{ field: "total", filter: "agNumberColumnFilter" },
];
public defaultColDef: ColDef = {
flex: 1,
minWidth: 120,
filter: true,
};
// Each computed produces a new function reference whenever ageType changes, and the grid
// re-runs external filtering as soon as it is given one.
public isExternalFilterPresent = computed(() => {
const ageType = this.ageType();
return (): boolean => ageType !== "everyone";
});
public doesExternalFilterPass = computed(() => {
const ageType = this.ageType();
return (node: IRowNode<IOlympicData>): boolean => {
if (node.data) {
switch (ageType) {
case "below25":
return node.data.age < 25;
case "between25and50":
return node.data.age >= 25 && node.data.age <= 50;
case "above50":
return node.data.age > 50;
case "dateAfter2008":
return this.asDate(node.data.date) > new Date(2008, 0, 1);
default:
return true;
}
}
return true;
};
});
public ngOnInit() {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((response) => response.json())
.then((data: IOlympicData[]) => this.rowData.set(data));
}
public onAgeTypeChanged(newValue: string) {
this.ageType.set(newValue);
}
private asDate(dateAsString: string): Date {
const splitFields = dateAsString.split("/");
return new Date(
Number.parseInt(splitFields[2]),
Number.parseInt(splitFields[1]) - 1,
Number.parseInt(splitFields[0]),
);
}
}
.test-container {
height: 100%;
display: flex;
flex-direction: column;
}
.test-header {
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 13px;
margin-bottom: 10px;
display: flex;
justify-content: space-around;
border: 1px solid grey;
padding: 10px;
border-radius: 5px;
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
if (new URLSearchParams(window.location.search).get('prod') !== 'false') {
enableProdMode();
}
const app = bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
});
Implementing External Filtering Copy Link
The example above shows external filters in action. Two methods on gridOptions are required to be implemented: isExternalFilterPresent and doesExternalFilterPass.
Grid calls this method to know if an external filter is present.
Called exactly once every time the grid senses a filter change.
Should return true if external filtering is active, otherwise false.
If true, doesExternalFilterPass is called while filtering, otherwise it is not called.
Supplying a new function reference re-runs external filtering. |
Called once for each row node in the grid.
Should return true if external filter passes, otherwise false.
If false, the node is excluded from the final set.
Only runs if isExternalFilterPresent returns true.
Supplying a new function reference re-runs external filtering. |
Re-running the External Filter Copy Link
The filter state is held outside the grid, so the grid has to be told when that state has changed. Pick one of the following approaches:
- Calling onFilterChanged - the callback references are kept stable and the filter is re-run only when the API is called.
- Supplying New Callbacks - a new callback reference is handed to the grid and the filter is re-run automatically.
Calling onFilterChanged Copy Link
After the filter state has changed call api.onFilterChanged() to ask the grid to run filtering again.
Informs the grid that a filter has changed. This is typically called after a filter change through one of the filter APIs.
source: The source of the filter change event. If not specified defaults to 'api'. |
// Filter state updated now re-run filtering
api.onFilterChanged();Ensure the callbacks have stable references to avoid triggering filtering excessively.
A template binding is re-evaluated on every change-detection run, and .bind(this) returns a new function each time it is evaluated so the grid re-filters on every cycle:
<ag-grid-angular [isExternalFilterPresent]="isExternalFilterPresent.bind(this)" />Prefer an arrow function defined on the component instead as the reference is stable for the lifetime of the component.
Supplying New Callbacks Copy Link
isExternalFilterPresent and doesExternalFilterPass are reactive grid properties, so replacing either one with a new function re-runs filtering automatically.
Where the filter state is held in a signal, each callback is exposed as a computed that returns the filter function, and the invoked signal is bound in the template. A change to the state signal recomputes the callback, so the grid is given a new reference and re-runs filtering:
@Component({
template: `<ag-grid-angular [doesExternalFilterPass]="doesExternalFilterPass()" />`,
})
export class AppComponent {
public minAge = signal(0);
public doesExternalFilterPass = computed(() => {
const minAge = this.minAge();
return (node: IRowNode<IOlympicData>) => node.data!.age > minAge;
});
}The state signal is read outside the returned function so that it is a dependency of the computed. Reading it inside the returned function instead leaves the computed with no dependencies, and the reference never changes.
Reassigning a plain class field hands the grid a new reference in the same way:
// reassigning the field hands the grid a new reference, and filtering re-runs
this.doesExternalFilterPass = (node: IRowNode<IOlympicData>) => node.data!.age > this.minAge;The example on this page takes the second path: ageType is a signal, and both callbacks are computed values that read it.