Skip to content

Excel-Style Filter

Windows Excel mode turns every selection-filter column into a staged checklist modeled after the Windows Excel AutoFilter menu. Users search or check values, review the draft, and then choose Apply or Cancel.

Source code
TypeScriptts
grid.plugins = [AdvanceFilterPlugin];
grid.columns = [
  { prop: 'status', name: 'Status', filter: [FIlTER_SELECTION] },
];
grid.filter = {
  selection: {
    excelMode: 'windows',
  },
};

return [
  { status: 'Open' },
  { status: 'Closed' },
  { status: 'Pending' },
  { status: 'Closed' },
];

Register AdvanceFilterPlugin, enable the selection family on a column, and set the global selection preset to windows:

import { AdvanceFilterPlugin, FIlTER_SELECTION } from '@revolist/revogrid-pro';
import '@revolist/revogrid-pro/dist/revogrid-pro.css';
grid.plugins = [AdvanceFilterPlugin];
grid.columns = [
{ name: 'Status', prop: 'status', filter: [FIlTER_SELECTION] },
{ name: 'Owner', prop: 'owner', filter: ['string'] },
];
grid.filter = {
selection: {
excelMode: 'windows',
sortDirection: 'asc',
},
};
grid.source = [
{ status: 'Open', owner: 'Ana' },
{ status: 'Closed', owner: 'Maks' },
];

selection.excelMode is grid-wide selection configuration, but it only changes columns that include the selection filter. The Owner column above keeps its ordinary text-filter popup.

Source code
TypeScriptts
// 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();
}
Vuevue
// 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>
Reacttsx
// 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
      plugins={plugins}
      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'}
    />
  );
}

export default FilterAdvanced;
Angularts
// 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
      [plugins]="plugins"
      [columns]="columns"
      [source]="source"
      [columnTypes]="columnTypes"
      stretch="last"
      [filter]="filter"
      [hideAttribution]="true"
      [theme]="theme"
      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 setupts
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;
  harvestedAt: string | null;
};

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[] {
  const harvestDates = [
    '2024-12-01',
    '2025-02-03',
    '2025-02-03T18:30:00',
    '2025-07-14',
    '2026-01-05',
    null,
  ];
  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),
      harvestedAt: harvestDates[index % harvestDates.length],
    };
  });
}

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: (h, { value }) => renderAvailabilityBadge(h, value),
      dropdown: {
        source: AVAILABILITY_OPTIONS,
        syncCellTemplate: true,
      },
    },
    {
      name: 'Harvest date',
      prop: 'harvestedAt',
      size: 150,
      filter: ['date', 'selection'],
      columnType: 'date',
    },
    {
      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: {
    excelMode: 'windows';
    sortDirection: 'asc';
    grouping: Record<string, { props: string[]; expandedAll: boolean }>;
    getItems: Record<string, () => any[]>;
    itemTemplate: Record<string, (h: any, props: any) => any>;
    syncCellTemplate: Record<string, boolean>;
  };
} {
  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.',
        selectionApply: 'Apply',
        selectionCancel: 'Cancel',
        selectionSelectAll: '(Select All)',
        selectionSelectAllSearchResults: '(Select All Search Results)',
        selectionAddCurrentSelection: 'Add current selection to filter',
        selectionBlanks: '(Blanks)',
      },
    },
    selection: {
      excelMode: 'windows',
      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,
      },
      syncCellTemplate: {
        availability: true,
      },
      itemTemplate: {
        name: (h, { label }) => renderFruitLabel(h, label),
      },
    },
  };
}

The example uses the same setup in Vanilla TypeScript, Vue, React, and Angular. Open the Fruit or Availability filter for a flat checklist, or Harvest date for the date hierarchy.

Excel mode is always transactional, even when disableDynamicFiltering is false or omitted:

  • Checking values and typing in the mini-filter only changes the popup draft.
  • Apply commits the draft and closes the popup.
  • Cancel, Escape, outside click, the close button, and switching to another popup discard the draft.
  • Replacing the grid source or filter configuration while the popup is open also discards its draft.
  • Pressing Enter in the mini-filter applies the current search result and closes the popup.

