Back to all examples

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