Unlocking Advanced Filtering with Custom Filter Components in AG-Grid React
Welcome to the 14th installment of our AG-Grid React series! In previous parts, we've explored various powerful features of AG-Grid, from basic setup to complex data operations. Today, we're diving into one of the most flexible and potent customization points: Custom Filter Components.
While AG-Grid provides a robust set of built-in filters (text, number, date, set filters), there are often scenarios where your application demands unique filtering logic or a highly specialized user interface. This is precisely where custom filter components shine, allowing you to tailor the filtering experience to your exact needs.
Why Custom Filters? Extending AG-Grid's Capabilities
The standard AG-Grid filters are excellent for common use cases, but they can be limiting when you encounter requirements such as:
- Unique Business Logic: Filtering based on complex calculations involving multiple columns or external data sources.
- Custom UI/UX: Implementing a highly specific design for your filter, perhaps with sliders, multi-select dropdowns from a specific dataset, or interactive graphs.
- Combined Filtering: A single filter component that applies logic across several fields or uses a combination of input types (e.g., text search plus a date range in one component).
- External State Management: Integrating with global application state or a Redux store for filter values.
By creating a custom filter component, you gain complete control over both the filter's appearance and its underlying logic.
The Anatomy of an AG-Grid Custom Filter Component
An AG-Grid custom filter component, whether built with React or plain JavaScript, must implement a specific interface that AG-Grid can interact with. For React, we'll leverage the IRichFilterComp interface and React's component lifecycle.
The core methods that AG-Grid will call on your filter component include:
init(params): Called once to initialize the filter. Provides parameters like the column, grid API, and a callback for when the filter changes.getGui(): Returns the DOM element that represents your filter's UI.isFilterActive(): Tells the grid if the filter is currently active (i.e., should it be applied to rows).doesFilterPass(params): The crucial method that contains your custom filtering logic. It determines if a given row should pass the filter.getModel(): Returns the current state of your filter. Useful for saving/restoring filter state.setModel(model): Sets the state of your filter, typically from a saved model.afterGuiAttached(params): Called after the filter GUI is attached to the DOM. Useful for focusing input fields.onNewRowsLoaded(): Called when new rows are loaded into the grid, allowing the filter to react to data changes.destroy(): Called when the filter component is no longer needed, for cleanup.
Building a React Custom Filter Component: A Step-by-Step Guide
Let's walk through creating a simple custom text filter that only shows rows where the specified column's value starts with the text entered by the user. This will demonstrate the essential concepts.
Step 1: Create the Custom Filter React Component
We'll create a functional React component called CustomStartsWithFilter.jsx. To make it compatible with AG-Grid's API, we'll use forwardRef and useImperativeHandle to expose the necessary AG-Grid methods.
CustomStartsWithFilter.jsx:
import React, { useState, forwardRef, useImperativeHandle, useRef, useEffect } from 'react';
const CustomStartsWithFilter = forwardRef((props, ref) => {
const [filterText, setFilterText] = useState('');
const inputRef = useRef(null);
// Expose AG-Grid filter API methods via useImperativeHandle
useImperativeHandle(ref, () => {
return {
// AG-Grid will call this to check if the filter is active
isFilterActive() {
return filterText !== null && filterText !== undefined && filterText !== '';
},
// AG-Grid will call this for each row to see if it passes the filter
doesFilterPass(params) {
const value = props.valueGetter(params.node);
// Convert to string and to lower case for case-insensitive comparison
const valueStr = String(value || '').toLowerCase();
const filterTextLower = filterText.toLowerCase();
return valueStr.startsWith(filterTextLower);
},
// AG-Grid will call this to get the current filter model
getModel() {
if (!filterText) {
return null;
}
return { value: filterText };
},
// AG-Grid will call this to set the filter model
setModel(model) {
setFilterText(model ? model.value : '');
},
// AG-Grid will call this after the GUI is attached to the DOM
afterGuiAttached(params) {
if (inputRef.current) {
inputRef.current.focus();
}
},
// AG-Grid can optionally call this when new rows are loaded (not needed for this simple filter)
onNewRowsLoaded() {
// For example, you might want to reset the filter or react to new data
}
};
});
// Handle input change and notify AG-Grid
const onFilterTextChanged = (event) => {
const newValue = event.target.value;
setFilterText(newValue);
// Important: Notify AG-Grid that the filter state has changed
props.filterChangedCallback();
};
// UseEffect to focus input when filter appears (alternative to afterGuiAttached)
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []); // Run once on mount
return (
<div style={{ padding: '8px', width: '200px' }}>
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>Starts With Filter</div>
<input
ref={inputRef}
type="text"
value={filterText}
onChange={onFilterTextChanged}
placeholder="Filter by..."
style={{ width: '100%', padding: '4px' }}
/>
{filterText && (
<button
onClick={() => {
setFilterText('');
props.filterChangedCallback();
}}
style={{ marginTop: '5px', padding: '5px 10px', cursor: 'pointer' }}
>
Clear Filter
</button>
)}
</div>
);
});
export default CustomStartsWithFilter;
Explanation of Key Parts:
useState(filterText): Manages the internal state of our input field.useRef(inputRef): Provides a reference to the input element for focusing.forwardRef&useImperativeHandle: This is how our React component exposes the methods AG-Grid expects from a filter component. Without this, AG-Grid wouldn't know how to interact with our functional React component.props.valueGetter(params.node): This helper function (provided by AG-Grid ininitparams) safely retrieves the value for the current row and column, which is essential for ourdoesFilterPasslogic.props.filterChangedCallback(): Crucial! Whenever the state of your filter changes (e.g., the user types something), you must call this method. It signals to AG-Grid that the filter should be re-applied to the dataset.getModel()andsetModel(): These allow AG-Grid to save and restore the state of your filter, for instance, when saving grid state or navigating away and back.
Step 2: Integrate the Custom Filter into Your AG-Grid
Now, let's integrate this custom filter into your main application component where AgGridReact is used.
App.js:
import React, { useState, useRef, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
// Import your custom filter component
import CustomStartsWithFilter from './CustomStartsWithFilter';
const App = () => {
const gridRef = useRef();
const [rowData] = useState([
{ make: 'Toyota', model: 'Celica', price: 35000 },
{ make: 'Ford', model: 'Mondeo', price: 32000 },
{ make: 'Porsche', model: 'Boxster', price: 72000 },
{ make: 'BMW', model: 'M5', price: 60000 },
{ make: 'Audi', model: 'A4', price: 40000 }
]);
const [columnDefs] = useState([
{
field: 'make',
filter: true, // Default text filter
sortable: true
},
{
field: 'model',
filter: 'customStartsWithFilter', // Use our custom filter by its key
sortable: true
},
{
field: 'price',
filter: 'agNumberColumnFilter', // Built-in number filter
sortable: true
}
]);
// Define frameworkComponents to register your custom filter
// The key here ('customStartsWithFilter') is what you'll use in columnDefs
const frameworkComponents = {
customStartsWithFilter: CustomStartsWithFilter,
};
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
frameworkComponents={frameworkComponents} // Register custom components here
defaultColDef={{
flex: 1,
minWidth: 100,
// Enable filtering by default for all columns, but columnDefs can override
floatingFilter: true, // Show filter inputs below headers
}}
/>
</div>
);
};
export default App;
Integration Details:
frameworkComponents: This prop onAgGridReactis where you register all your custom React components (filters, cell renderers, cell editors, etc.). You provide a key (e.g.,'customStartsWithFilter') and the corresponding React component.columnDefs: In the column definition for the 'model' field, we setfilter: 'customStartsWithFilter'. This tells AG-Grid to use our registered custom component for that column's filter.defaultColDef.floatingFilter: true: This makes the filter UI appear directly below the column header, which is a common and user-friendly pattern for filters.
Understanding Key Methods in Detail
isFilterActive()
This method is called by AG-Grid to quickly determine if the filter has any criteria applied. It's crucial for performance and for AG-Grid's internal state management (e.g., showing a filter icon on the column header). If it returns false, AG-Grid knows it doesn't need to call doesFilterPass for that filter.
// In CustomStartsWithFilter.jsx
isFilterActive() {
return filterText !== null && filterText !== undefined && filterText !== '';
}
doesFilterPass(params)
This is the heart of your custom filter's logic. AG-Grid calls this method for every row in the dataset (when the filter is active) to determine if that row should be displayed. The params object contains:
node: The row node being evaluated.data: The raw data for the row.valueGetter(node): A convenient function to get the value for the filter's associated column from the row node.
// In CustomStartsWithFilter.jsx
doesFilterPass(params) {
const value = props.valueGetter(params.node);
const valueStr = String(value || '').toLowerCase(); // Handle null/undefined gracefully
const filterTextLower = filterText.toLowerCase();
return valueStr.startsWith(filterTextLower);
}
getModel() and setModel(model)
These methods are vital for managing the filter's state. getModel() is called when AG-Grid needs to save the current filter configuration (e.g., when the grid state is persisted). setModel(model) is called to restore a previously saved filter state. This enables features like:
- Saving grid state to local storage or a backend.
- Applying filters programmatically.
- Resetting filters to a default state.
// In CustomStartsWithFilter.jsx
getModel() {
if (!filterText) {
return null; // Return null if filter is inactive to save space
}
return { value: filterText };
}
setModel(model) {
// When model is null or undefined, clear the filter
setFilterText(model ? model.value : '');
}
Best Practices and Considerations
- Performance: For very large datasets, ensure your
doesFilterPasslogic is as efficient as possible. Avoid heavy computations if not strictly necessary. - User Experience: Provide clear feedback to the user. Consider adding a "Clear" button, visual indicators of an active filter, and ensuring the filter input is focused when opened.
- Accessibility: Ensure your custom filter UI is accessible. Use proper ARIA attributes, keyboard navigation, and clear labels.
- AG-Grid API: Your
props.api(passed ininitparams) gives you access to the full AG-Grid API, allowing you to interact with other grid features programmatically. - Error Handling: Gracefully handle cases where
valueGettermight return unexpected types (e.g.,null,undefined) or when external data is not in the expected format. - State Management: For complex filters, consider using React's
useReduceror even a global state management solution if the filter state needs to be shared widely.
Conclusion
Custom filter components in AG-Grid with React provide an unparalleled level of flexibility, allowing you to create highly specific and visually rich filtering experiences. By understanding the core interface methods and leveraging React's powerful component model, you can overcome the limitations of built-in filters and craft an AG-Grid instance that perfectly matches your application's unique requirements. This capability empowers you to deliver sophisticated data interaction features that truly enhance user productivity and satisfaction.