Skip to content

In this tutorial, you will build an external filter panel with a category dropdown and a price range slider that controls Handsontable filtering. You will learn how to apply multiple conditions at once through the Filters plugin API and clear them all with a single button.

TypeScript
/* file: app.component.ts */
import { Component, ViewChild } from '@angular/core';
import { GridSettings, HotTableComponent, HotTableModule } from '@handsontable/angular-wrapper';
import { RowObject } from 'handsontable/common';
type Product = {
name: string;
category: string;
price: number;
stock: number;
};
const sourceData: Product[] = [
{ name: 'Trail Bike', category: 'Bikes', price: 1499, stock: 12 },
{ name: 'Road Helmet', category: 'Safety', price: 89, stock: 42 },
{ name: 'Flat Pedals', category: 'Components', price: 59, stock: 80 },
{ name: 'Hydration Pack', category: 'Accessories', price: 129, stock: 23 },
{ name: 'Brake Pads', category: 'Components', price: 25, stock: 150 },
{ name: 'Cycling Glasses', category: 'Accessories', price: 79, stock: 33 },
{ name: 'Chain Lube', category: 'Maintenance', price: 16, stock: 99 },
{ name: 'Torque Wrench', category: 'Maintenance', price: 139, stock: 14 },
{ name: 'Kids Helmet', category: 'Safety', price: 54, stock: 20 },
{ name: 'Gravel Bike', category: 'Bikes', price: 2199, stock: 7 },
];
@Component({
standalone: true,
imports: [HotTableModule],
selector: 'example1-multi-column-filter-panel',
template: `
<div class="example-controls-container">
<div class="filter-panel">
<label class="filter-label filter-label--wide">
Product name
<input type="text" [value]="enteredName" placeholder="Contains..." (input)="onNameFilter($event)" />
</label>
<label class="filter-label filter-label--wide">
Category
<select [value]="selectedCategory" (change)="onCategoryFilter($event)">
<option value="">All categories</option>
<option value="Bikes">Bikes</option>
<option value="Safety">Safety</option>
<option value="Components">Components</option>
<option value="Accessories">Accessories</option>
<option value="Maintenance">Maintenance</option>
</select>
</label>
<label class="filter-label">
Min price
<input type="number" min="0" [value]="minPrice" placeholder="0" (input)="onMinPriceFilter($event)" />
</label>
<label class="filter-label">
Max price
<input type="number" min="0" [value]="maxPrice" placeholder="2500" (input)="onMaxPriceFilter($event)" />
</label>
<button type="button" (click)="clearFilters()">Clear all filters</button>
</div>
</div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
`,
})
export class AppComponent {
@ViewChild(HotTableComponent, { static: false }) readonly hotTable!: HotTableComponent;
readonly data = sourceData;
readonly gridSettings: GridSettings = {
columns: [
{ data: 'name', type: 'text', title: 'Product' },
{ data: 'category', type: 'text', title: 'Category' },
{ data: 'price', type: 'numeric', title: 'Price' },
{ data: 'stock', type: 'numeric', title: 'Stock' },
],
colHeaders: ['Product', 'Category', 'Price', 'Stock'],
rowHeaders: true,
filters: true,
dropdownMenu: false,
width: '100%',
height: 320,
autoWrapRow: true,
autoWrapCol: true,
};
enteredName = '';
selectedCategory = '';
minPrice = '';
maxPrice = '';
onNameFilter(event: Event): void {
this.enteredName = (event.target as HTMLInputElement).value.trim();
this.applyFilters();
}
onCategoryFilter(event: Event): void {
this.selectedCategory = (event.target as HTMLSelectElement).value;
this.applyFilters();
}
onMinPriceFilter(event: Event): void {
this.minPrice = (event.target as HTMLInputElement).value.trim();
this.applyFilters();
}
onMaxPriceFilter(event: Event): void {
this.maxPrice = (event.target as HTMLInputElement).value.trim();
this.applyFilters();
}
clearFilters(): void {
this.enteredName = '';
this.selectedCategory = '';
this.minPrice = '';
this.maxPrice = '';
const hot = this.hotTable.hotInstance;
if (!hot) {
return;
}
const filtersPlugin = hot.getPlugin('filters');
filtersPlugin.clearConditions();
filtersPlugin.filter();
hot.render();
}
private applyFilters(): void {
const hot = this.hotTable.hotInstance;
if (!hot) {
return;
}
const filtersPlugin = hot.getPlugin('filters');
filtersPlugin.clearConditions();
if (this.enteredName) {
filtersPlugin.addCondition(0, 'contains', [this.enteredName]);
}
if (this.selectedCategory) {
filtersPlugin.addCondition(1, 'eq', [this.selectedCategory]);
}
if (this.minPrice && this.maxPrice) {
const lowerBound = Number(this.minPrice);
const upperBound = Number(this.maxPrice);
if (Number.isFinite(lowerBound) && Number.isFinite(upperBound)) {
filtersPlugin.addCondition(2, 'between', [lowerBound, upperBound]);
}
} else if (this.minPrice) {
const lowerBound = Number(this.minPrice);
if (Number.isFinite(lowerBound)) {
filtersPlugin.addCondition(2, 'gte', [lowerBound]);
}
} else if (this.maxPrice) {
const upperBound = Number(this.maxPrice);
if (Number.isFinite(upperBound)) {
filtersPlugin.addCondition(2, 'lte', [upperBound]);
}
}
filtersPlugin.filter();
hot.render();
}
}
/* end-file */
/* file: app.config.ts */
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
{
provide: HOT_GLOBAL_CONFIG,
useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig,
},
],
};
/* end-file */
HTML
<div><example1-multi-column-filter-panel></example1-multi-column-filter-panel></div>