The popup is checklist-only; it does not show the normal operator dropdown. Use the standard Selection Filter when changes should apply immediately or users need the ordinary filter-family workflow.

Excel-style selection filter with a mini-filter, checked Select All control, collapsed year values, a Blanks option, and Apply and Cancel buttons.

Rows remain unchanged while the checklist is edited and update only after Apply.

Typing a non-empty mini-filter selects the matching results and changes the parent action to Select All Search Results. Applying immediately keeps the matches and discards selections hidden by the search.

Enable Add current selection to filter when the result should instead be the union of the last applied selection and the current matches. Clearing the search returns the checklist to the last applied state.

Select All affects the currently visible or searched options. Parent checkboxes show a mixed state when only some of their leaf values are selected.

Excel-style selection filter searching for ap, with Select All Search Results, Add current selection to filter, and matching Apple and Grapes options.

Search selects its matches; Add current selection preserves the previously applied values as well.

Checked values are included. Internally, the selection filter stores unchecked values in an excluded-value Set. Applying either of these boundary states removes the selection condition:

  • every available value is checked
  • every available value is unchecked

This normalization avoids leaving a hidden condition that represents the complete option set.

Empty strings, null, and other normalized blank options share the (Blanks) item. It remains searchable and sorts after nonblank options in Excel mode.

By default, options are derived from the current grid rows and account for compatible filters on other columns. An explicit synchronous or asynchronous selection.getItems is authoritative:

grid.filter = {
selection: {
excelMode: 'windows',
getItems: {
status: async () => {
const response = await fetch('/api/filter-options/status');
return response.json(); // [{ value: 'open', label: 'Open' }, ...]
},
},
},
};

Keep each value aligned with the normalized value used for row comparison. Custom item templates, grouping, nested-grid settings, parsed cell values, and cascading options work as described in the general Selection Filter guide.

Excel mode creates a local-calendar Year → Month → Day tree only when the column provides all three signals:

grid.columns = [
{
name: 'Created',
prop: 'createdAt',
columnType: 'date',
filter: ['date', 'selection'],
},
];

The hierarchy starts collapsed and uses browser-locale month names. Multiple timestamps on the same local day share one day node. Parent and Select All checkboxes derive their mixed state from the raw leaf values. Invalid nonblank values remain selectable as flat root options, followed by (Blanks).

Excel-style date checklist with 2025 and February expanded to day 03, and mixed-state checkboxes on Select All and the 2025 parent.

The hierarchy groups raw values by their browser-local calendar day while preserving each raw leaf for filtering.

Setting only columnType: 'date', or only including date in filter, is not enough. Without the full configuration, the column uses a flat selection list.

Restore the same state model used by the default selection filter. The Set contains normalized values that should be unchecked:

grid.filter = {
multiFilterItems: {
status: [
{
id: 1,
type: 'selection',
value: new Set(['closed', 'archived']),
relation: 'and',
hidden: true,
},
],
},
selection: {
excelMode: 'windows',
},
};

Do not replace the full filter object with only multiFilterItems when the grid also uses localization or selection settings. Build the complete configuration or use the active plugin APIs described in Filter State and Presets.

Override only the captions the application needs:

import { defineAdvancedFilterConfig } from '@revolist/revogrid-pro';
grid.filter = defineAdvancedFilterConfig({
localization: {
captions: {
selectionSearchPlaceholder: 'Search values...',
selectionApply: 'Use values',
selectionCancel: 'Discard',
selectionSelectAll: '(All values)',
selectionSelectAllSearchResults: '(All search results)',
selectionAddCurrentSelection: 'Keep the current selection too',
selectionBlanks: '(Empty)',
},
},
selection: {
excelMode: 'windows',
},
});

defineAdvancedFilterConfig fills the remaining default filter names and captions.

  • Expecting Excel mode on a column that does not include the selection filter.
  • Treating the mini-filter as a live grid search; its draft is committed only by Apply or Enter.
  • Expecting disableDynamicFiltering to remove Apply and Cancel from Excel mode.
  • Saving multiFilterItems directly as JSON without converting selection Set values.
  • Configuring only one of the two required date signals and expecting a hierarchy.
  • Returning display labels from getItems that do not map to the values compared with row data.