Skip to content

Filter Selection

The Advanced Filtering Plugin for RevoGrid extends the grid’s capabilities by introducing powerful filtering options, making data management more efficient and flexible. This plugin includes new filter types, such as slider and selection, which provide intuitive ways for users to interact with and refine data views.

Selection filter options are rendered in a virtualized nested revo-grid, so large custom option lists stay fast while keeping the same search, select-all-visible, checkbox toggling, sorting, and filter semantics.

The live example also enables the optional expression editor. Open a selection filter popup and click Expression to type selection expressions such as in ("Apple 🍎", "Orange 🍊") or not in ("Banana 🍌", "Lemon 🍋").

Source code
TypeScript ts
// src/components/filter-selection/FilterAdvanced.ts

import { defineCustomElements } from '@revolist/revogrid/loader';
import { currentTheme } from '../composables/useRandomData';
import {
  createFilterSelectionColumns,
  createFilterSelectionFilter,
  createFilterSelectionRows,
  filterSelectionColumnTypes,
  filterSelectionPlugins,
} from './filter-selection.shared';
import './filter-selection.css';

defineCustomElements();

const { isDark } = currentTheme();

export function load(parentSelector: string) {
  const grid = document.createElement('revo-grid');

  grid.columns = createFilterSelectionColumns();
  grid.columnTypes = filterSelectionColumnTypes;
  grid.plugins = filterSelectionPlugins;
  grid.stretch = 'last';
  grid.filter = createFilterSelectionFilter();
  grid.theme = isDark() ? 'darkCompact' : 'compact';
  grid.className = 'filter-selection-grid cell-border';
  grid.hideAttribution = true;
  document.querySelector(parentSelector)?.appendChild(grid);
  grid.source = createFilterSelectionRows();

  return () => grid.remove();
}
Vue vue
// src/components/filter-selection/FilterAdvanced.vue

<template>
  <RevoGrid
    :theme="isDark ? 'darkCompact' : 'compact'"
    :columns="columns"
    :source="rows"
    :plugins="plugins"
    :column-types="columnTypes"
    class="filter-selection-grid grow h-full cell-border"
    stretch="last"
    :filter="filter"
    hide-attribution
  />
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { currentThemeVue } from '../composables/useRandomData';
import {
  createFilterSelectionColumns,
  createFilterSelectionFilter,
  createFilterSelectionRows,
  filterSelectionColumnTypes,
  filterSelectionPlugins,
} from './filter-selection.shared';
import './filter-selection.css';

const { isDark } = currentThemeVue();
const columns = ref(createFilterSelectionColumns());
const rows = ref(createFilterSelectionRows());
const filter = computed(() => createFilterSelectionFilter());
const plugins = filterSelectionPlugins;
const columnTypes = filterSelectionColumnTypes;
</script>
React tsx
// src/components/filter-selection/FilterAdvanced.tsx

import { useMemo } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { currentTheme } from '../composables/useRandomData';
import {
  createFilterSelectionColumns,
  createFilterSelectionFilter,
  createFilterSelectionRows,
  filterSelectionColumnTypes,
  filterSelectionPlugins,
} from './filter-selection.shared';
import './filter-selection.css';

function FilterAdvanced() {
  const { isDark } = currentTheme();
  const source = useMemo(() => createFilterSelectionRows(), []);
  const columns = useMemo(() => createFilterSelectionColumns(), []);
  const filter = useMemo(() => createFilterSelectionFilter(), []);
  const plugins = useMemo(() => filterSelectionPlugins, []);
  const columnTypes = useMemo(() => filterSelectionColumnTypes, []);

  return (
    <RevoGrid
      columns={columns}
      source={source}
      columnTypes={columnTypes}
      className="filter-selection-grid grow h-full cell-border"
      hide-attribution
      filter={filter}
      stretch="last"
      theme={isDark() ? 'darkCompact' : 'compact'}
      plugins={plugins}
    />
  );
}

export default FilterAdvanced;
Angular ts
// src/components/filter-selection/FilterAdvancedAngular.ts