Overview

This recipe shows how to control the Filters plugin from a filter panel outside of the grid. The panel includes controls aligned with the grid columns, and it applies all active filters together with AND logic.

Difficulty: Intermediate Time: ~20 minutes Libraries: Handsontable only

What You’ll Build

An external filter panel that:

  • Enables filters: true while hiding the built-in filter menu UI.
  • Uses hot.getPlugin('filters') to control filtering through API calls.
  • Applies a text filter with addCondition(columnIndex, 'contains', [value]).
  • Applies a numeric range filter with addCondition(columnIndex, 'between', [min, max]).
  • Clears and re-applies all active conditions every time controls change.
  • Includes a Clear all filters button that restores all rows.

Step 1 - Enable filtering and create the grid

Enable the Filters plugin in Handsontable configuration:

const hot = new Handsontable(container, {
data: productData,
colHeaders: ['Product', 'Category', 'Price', 'Stock'],
columns: [
{ data: 'name' },
{ data: 'category' },
{ data: 'price', type: 'numeric', numericFormat: { pattern: '$0,0.00', culture: 'en-US' } },
{ data: 'stock', type: 'numeric' },
],
filters: true,
dropdownMenu: false,
licenseKey: 'non-commercial-and-evaluation',
});

Setting dropdownMenu: false keeps the filter panel external, while the Filters plugin remains active.

Step 2 - Get the plugin and apply conditions

Get the plugin instance and apply conditions every time a filter value changes:

const filtersPlugin = hot.getPlugin('filters');
const applyFilters = () => {
filtersPlugin.clearConditions();
if (enteredName) {
filtersPlugin.addCondition(0, 'contains', [enteredName]);
}
if (selectedCategory) {
filtersPlugin.addCondition(1, 'eq', [selectedCategory]);
}
if (minPrice && maxPrice) {
filtersPlugin.addCondition(2, 'between', [Number(minPrice), Number(maxPrice)]);
} else if (minPrice) {
filtersPlugin.addCondition(2, 'gte', [Number(minPrice)]);
} else if (maxPrice) {
filtersPlugin.addCondition(2, 'lte', [Number(maxPrice)]);
}
filtersPlugin.filter();
hot.render();
};

This pattern guarantees each update uses the current set of active controls.

Step 3 - Add clear-all behavior

Add a button that clears control values, removes all conditions, and shows all rows again:

clearAllButton.addEventListener('click', () => {
nameInput.value = '';
categorySelect.value = '';
minPriceInput.value = '';
maxPriceInput.value = '';
filtersPlugin.clearConditions();
filtersPlugin.filter();
hot.render();
});

How it works

  1. User changes one or more controls in the external panel.
  2. The code clears previously applied conditions.
  3. The code re-applies current conditions for product name, category, and price.
  4. filtersPlugin.filter() updates the visible rows.
  5. Clear all filters resets controls and restores the full dataset.

The full implementation is available in the runnable example above.

What you learned

  • How to use filtersPlugin.clearConditions() and filtersPlugin.addCondition() to apply fresh conditions on every control change.
  • How to call filtersPlugin.filter() to update the visible rows after adding conditions, and how hot.render() keeps the view in sync.
  • How to build a clear-all button that resets external controls, removes all conditions, and restores the full dataset.
  • Why you must enable the Filters plugin with filters: true and pair it with dropdownMenu: true to expose per-column filter UI alongside your external panel.

Next steps

  • Explore external search box to add a text search that works alongside the filter panel.
  • Read the Filters plugin API reference for the full list of built-in condition types (between, contains, begins with, and more).