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]);<GoabFormItem id="filterChipInput" error={inputError} mb="m">
<GoabBlock gap="xs" direction="row" alignment="start" width="100%">
<div style={{ flex: 1 }}>
<GoabInput
name="filterChipInput"
aria-labelledby="filterChipInput"
value={inputValue}
leadingIcon="search"
width="100%"
onChange={handleInputChange}
onKeyPress={handleInputKeyPress}
/>
</div>
<GoabButton type="secondary" onClick={applyFilter} leadingIcon="filter">
Filter
</GoabButton>
</GoabBlock>
</GoabFormItem>
{typedChips.length > 0 && (
<div>
<GoabText tag="span" color="secondary" mb="xs" mr="xs">
Filter:
</GoabText>
{typedChips.map((typedChip, index) => (
<GoabFilterChip
key={index}
content={typedChip}
mb="xs"
mr="xs"
onClick={() => removeTypedChip(typedChip)}
/>
))}
<GoabButton
type="tertiary"
size="compact"
mb="xs"
onClick={() => setTypedChips([])}
>
Clear all
</GoabButton>
</div>
)}
<GoabTable width="100%">
<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>
<GoabBadge
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>
</GoabTable>
{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()),
);
}<goab-form-item id="filterChipInput" [error]="inputError" mb="m">
<goab-block gap="xs" direction="row" alignment="start" width="100%">
<div style="flex: 1">
<goab-input
name="filterChipInput"
aria-labelledby="filterChipInput"
[value]="inputValue"
leadingIcon="search"
width="100%"
(onChange)="handleInputChange($event)"
(onKeyPress)="handleInputKeyPress($event)"
>
</goab-input>
</div>
<goab-button type="secondary" (onClick)="applyFilter()" leadingIcon="filter">
Filter
</goab-button>
</goab-block>
</goab-form-item>
@if (typedChips.length > 0) {
<ng-container>
<goab-text tag="span" color="secondary" mb="xs" mr="xs"> Filter: </goab-text>
@for (typedChip of typedChips; track typedChip; let index = $index) {
<goab-filter-chip
[content]="typedChip"
mb="xs"
mr="xs"
(onClick)="removeTypedChip(typedChip)"
>
</goab-filter-chip>
}
<goab-button type="tertiary" size="compact" mb="xs" (onClick)="removeAllTypedChips()">
Clear all
</goab-button>
</ng-container>
}
<goab-table width="100%">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th class="goa-table-number-header">ID Number</th>
</tr>
</thead>
<tbody>
@for (item of dataFiltered; track $index) {
<tr>
<td>
<goab-badge
[type]="item.status.type"
[content]="item.status.text"
[icon]="false"
></goab-badge>
</td>
<td>{{ item.name }}</td>
<td class="goa-table-number-column">{{ item.id }}</td>
</tr>
}
</tbody>
</goab-table>
@if (dataFiltered.length === 0 && data.length > 0) {
<goab-block mt="l" mb="l"> 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 as="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