Filter data in a table
const [typedChips, setTypedChips] = useState<string[]>([]);
const [inputValue, setInputValue] = useState("");
const [inputError, setInputError] = useState("");
const errorEmpty = "Empty filter";
const errorDuplicate = "Enter a unique filter";
const data = useMemo(
() => [
{
status: { type: "information" as GoabBadgeType, text: "In progress" },
name: "Ivan Schmidt",
id: "7838576954",
},
{
status: { type: "success" as GoabBadgeType, text: "Completed" },
name: "Luz Lakin",
id: "8576953364",
},
{
status: { type: "information" as GoabBadgeType, text: "In progress" },
name: "Keith McGlynn",
id: "9846041345",
},
{
status: { type: "success" as GoabBadgeType, text: "Completed" },
name: "Melody Frami",
id: "7385256175",
},
{
status: { type: "important" as GoabBadgeType, text: "Updated" },
name: "Frederick Skiles",
id: "5807570418",
},
{
status: { type: "success" as GoabBadgeType, text: "Completed" },
name: "Dana Pfannerstill",
id: "5736306857",
},
],
[]
);
const [dataFiltered, setDataFiltered] = useState(data);
const handleInputChange = (detail: GoabInputOnChangeDetail) => {
const newValue = detail.value.trim();
setInputValue(newValue);
};
const handleInputKeyPress = (detail: GoabInputOnKeyPressDetail) => {
if (detail.key === "Enter") {
applyFilter();
}
};
const applyFilter = () => {
if (inputValue === "") {
setInputError(errorEmpty);
return;
}
if (typedChips.length > 0 && typedChips.includes(inputValue)) {
setInputError(errorDuplicate);
return;
}
setTypedChips([...typedChips, inputValue]);
setTimeout(() => {
setInputValue("");
}, 0);
setInputError("");
};
const removeTypedChip = (chip: string) => {
setTypedChips(typedChips.filter(c => c !== chip));
setInputError("");
};
const checkNested = useCallback((obj: object, chip: string): boolean => {
return Object.values(obj).some(value =>
typeof value === "object" && value !== null
? checkNested(value, chip)
: typeof value === "string" && value.toLowerCase().includes(chip.toLowerCase())
);
}, []);
const getFilteredData = useCallback(
(typedChips: string[]) => {
if (typedChips.length === 0) {
return data;
}
return data.filter((item: object) =>
typedChips.every(chip => checkNested(item, chip))
);
},
[checkNested, data]
);
useEffect(() => {
setDataFiltered(getFilteredData(typedChips));
}, [getFilteredData, typedChips]);<GoabxFormItem id="filterChipInput" error={inputError} mb="m">
<GoabBlock gap="xs" direction="row" alignment="start" width="100%">
<div style={{ flex: 1 }}>
<GoabxInput
name="filterChipInput"
aria-labelledby="filterChipInput"
value={inputValue}
leadingIcon="search"
width="100%"
onChange={handleInputChange}
onKeyPress={handleInputKeyPress}
/>
</div>
<GoabxButton type="secondary" onClick={applyFilter} leadingIcon="filter">
Filter
</GoabxButton>
</GoabBlock>
</GoabxFormItem>
{typedChips.length > 0 && (
<div>
<GoabText tag="span" color="secondary" mb="xs" mr="xs">
Filter:
</GoabText>
{typedChips.map((typedChip, index) => (
<GoabxFilterChip
key={index}
content={typedChip}
mb="xs"
mr="xs"
onClick={() => removeTypedChip(typedChip)}
/>
))}
<GoabxButton type="tertiary" size="compact" mb="xs" onClick={() => setTypedChips([])}>
Clear all
</GoabxButton>
</div>
)}
<GoabxTable width="full">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th className="goa-table-number-header">ID Number</th>
</tr>
</thead>
<tbody>
{dataFiltered.map(item => (
<tr key={item.id}>
<td>
<GoabxBadge type={item.status.type} content={item.status.text} icon={false} />
</td>
<td>{item.name}</td>
<td className="goa-table-number-column">{item.id}</td>
</tr>
))}
</tbody>
</GoabxTable>
{dataFiltered.length === 0 && data.length > 0 && (
<GoabBlock mt="l" mb="l">
No results found
</GoabBlock>
)}typedChips: string[] = [];
inputValue = "";
inputError = "";
readonly errorEmpty = "Empty filter";
readonly errorDuplicate = "Enter a unique filter";
readonly data: DataItem[] = [
{
status: { type: "information", text: "In progress" },
name: "Ivan Schmidt",
id: "7838576954",
},
{
status: { type: "success", text: "Completed" },
name: "Luz Lakin",
id: "8576953364",
},
{
status: { type: "information", text: "In progress" },
name: "Keith McGlynn",
id: "9846041345",
},
{
status: { type: "success", text: "Completed" },
name: "Melody Frami",
id: "7385256175",
},
{
status: { type: "important", text: "Updated" },
name: "Frederick Skiles",
id: "5807570418",
},
{
status: { type: "success", text: "Completed" },
name: "Dana Pfannerstill",
id: "5736306857",
},
];
dataFiltered = this.getFilteredData(this.typedChips);
handleInputChange(detail: GoabInputOnChangeDetail): void {
const newValue = detail.value.trim();
this.inputValue = newValue;
}
handleInputKeyPress(detail: GoabInputOnKeyPressDetail): void {
if (detail.key === "Enter") {
this.applyFilter();
}
}
applyFilter(): void {
if (this.inputValue === "") {
this.inputError = this.errorEmpty;
return;
}
if (this.typedChips.includes(this.inputValue)) {
this.inputError = this.errorDuplicate;
return;
}
this.typedChips = [...this.typedChips, this.inputValue];
this.inputValue = "";
this.inputError = "";
this.dataFiltered = this.getFilteredData(this.typedChips);
}
removeTypedChip(chip: string): void {
this.typedChips = this.typedChips.filter(c => c !== chip);
this.dataFiltered = this.getFilteredData(this.typedChips);
this.inputError = "";
}
removeAllTypedChips(): void {
this.typedChips = [];
this.dataFiltered = this.getFilteredData(this.typedChips);
this.inputError = "";
}
getFilteredData(typedChips: string[]): DataItem[] {
if (typedChips.length === 0) {
return this.data;
}
return this.data.filter(item =>
typedChips.every(chip => this.checkNested(item, chip))
);
}
checkNested(obj: object, chip: string): boolean {
return Object.values(obj).some(value =>
typeof value === "object" && value !== null
? this.checkNested(value, chip)
: typeof value === "string" && value.toLowerCase().includes(chip.toLowerCase())
);
}<goabx-form-item id="filterChipInput" [error]="inputError" mb="m">
<goab-block gap="xs" direction="row" alignment="start" width="100%">
<div style="flex: 1;">
<goabx-input
name="filterChipInput"
aria-labelledby="filterChipInput"
[value]="inputValue"
leadingIcon="search"
width="100%"
(onChange)="handleInputChange($event)"
(onKeyPress)="handleInputKeyPress($event)">
</goabx-input>
</div>
<goabx-button type="secondary" (onClick)="applyFilter()" leadingIcon="filter">
Filter
</goabx-button>
</goab-block>
</goabx-form-item>
<ng-container *ngIf="typedChips.length > 0">
<goab-text tag="span" color="secondary" mb="xs" mr="xs">
Filter:
</goab-text>
<goabx-filter-chip
*ngFor="let typedChip of typedChips; let index = index"
[content]="typedChip"
mb="xs"
mr="xs"
(onClick)="removeTypedChip(typedChip)">
</goabx-filter-chip>
<goabx-button type="tertiary" size="compact" mb="xs" (onClick)="removeAllTypedChips()">
Clear all
</goabx-button>
</ng-container>
<goabx-table width="full">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th class="goa-table-number-header">ID Number</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of dataFiltered">
<td>
<goabx-badge [type]="item.status.type" [content]="item.status.text" [icon]="false"></goabx-badge>
</td>
<td>{{ item.name }}</td>
<td class="goa-table-number-column">{{ item.id }}</td>
</tr>
</tbody>
</goabx-table>
<goab-block mt="l" mb="l" *ngIf="dataFiltered.length === 0 && data.length > 0">
No results found
</goab-block>const filterInput = document.getElementById('filter-input');
const filterBtn = document.getElementById('filter-btn');
const filterFormItem = document.getElementById('filter-form-item');
const chipsContainer = document.getElementById('chips-container');
const chipsList = document.getElementById('chips-list');
const clearAllBtn = document.getElementById('clear-all-btn');
const tableRows = document.querySelectorAll('tbody tr');
let typedChips = [];
function filterTable() {
tableRows.forEach(row => {
const badge = row.querySelector('goa-badge');
const badgeText = badge ? badge.getAttribute('content') || '' : '';
const text = (row.textContent + ' ' + badgeText).toLowerCase();
const matches = typedChips.length === 0 || typedChips.every(chip => text.includes(chip.toLowerCase()));
row.style.display = matches ? '' : 'none';
});
}
function renderChips() {
chipsList.innerHTML = '';
typedChips.forEach(chip => {
const filterChip = document.createElement('goa-filter-chip');
filterChip.setAttribute('version', '2');
filterChip.setAttribute('content', chip);
filterChip.setAttribute('mb', 'xs');
filterChip.setAttribute('mr', 'xs');
filterChip.addEventListener('_click', () => removeChip(chip));
chipsList.appendChild(filterChip);
});
chipsContainer.style.display = typedChips.length > 0 ? 'block' : 'none';
filterTable();
}
function applyFilter() {
const value = filterInput.value.trim();
if (value === '') {
filterFormItem.setAttribute('error', 'Empty filter');
return;
}
if (typedChips.includes(value)) {
filterFormItem.setAttribute('error', 'Enter a unique filter');
return;
}
typedChips.push(value);
filterInput.value = '';
filterFormItem.removeAttribute('error');
renderChips();
}
function removeChip(chip) {
typedChips = typedChips.filter(c => c !== chip);
renderChips();
}
filterBtn.addEventListener('_click', applyFilter);
clearAllBtn.addEventListener('_click', () => {
typedChips = [];
renderChips();
});<goa-form-item version="2" id="filter-form-item" mb="m">
<goa-block gap="xs" direction="row" alignment="center" width="100%">
<div style="flex: 1;">
<goa-input version="2"
id="filter-input"
name="filterChipInput"
leadingicon="search"
width="100%">
</goa-input>
</div>
<goa-button version="2" id="filter-btn" type="secondary" leadingicon="filter">
Filter
</goa-button>
</goa-block>
</goa-form-item>
<div id="chips-container" style="display: none;">
<goa-text tag="span" color="secondary" mb="xs" mr="xs">Filter:</goa-text>
<span id="chips-list"></span>
<goa-button version="2" id="clear-all-btn" type="tertiary" size="compact" mb="xs">
Clear all
</goa-button>
</div>
<goa-table version="2" width="100%" mt="s">
<table style="width: 100%;">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th class="goa-table-number-header">ID Number</th>
</tr>
</thead>
<tbody>
<tr>
<td><goa-badge version="2" type="information" content="In progress" icon="false"></goa-badge></td>
<td>Ivan Schmidt</td>
<td class="goa-table-number-column">7838576954</td>
</tr>
<tr>
<td><goa-badge version="2" type="success" content="Completed" icon="false"></goa-badge></td>
<td>Luz Lakin</td>
<td class="goa-table-number-column">8576953364</td>
</tr>
<tr>
<td><goa-badge version="2" type="information" content="In progress" icon="false"></goa-badge></td>
<td>Keith McGlynn</td>
<td class="goa-table-number-column">9846041345</td>
</tr>
<tr>
<td><goa-badge version="2" type="success" content="Completed" icon="false"></goa-badge></td>
<td>Melody Frami</td>
<td class="goa-table-number-column">7385256175</td>
</tr>
<tr>
<td><goa-badge version="2" type="important" content="Updated" icon="false"></goa-badge></td>
<td>Frederick Skiles</td>
<td class="goa-table-number-column">5807570418</td>
</tr>
<tr>
<td><goa-badge version="2" type="success" content="Completed" icon="false"></goa-badge></td>
<td>Dana Pfannerstill</td>
<td class="goa-table-number-column">5736306857</td>
</tr>
</tbody>
</table>
</goa-table>Enable users to filter table data using search input and filter chips.
When to use
Use this pattern when:
- Users need to narrow down large datasets
- Multiple filters can be applied simultaneously
- Filters should be visible and easily removable
- You want to provide real-time filtering feedback
Considerations
- Validate filter input to prevent empty or duplicate filters
- Show applied filters as removable chips for visibility
- Provide a “Clear all” option when multiple filters are applied
- Display a “No results found” message when filters return empty results
- Use case-insensitive matching for better user experience