import { Component, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { currentTheme } from '../composables/useRandomData';
import {
  createFilterSelectionColumns,
  createFilterSelectionFilter,
  createFilterSelectionRows,
  filterSelectionColumnTypes,
  filterSelectionPlugins,
} from './filter-selection.shared';
import './filter-selection.css';

@Component({
  selector: 'filter-selection-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <revo-grid
      [columns]="columns"
      [source]="source"
      [columnTypes]="columnTypes"
      stretch="last"
      [filter]="filter"
      [hideAttribution]="true"
      [theme]="theme"
      [plugins]="plugins"
      class="filter-selection-grid grow h-full cell-border"
    ></revo-grid>`,
  encapsulation: ViewEncapsulation.None,
})
export class FilterSelectionGridComponent {
  theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
  source = createFilterSelectionRows();
  columns = createFilterSelectionColumns();
  columnTypes = filterSelectionColumnTypes;
  filter = createFilterSelectionFilter();
  plugins = filterSelectionPlugins;
}
Shared setup ts
import type { ColumnFilterConfig, ColumnRegular, ColumnTypes } from '@revolist/revogrid';
import NumberColumnType from '@revolist/revogrid-column-numeral';
import {
  AdvanceFilterPlugin,
  ColumnDropdown,
  ColumnStretchPlugin,
  RowOddPlugin,
  columnTypeRenderer,
} from '@revolist/revogrid-pro';

type FilterSelectionFruit = {
  name: string;
  emoji: string;
  family: string;
  color: string;
  availability: AvailabilityValue;
};

type AvailabilityValue = 'In stock' | 'Backorder' | 'Seasonal';

export type FilterSelectionRow = {
  id: number;
  name: string;
  fruitFamily: string;
  fruitColor: string;
  availability: AvailabilityValue;
  price: number;
};

export const filterSelectionPlugins = [
  AdvanceFilterPlugin,
  ColumnStretchPlugin,
  RowOddPlugin,
];

export const filterSelectionColumnTypes: ColumnTypes = {
  currency: new NumberColumnType('$0,0.00'),
  dropdown: ColumnDropdown,
};

const FRUITS: FilterSelectionFruit[] = [
  { name: 'Apple', emoji: '🍎', family: 'Tree fruit', color: 'Red', availability: 'In stock' },
  { name: 'Pear', emoji: '🍐', family: 'Tree fruit', color: 'Green', availability: 'In stock' },
  { name: 'Peach', emoji: '🍑', family: 'Stone fruit', color: 'Orange', availability: 'Seasonal' },
  { name: 'Cherry', emoji: '🍒', family: 'Stone fruit', color: 'Red', availability: 'Seasonal' },
  { name: 'Orange', emoji: '🍊', family: 'Citrus', color: 'Orange', availability: 'In stock' },
  { name: 'Lemon', emoji: '🍋', family: 'Citrus', color: 'Yellow', availability: 'Backorder' },
  { name: 'Mango', emoji: '🥭', family: 'Tropical', color: 'Orange', availability: 'Backorder' },
  { name: 'Banana', emoji: '🍌', family: 'Tropical', color: 'Yellow', availability: 'In stock' },
  { name: 'Strawberry', emoji: '🍓', family: 'Berries', color: 'Red', availability: 'Seasonal' },
  { name: 'Grapes', emoji: '🍇', family: 'Berries', color: 'Purple', availability: 'Backorder' },
];

const AVAILABILITY_OPTIONS = [
  { value: 'In stock', label: 'In stock', tone: 'stock' },
  { value: 'Backorder', label: 'Backorder', tone: 'backorder' },
  { value: 'Seasonal', label: 'Seasonal', tone: 'seasonal' },
];

function normalizeFruitValue(fruit: FilterSelectionFruit) {
  return `${fruit.name} ${fruit.emoji}`.toLowerCase();
}

function availabilityTone(value: unknown) {
  const option = AVAILABILITY_OPTIONS.find((item) => item.value === value || item.label === value);
  return option?.tone ?? 'stock';
}

export function renderAvailabilityBadge(h: any, value: unknown) {
  const label = String(value || 'In stock');
  return h(
    'span',
    {
      class: `filter-selection-availability-badge filter-selection-availability-badge--${availabilityTone(label)}`,
    },
    label,
  );
}

function renderFruitLabel(h: any, label: string) {
  const [name, emoji = ''] = label.split(' ');
  return h('span', { class: 'filter-selection-fruit-option' }, [
    h('span', { class: 'filter-selection-fruit-option__emoji' }, emoji),
    h('span', { class: 'filter-selection-fruit-option__label' }, name),
  ]);
}

export function createFilterSelectionRows(count = 80): FilterSelectionRow[] {
  return Array.from({ length: count }, (_, index) => {
    const fruit = FRUITS[index % FRUITS.length];
    return {
      id: index + 1,
      name: `${fruit.name} ${fruit.emoji}`,
      fruitFamily: fruit.family,
      fruitColor: fruit.color,
      availability: fruit.availability,
      price: 5 + ((index * 7) % 24),
    };
  });
}

export function createFilterSelectionColumns(): ColumnRegular[] {
  return [
    {
      name: 'Fruit',
      prop: 'name',
      size: 220,
      filter: ['string', 'selection'],
      columnType: 'string',
      columnTemplate: columnTypeRenderer,
    },
    {
      name: 'Availability',
      prop: 'availability',
      size: 180,
      filter: ['selection'],
      columnType: 'dropdown',
      columnTemplate: columnTypeRenderer,
      cellTemplate: ColumnDropdown.cellTemplate,
      cellProperties: ColumnDropdown.cellProperties,
      readonly: true,
      dropdown: {
        source: AVAILABILITY_OPTIONS,
        renderSelectedValue: (h, selectedOptions, children) =>
          h('span', null, [
            renderAvailabilityBadge(h, selectedOptions[0]?.label ?? selectedOptions[0]?.value),
            children,
          ]),
        renderOption: (h, option) => renderAvailabilityBadge(h, option.label),
      },
    },
    {
      name: 'Price',
      prop: 'price',
      size: 110,
      filter: 'number',
      columnType: 'currency',
      columnTemplate: columnTypeRenderer,
    },
  ];
}

export function createFilterSelectionFilter(): {
  multiFilterItems: any;
  expressions: ColumnFilterConfig['expressions'];
  localization: {
    captions: Record<string, string>;
  };
  selection: {
    sortDirection: 'asc';
    grouping: Record<string, { props: string[]; expandedAll: boolean }>;
    getItems: Record<string, () => any[]>;
    itemTemplate: Record<string, (h: any, props: any) => any>;
  };
} {
  return {
    multiFilterItems: {
      name: [
        {
          id: 0,
          type: 'selection',
          value: new Set([
            normalizeFruitValue(FRUITS[7]),
            normalizeFruitValue(FRUITS[5]),
          ]),
          relation: 'and',
          hidden: true,
        },
      ],
    },
    expressions: {
      enabled: true,
      buttonLabel: 'Expression',
      placeholder: [
        'in ("Apple 🍎", "Orange 🍊")',
        'not in ("Banana 🍌", "Lemon 🍋")',
        'contains "berry" OR contains "citrus"',
      ].join('\n'),
    },
    localization: {
      captions: {
        expressionButton: 'Expression',
        expressionTitle: 'Filter expression',
        expressionPlaceholder: 'Type a filter expression',
        expressionApply: 'Apply expression',
        expressionInvalid: 'Fix the expression before applying.',
      },
    },
    selection: {
      sortDirection: 'asc',
      grouping: {
        name: {
          props: ['fruitFamily', 'fruitColor'],
          expandedAll: true,
        },
      },
      getItems: {
        name: () =>
          FRUITS.map((fruit) => ({
            value: normalizeFruitValue(fruit),
            label: `${fruit.name} ${fruit.emoji}`,
            fruitFamily: fruit.family,
            fruitColor: fruit.color,
          })),
        availability: () => AVAILABILITY_OPTIONS,
      },
      itemTemplate: {
        name: (h, { label }) => renderFruitLabel(h, label),
        availability: (h, { label }) => renderAvailabilityBadge(h, label),
      },
    },
  };
}

Key Features

  • Custom Filter Types: Enables the use of slider and selection filters, allowing users to quickly narrow down data based on specific criteria.
  • Virtualized Selection List: Selection options render through a nested revo-grid with row virtualization.
  • Custom Option Templates: Selection options can render custom content next to the checkbox, such as icons, badges, or formatted labels.
  • Grouped Selection Options: Selection options can be grouped by fields returned from selection.getItems.
  • Flexible and Extensible: This plugin demonstrates the potential for creating custom plugins, encouraging developers to expand the grid’s functionality further.
  • Automatic Theme Support: The plugin adapts to light or dark themes based on user settings.
  • sortDirection: The default sort direction, can be ‘asc’ or ‘desc’ or ‘none’
  • quickSearchFiltering: Controls whether typing in the selection popup search input also filters grid rows. Defaults to true. Set to false to only narrow the popup option list.
  • getItems: A custom source for selection values. It accepts either one loader function for every selection-filter column or a record keyed by column prop.
  • itemTemplate: Custom renderer for option content next to the plugin-owned checkbox. It accepts one renderer for every selection-filter column or a record keyed by column prop.
  • grouping: Optional grouping for the nested selection option grid. It accepts one GroupingOptions object or a record keyed by column prop.
  • plugins: Optional plugins for the nested selection option grid. It accepts one plugin array or a record keyed by column prop.
  • gridSettings: Optional nested selection grid settings, such as theme, rowSize, frameSize, columnTypes, additionalData, readonly, range, or useClipboard. source, columns, and grouping are controlled by the selection filter renderer.
  • cascadeOptions.enabled: Enables context-aware selection options that apply active filters from other columns and ignore the current column filter.
  • selectionTitle: Optional title shown above the selection filter options. Omit it to render no selection title.
  • selectionSearchPlaceholder: Placeholder text for the selection filter search input. Defaults to Search....
  • sliderTitle: The title of the slider filter
  • expressions: Enables the optional expression editor in the popup. Selection expressions such as is "Apple 🍎", in ("Apple 🍎", "Orange 🍊"), and not in ("Banana 🍌") compile into the same hidden selection filter model when the column has option metadata.
grid.filter = {
expressions: {
enabled: true,
placeholder: 'in ("Apple 🍎", "Orange 🍊")',
},
localization: {
captions: {
selectionTitle: 'Selection',
selectionSearchPlaceholder: 'Search values...',
sliderTitle: 'Slider',
expressionButton: 'Expression',
expressionTitle: 'Filter expression',
expressionInvalid: 'Fix the expression before applying.',
},
},
selection: {
sortDirection: 'asc', // default sort direction, can be 'asc' or 'desc' or 'none'
quickSearchFiltering: true, // default: selection popup search also filters grid rows
getItems: async (prop) => {
if (prop !== 'name') {
return [];
}
return [
{ value: 'apple', label: 'Apple', family: 'Tree fruit', color: 'Red' },
{ value: 'banana', label: 'Banana', family: 'Tropical', color: 'Yellow' },
{ value: 'orange', label: 'Orange', family: 'Citrus', color: 'Orange' },
];
},
grouping: {
name: {
props: ['family', 'color'],
expandedAll: true,
},
},
},
};
import { AdvanceFilterPlugin } from '@revolist/revogrid-pro';
// ...
grid.columns = [
{
// ...
filter: ['string', 'selection']
}
];
grid.plugins = [AdvanceFilterPlugin];
grid.filter = {
localization: {
captions: {
selectionTitle: 'Selection',
},
},
selection: {
sortDirection: 'asc', // default sort direction, can be 'asc' or 'desc' or 'none'
quickSearchFiltering: true, // default: search narrows options and filters grid rows
},
};

The selection popup keeps the search row fixed at the top and renders option rows through an internal revo-grid.

  • Search still matches normalized option value.
  • Select-all only affects currently searched and visible option rows.
  • Checked state still means the value is included; unchecked values are stored in the selection filter’s excluded-value Set.
  • Opening and closing a fully selected popup does not create an empty selection filter, so FilterHeaderPlugin keeps showing All.
  • Group rows are visual only; option checkboxes still belong to leaf option rows.

The old .filter-list li DOM shape is not part of the public API. Use the visible option behavior or .filter-list-option when tests need to interact with option rows.

By default, the selection popup search input does two things:

  • narrows the option rows shown inside the popup
  • applies a hidden quickSearch filter to the grid rows

This preserves the existing behavior for users who expect the grid to narrow while they type. Set selection.quickSearchFiltering to false when search should only help users find values in the popup and should not change the grid rows until they check or uncheck selection values.

grid.filter = {
selection: {
quickSearchFiltering: false,
},
};

Enable context-aware selection options with selection.cascadeOptions.enabled.

  • For column X, options are built from rows matching all active filters except filters for X.
  • This keeps current-column values reversible while still narrowing related column options.
  • The feature is opt-in, and default behavior is unchanged when omitted.
grid.filter = {
selection: {
cascadeOptions: {
enabled: true,
},
},
};

By default, the selection filter builds its checkbox list from the current grid data. Use filter.selection.getItems when the list should come from somewhere else, for example:

  • server-side or infinite-scroll datasets
  • a curated allow-list of accepted values
  • normalized values that differ from the rendered cell text

Default lists are rebuilt when the popup opens. If a cell value is edited, rows are removed, or grid.source is replaced, the next selection popup reflects the latest column values. Values that no longer exist in the source are not shown unless you provide them through a custom selection.getItems loader.

The loader receives the current column prop and may return data synchronously or asynchronously.

grid.filter = {
selection: {
getItems: async (prop) => {
if (prop !== 'name') {
return [];
}
const response = await fetch('/api/filter-options/names');
const items = await response.json();
return items.map((item: { id: string; title: string }) => ({
value: item.id.toLowerCase(),
label: item.title,
}));
},
},
};

If only some columns need custom values, pass a record keyed by column prop:

grid.filter = {
selection: {
getItems: {
name: async () => [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
],
category: () => [
{ value: 'fresh', label: 'Fresh' },
{ value: 'frozen', label: 'Frozen' },
],
},
},
};

Keep value aligned with the value used during filter comparison. The built-in selection filter compares lower-cased string values, so custom lists should usually provide lower-cased value fields.

Use selection.itemTemplate to render badges, icons, or richer labels inside the selection popup. The checkbox remains controlled by the filter plugin; the template only replaces the content next to it.

grid.filter = {
selection: {
getItems: {
availability: () => [
{ value: 'in stock', label: 'In stock', tone: 'green' },
{ value: 'backorder', label: 'Backorder', tone: 'amber' },
{ value: 'seasonal', label: 'Seasonal', tone: 'indigo' },
],
},
itemTemplate: {
availability: (h, { item, label, checked }) =>
h('span', {
class: `availability-badge availability-badge--${item.tone}`,
'data-selected': String(checked),
}, label),
},
},
};

itemTemplate receives:

  • columnProp: Current column property.
  • item: Original item returned by selection.getItems or the default source loader.
  • value: Normalized value used by selection filtering.
  • label: Display label.
  • checked: Whether the option is currently included in the result.

Selection option rows can be grouped by fields on the option item. This is useful for large curated lists where users need a hierarchy such as family and color, region and country, or category and status.

grid.filter = {
selection: {
grouping: {
name: {
props: ['family', 'color'],
expandedAll: true,
},
},
getItems: {
name: () => [
{ value: 'apple', label: 'Apple', family: 'Tree fruit', color: 'Red' },
{ value: 'pear', label: 'Pear', family: 'Tree fruit', color: 'Green' },
{ value: 'lemon', label: 'Lemon', family: 'Citrus', color: 'Yellow' },
],
},
},
};

selection.grouping can also be a single GroupingOptions object when every selection-filter column should use the same grouping configuration.

The selection list is a nested grid, and you can pass plugins or grid settings to that nested grid when you need additional rendering behavior.

import { AdvanceFilterPlugin } from '@revolist/revogrid-pro';
class SelectionListPlugin {
constructor(selectionGrid) {
selectionGrid.setAttribute('data-selection-list-plugin', 'mounted');
}
}
grid.plugins = [AdvanceFilterPlugin];
grid.filter = {
selection: {
plugins: {
name: [SelectionListPlugin],
},
gridSettings: {
name: {
theme: 'compact',
rowSize: 32,
frameSize: 2,
hideAttribution: true,
},
},
},
};

selection.plugins and selection.gridSettings accept either a global value or a record keyed by column prop.

The filter list owns source, columns, and grouping so filtering, checkbox state, and virtualization stay consistent. Use selection.grouping for grouped option rows instead of gridSettings.grouping.

Selection filters store unchecked values as an excluded-value Set. To predefine a selection filter, add a hidden selection filter with values that should be excluded.

grid.filter = {
multiFilterItems: {
name: [
{
id: 0,
type: 'selection',
value: new Set(['banana', 'lemon']),
relation: 'and',
hidden: true,
},
],
},
};

The filter-selection demo uses this pattern for the Fruit column, so one or two fruit values are already unchecked when the demo loads.

This plugin highlights the extensibility of RevoGrid’s plugin system, showcasing how developers can build and integrate advanced features tailored to specific needs. The flexibility of RevoGrid plugins enables comprehensive data manipulation and display control, making it an ideal choice for complex data-driven applications. For a deeper dive into plugin development, refer to the RevoGrid Plugin Documentation.