niko-table
componentdata-table-faceted-filtersource: ok · hit
DataTable Faceted Filter
Faceted filter for single/multiple selection with option counts and search.
npx shadcn add "https://niko-table.com/r/data-table-faceted-filter.json"free
Where it goes
data-table
Interaction
dropdown
Use cases
dashboard
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
https://niko-table.com/r/data-table-core.jsonbuttonbadgecommandpopoverseparator
Files3 · source cached 10/8/2026, 7:44:19 PM
src/components/niko-table/components/data-table-faceted-filter.tsx
"use client"
/**
* niko-table — created by Semir N. (Semkoo, https://github.com/Semkoo) with AI assistance.
*
* Before reporting anything: please check the changelog first.
* - In-repo: ./CHANGELOG.md
* - Docs site: https://niko-table.com/changelog
*
* Found a bug or have a fix? Open an issue or PR on GitHub so other
* users (and future LLMs reading this code) benefit:
* https://github.com/Semkoo/niko-table-registry
*/
import * as React from "react"
import {
TableFacetedFilter,
TableFacetedFilterContent,
useTableFacetedFilter,
type TableFacetedFilterProps,
} from "../filters/table-faceted-filter"
import { useDataTable } from "../core/data-table-context"
import type { DataTableInstance, Option } from "../types"
import { useDerivedColumnTitle } from "../hooks/use-derived-column-title"
import { useGeneratedOptionsForColumn } from "../hooks/use-generated-options"
import { buildFacetedOptions } from "../lib/build-faceted-options"
import type { RowData } from "@tanstack/react-table"
type DataTableFacetedFilterProps<TData extends RowData, TValue> = Omit<
TableFacetedFilterProps<TData, TValue>,
"column" | "options"
> & {
/**
* The accessor key of the column to filter (matches column definition)
*/
accessorKey: keyof TData & string
/**
* Optional title override (if not provided, will use column.meta.label)
*/
title?: string
/**
* Static options (if provided, will be used instead of dynamic generation)
*/
options?: Option[]
/**
* Whether to show counts for each option
* @default true
*/
showCounts?: boolean
/**
* Whether to update counts based on other active filters
* @default true
*/
dynamicCounts?: boolean
/**
* If true, only show options that exist in the currently filtered table rows.
* If false, show all options from the entire dataset (useful for multi-select filters
* where you want to see all possible options even if they're not in the current filtered results).
* @default !multiple (true for single-select, false for multi-select)
*/
limitToFilteredRows?: boolean
}
/**
* A faceted filter component that automatically connects to the DataTable context
* and dynamically generates options with counts based on the filtered data.
*
* @example - Auto-detect options from data with dynamic counts
* const columns: DataTableColumnDef[] = [{ accessorKey: "category", ..., meta: { label: "Category" } }, ...]
* <DataTableFacetedFilter accessorKey="category" />
*
* @example - With static options
* const categoryOptions: Option[] = [
* { label: "Electronics", value: "electronics" },
* { label: "Clothing", value: "clothing" },
* ]
* <DataTableFacetedFilter
* accessorKey="category"
* title="Category"
* options={categoryOptions}
* />
*
* @example - With dynamic option generation and multiple selection
* <DataTableFacetedFilter
* accessorKey="brand"
* title="Brand"
* multiple
* dynamicCounts
* />
*
* @example - Without counts
* <DataTableFacetedFilter
* accessorKey="status"
* showCounts={false}
* />
*/
interface UseFacetedOptionsArgs<TData extends RowData> {
table: DataTableInstance<TData>
accessorKey: string
options?: Option[]
showCounts: boolean
dynamicCounts: boolean
limitToFilteredRows: boolean
precomputedOptions?: Record<string, Option[]>
}
// Resolves option list in priority order: 1) caller `options`, 2) meta-aware
// generator (select/multiSelect), 3) data-derived fallback. Memo gates it so
// we don't walk rows twice.
function useFacetedOptions<TData extends RowData>({
table,
accessorKey,
options,
showCounts,
dynamicCounts,
limitToFilteredRows,
precomputedOptions,
}: UseFacetedOptionsArgs<TData>): Option[] {
const column = table.getColumn(accessorKey)
// The meta-aware generator is authoritative for declared select variants —
// it handles augment/preserve strategies and per-column meta overrides.
// It returns `[]` for columns that don't match a select variant, which is
// how we detect "fall back to data-derived options."
const metaGenerated = precomputedOptions?.[accessorKey] ?? []
const needsFallbackGeneration = !precomputedOptions
const perColumnGenerated = useGeneratedOptionsForColumn(
table,
needsFallbackGeneration ? accessorKey : "__noop__",
{
showCounts,
dynamicCounts,
limitToFilteredRows,
},
)
const resolvedMetaGenerated = needsFallbackGeneration
? perColumnGenerated
: metaGenerated
// Pull state slices for memo reactivity.
const state = table.state
const columnFilters = state.columnFilters
const globalFilter = state.globalFilter
// Extract `coreRows` so async-data row-array identity drives recompute;
// `table` ref is stable and would hold stale (empty) results.
const coreRows = table.getCoreRowModel().rows
return React.useMemo((): Option[] => {
if (!column) return []
const meta = column.columnDef.meta
const autoOptionsFormat = meta?.autoOptionsFormat ?? true
const formatOptionLabel = meta?.formatOptionLabel
// Priority 1: caller-supplied options — always wins over meta/data.
if (options && options.length > 0) {
return buildFacetedOptions(
table,
coreRows,
accessorKey,
columnFilters,
globalFilter,
{
staticOptions: options,
limitToFilteredRows,
dynamicCounts,
showCounts,
autoOptionsFormat,
formatOptionLabel,
},
)
}
// Priority 2: trust the meta-aware generator when it produced anything.
// (Preserved original "non-empty result wins" behavior so auto-generated
// columns with valid data don't get clobbered by the fallback.)
if (resolvedMetaGenerated.length > 0) return resolvedMetaGenerated
// Priority 3: data-derived fallback for non-select variants (or select
// variants that had no rows to work with — empty output either way).
return buildFacetedOptions(
table,
coreRows,
accessorKey,
columnFilters,
globalFilter,
{
limitToFilteredRows,
dynamicCounts,
showCounts,
autoOptionsFormat,
formatOptionLabel,
},
)
}, [
column,
options,
resolvedMetaGenerated,
table,
coreRows,
accessorKey,
columnFilters,
globalFilter,
limitToFilteredRows,
dynamicCounts,
showCounts,
])
}
/**
* Shared setup for the two exported wrapper components. Keeps column lookup,
* title derivation, and options resolution in one place so the wrappers stay
* thin.
*/
function useFacetedFilterSetup<TData extends RowData>({
accessorKey,
options,
showCounts,
dynamicCounts,
limitToFilteredRows,
title,
}: {
accessorKey: string
options?: Option[]
showCounts: boolean
dynamicCounts: boolean
limitToFilteredRows: boolean
title?: string
}) {
const { table, generatedOptionsMap } = useDataTable<TData>()
const column = table.getColumn(accessorKey)
const derivedTitle = useDerivedColumnTitle(column, accessorKey, title)
const dynamicOptions = useFacetedOptions({
table,
accessorKey,
options,
showCounts,
dynamicCounts,
limitToFilteredRows,
precomputedOptions: generatedOptionsMap,
})
return { table, column, derivedTitle, dynamicOptions }
}
export function DataTableFacetedFilter<
TData extends RowData,
TValue = unknown,
>({
accessorKey,
options,
showCounts = true,
dynamicCounts = true,
limitToFilteredRows,
title,
multiple,
trigger,
...props
}: DataTableFacetedFilterProps<TData, TValue>) {
// Default: multi-select shows all options, single-select filters to visible rows
const resolvedLimitToFilteredRows = limitToFilteredRows ?? !multiple
const { column, derivedTitle, dynamicOptions } = useFacetedFilterSetup<TData>(
{
accessorKey: accessorKey as string,
options,
showCounts,
dynamicCounts,
limitToFilteredRows: resolvedLimitToFilteredRows,
title,
},
)
// Early return if column not found
if (!column) {
console.warn(
`Column with accessorKey "${accessorKey}" not found in table columns`,
)
return null
}
return (
<TableFacetedFilter
column={column}
options={dynamicOptions}
title={derivedTitle}
multiple={multiple}
trigger={trigger}
{...props}
/>
)
}
/**
* @required displayName is required for auto feature detection
* @see "feature-detection.ts"
*/
DataTableFacetedFilter.displayName = "DataTableFacetedFilter"
export function DataTableFacetedFilterContent<
TData extends RowData,
TValue = unknown,
>({
accessorKey,
options,
showCounts = true,
dynamicCounts = true,
limitToFilteredRows,
title,
multiple,
onValueChange,
}: DataTableFacetedFilterProps<TData, TValue>) {
// Default: multi-select shows all options, single-select filters to visible rows
const resolvedLimitToFilteredRows = limitToFilteredRows ?? !multiple
const { column, derivedTitle, dynamicOptions } = useFacetedFilterSetup<TData>(
{
accessorKey: accessorKey as string,
options,
showCounts,
dynamicCounts,
limitToFilteredRows: resolvedLimitToFilteredRows,
title,
},
)
// Use the shared hook for filter logic
const { selectedValues, onItemSelect, onReset } = useTableFacetedFilter({
column,
onValueChange,
multiple,
})
if (!column) return null
return (
<TableFacetedFilterContent
title={derivedTitle}
options={dynamicOptions}
selectedValues={selectedValues}
onItemSelect={onItemSelect}
onReset={onReset}
/>
)
}
DataTableFacetedFilterContent.displayName = "DataTableFacetedFilterContent"
src/components/niko-table/filters/table-faceted-filter.tsx
"use client"
/**
* niko-table — created by Semir N. (Semkoo, https://github.com/Semkoo) with AI assistance.
*
* Before reporting anything: please check the changelog first.
* - In-repo: ./CHANGELOG.md
* - Docs site: https://niko-table.com/changelog
*
* Found a bug or have a fix? Open an issue or PR on GitHub so other
* users (and future LLMs reading this code) benefit:
* https://github.com/Semkoo/niko-table-registry
*/
/**
* Table faceted filter component
* @description A faceted filter component for DataTable that allows users to filter data based on multiple selectable options. It supports both single and multiple selection modes.
*/
import { Check, PlusCircle, XCircle } from "lucide-react"
import * as React from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils"
import type { DataTableColumn, ExtendedColumnFilter, Option } from "../types"
import {
FILTER_OPERATORS,
FILTER_VARIANTS,
JOIN_OPERATORS,
} from "../lib/constants"
import type { RowData } from "@tanstack/react-table"
export interface TableFacetedFilterProps<TData extends RowData, TValue> {
column?: DataTableColumn<TData, TValue>
title?: string
options: Option[]
multiple?: boolean
/**
* Callback fired when filter value changes
* Useful for server-side filtering or external state management
*/
onValueChange?: (value: string[] | undefined) => void
/**
* Optional custom trigger element
*/
trigger?: React.ReactNode
}
export function useTableFacetedFilter<TData extends RowData, TValue = unknown>({
column,
onValueChange,
multiple,
}: {
column?: DataTableColumn<TData, TValue>
onValueChange?: (value: string[] | undefined) => void
multiple?: boolean
}) {
const columnFilterValue = column?.getFilterValue()
// Handle both ExtendedColumnFilter format (new) and legacy array format
const selectedValues = React.useMemo(() => {
// Handle ExtendedColumnFilter format (from filter menu or new faceted filter)
if (
columnFilterValue &&
typeof columnFilterValue === "object" &&
!Array.isArray(columnFilterValue) &&
"value" in columnFilterValue
) {
const filterValue = (columnFilterValue as ExtendedColumnFilter<TData>)
.value
return new Set(
Array.isArray(filterValue) ? filterValue : [String(filterValue)],
)
}
// Handle legacy array format (backward compatibility)
return new Set(Array.isArray(columnFilterValue) ? columnFilterValue : [])
}, [columnFilterValue])
const onItemSelect = React.useCallback(
(option: Option, isSelected: boolean) => {
if (!column) return
if (multiple) {
const newSelectedValues = new Set(selectedValues)
if (isSelected) {
newSelectedValues.delete(option.value)
} else {
newSelectedValues.add(option.value)
}
const filterValues = Array.from(newSelectedValues)
if (filterValues.length === 0) {
column.setFilterValue(undefined)
onValueChange?.(undefined)
} else {
// Create ExtendedColumnFilter format for interoperability with filter menu
// FORCE variant to multiSelect when using IN operator to ensure it shows up in the menu
const extendedFilter: ExtendedColumnFilter<TData> = {
id: column.id as Extract<keyof TData, string>,
value: filterValues,
variant: FILTER_VARIANTS.MULTI_SELECT,
operator: FILTER_OPERATORS.IN,
filterId: `faceted-${column.id}`,
joinOperator: JOIN_OPERATORS.AND,
}
column.setFilterValue(extendedFilter)
onValueChange?.(filterValues)
}
} else {
// Single selection
if (isSelected) {
column.setFilterValue(undefined)
onValueChange?.(undefined)
} else {
// Create ExtendedColumnFilter format for single selection
// Use EQUAL operator for single select
const extendedFilter: ExtendedColumnFilter<TData> = {
id: column.id as Extract<keyof TData, string>,
value: option.value, // Single value, not array
variant: FILTER_VARIANTS.SELECT,
operator: FILTER_OPERATORS.EQ,
filterId: `faceted-${column.id}`,
joinOperator: JOIN_OPERATORS.AND,
}
column.setFilterValue(extendedFilter)
onValueChange?.([option.value])
}
}
},
[column, multiple, selectedValues, onValueChange],
)
const onReset = React.useCallback(
(event?: React.MouseEvent) => {
event?.stopPropagation()
column?.setFilterValue(undefined)
onValueChange?.(undefined)
},
[column, onValueChange],
)
return {
selectedValues,
onItemSelect,
onReset,
}
}
export function TableFacetedFilter<TData extends RowData, TValue>({
column,
title,
options = [],
multiple,
onValueChange,
trigger,
}: TableFacetedFilterProps<TData, TValue>) {
const [open, setOpen] = React.useState(false)
const { selectedValues, onItemSelect, onReset } = useTableFacetedFilter({
column,
onValueChange,
multiple,
})
// Wrap onItemSelect to close multiple=false popover
const handleItemSelect = React.useCallback(
(option: Option, isSelected: boolean) => {
onItemSelect(option, isSelected)
if (!multiple) {
setOpen(false)
}
},
[onItemSelect, multiple, setOpen],
)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{trigger || (
<Button variant="outline" size="sm" className="h-8 border-dashed">
{selectedValues?.size > 0 ? (
<div
role="button"
aria-label={`Clear ${title} filter`}
tabIndex={0}
onClick={onReset}
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
onReset(e as unknown as React.MouseEvent)
}
}}
className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
>
<XCircle className="size-4" />
</div>
) : (
<PlusCircle className="size-4" />
)}
{title}
{selectedValues?.size > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden items-center gap-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal"
>
{selectedValues.size} selected
</Badge>
) : (
options
.filter(option => selectedValues.has(option.value))
.map(option => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
)}
</PopoverTrigger>
<PopoverContent className="w-52 p-0" align="start">
<TableFacetedFilterContent
title={title}
options={options}
selectedValues={selectedValues}
onItemSelect={handleItemSelect}
onReset={onReset}
/>
</PopoverContent>
</Popover>
)
}
export function TableFacetedFilterContent({
title,
options,
selectedValues,
onItemSelect,
onReset,
}: {
title?: string
options: Option[]
selectedValues: Set<string>
onItemSelect: (option: Option, isSelected: boolean) => void
onReset: (event?: React.MouseEvent) => void
}) {
return (
<Command>
<CommandInput placeholder={title} className="pl-2" />
<CommandList className="max-h-full">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup className="max-h-75 overflow-x-hidden overflow-y-auto">
{options.map(option => {
const isSelected = selectedValues.has(option.value)
return (
<CommandItem
key={option.value}
onSelect={() => onItemSelect(option, isSelected)}
>
<div
className={cn(
"mr-2 flex size-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<Check className="size-4" />
</div>
{option.icon && <option.icon className="mr-2 size-4" />}
<span className="truncate">{option.label}</span>
{option.count !== undefined && (
<span className="ml-auto font-mono text-xs">
{option.count}
</span>
)}
</CommandItem>
)
})}
</CommandGroup>
{selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => onReset()}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
)
}
/**
* @required displayName is required for auto feature detection
* @see "feature-detection.ts"
*/
TableFacetedFilter.displayName = "TableFacetedFilter"
src/components/niko-table/lib/build-faceted-options.ts
/**
* niko-table — created by Semir N. (Semkoo, https://github.com/Semkoo) with AI assistance.
*
* Before reporting anything: please check the changelog first.
* - In-repo: ./CHANGELOG.md
* - Docs site: https://niko-table.com/changelog
*
* Found a bug or have a fix? Open an issue or PR on GitHub so other
* users (and future LLMs reading this code) benefit:
* https://github.com/Semkoo/niko-table-registry
*/
import type { RowData } from "@tanstack/react-table"
import type { DataTableInstance, DataTableRow } from "../types"
import { getFilteredRowsExcludingColumn } from "./filter-rows"
import { formatLabel } from "./format"
import type { Option } from "../types"
export interface BuildFacetedOptionsConfig {
/**
* If provided, these options are the source of truth and the returned list
* is a subset of them (optionally narrowed by `limitToFilteredRows` and
* enriched with live counts).
*
* If omitted, options are derived from the rows themselves — useful for
* columns that are not declared as `select`/`multiSelect` variants but are
* still being used with a faceted filter (boolean, text, etc.).
*/
staticOptions?: Option[]
/**
* Narrow the returned options to values that exist in rows passing every
* *other* active filter (the current column's own filter is excluded).
*/
limitToFilteredRows: boolean
/**
* Compute counts against filtered rows (again, excluding the current
* column's filter) rather than against the full core row model.
*/
dynamicCounts: boolean
/**
* When false, `count` is stripped from the output for a consistent shape.
*/
showCounts: boolean
/**
* When deriving options from rows, run labels through `formatLabel` (title
* case etc.). Ignored when `staticOptions` is provided — callers' labels
* are always preserved as-is.
*/
autoOptionsFormat: boolean
/**
* Optional per-column label formatter. Wins over `autoOptionsFormat` when
* present. Receives the stringified row value, returns the display label.
* Ignored when `staticOptions` is provided.
*/
formatOptionLabel?: (value: string) => string
}
/**
* Pure builder for the option list surfaced by a faceted filter. Handles
* both explicit `staticOptions` (narrow + enrich with counts) and
* row-derived options. Plain function (not a hook) so the wrapper can
* gate it behind a single `useMemo`.
*/
export function buildFacetedOptions<TData extends RowData>(
table: DataTableInstance<TData>,
coreRows: DataTableRow<TData>[],
accessorKey: string,
columnFilters: Array<{ id: string; value: unknown }>,
globalFilter: unknown,
config: BuildFacetedOptionsConfig,
): Option[] {
const {
staticOptions,
limitToFilteredRows,
dynamicCounts,
showCounts,
autoOptionsFormat,
formatOptionLabel,
} = config
// Fast path: explicit options, no narrowing, no counts — just normalize the
// shape so every return path of this function looks the same to callers.
if (staticOptions && !limitToFilteredRows && !showCounts) {
return staticOptions.map(opt => ({ ...opt, count: undefined }))
}
// Only compute the filtered row subset if at least one flag actually needs
// it. This avoids the cost of `getFilteredRowsExcludingColumn` when the
// caller has opted out of both narrowing and dynamic counts.
const needsFilteredRows = limitToFilteredRows || dynamicCounts
const filteredRows = needsFilteredRows
? getFilteredRowsExcludingColumn(
table,
coreRows,
accessorKey,
columnFilters,
globalFilter,
)
: coreRows
const optionRows = limitToFilteredRows ? filteredRows : coreRows
const countRows = dynamicCounts ? filteredRows : coreRows
// Collect the set of values present in `optionRows`. Used for narrowing
// static options and for producing the auto-derived option list.
const availableValues = collectRowValues(optionRows, accessorKey)
// The column's own currently-selected values must never be narrowed away:
// otherwise a selection that another filter excludes (e.g. Brand=Apple while
// Category=Clothing) vanishes from its own facet — the pill disappears and
// the value can't be un-checked, even though the filter is still applied.
const selectedValues = getSelectedValues(columnFilters, accessorKey)
const keepValue = (value: string) =>
availableValues.has(value) || selectedValues.has(value)
// Build the base option list (no counts yet).
let baseOptions: Option[]
if (staticOptions) {
baseOptions = limitToFilteredRows
? staticOptions.filter(opt => keepValue(opt.value))
: staticOptions
} else {
const optionValues = limitToFilteredRows
? new Set([...availableValues, ...selectedValues])
: availableValues
baseOptions = Array.from(optionValues)
.map(value => ({
value,
label: formatOptionLabel
? formatOptionLabel(value)
: autoOptionsFormat
? formatLabel(value)
: value,
}))
.sort((a, b) => a.label.localeCompare(b.label))
}
if (!showCounts) {
return baseOptions.map(opt => ({ ...opt, count: undefined }))
}
// Scope counts to baseOptions values up-front so every returned option
// has a count and every count maps to a returned option.
const targetValues = new Set(baseOptions.map(opt => opt.value))
const valueCounts = new Map<string, number>()
for (const row of countRows) {
const raw = row.getValue(accessorKey) as unknown
const values: unknown[] = Array.isArray(raw) ? raw : [raw]
for (const v of values) {
if (v == null) continue
const str = String(v)
if (!str) continue
if (targetValues.has(str)) {
valueCounts.set(str, (valueCounts.get(str) ?? 0) + 1)
}
}
}
return baseOptions.map(opt => ({
...opt,
// Prefer a caller-supplied count over the row-derived one. On a server-side
// table the rows are only the current page, so the caller passes true
// server-computed facet counts; keep them. Options without a count fall
// back to the row-derived value. Matches the header-funnel path.
count: opt.count ?? valueCounts.get(opt.value) ?? 0,
}))
}
/**
* Extract the set of selected values from a single column's filter value,
* tolerating every shape it can take: the `ExtendedColumnFilter` object written
* by the faceted control and the advanced menu (`{ value: string | string[] }`),
* and the legacy bare array / scalar.
*/
export function extractFilterSelectedValues(rawValue: unknown): Set<string> {
let raw = rawValue
if (raw && typeof raw === "object" && !Array.isArray(raw) && "value" in raw) {
raw = (raw as { value: unknown }).value
}
if (raw == null || raw === "") return new Set()
const values = Array.isArray(raw) ? raw : [raw]
return new Set(values.filter(v => v != null && v !== "").map(String))
}
function getSelectedValues(
columnFilters: Array<{ id: string; value: unknown }>,
accessorKey: string,
): Set<string> {
const entry = columnFilters.find(filter => filter.id === accessorKey)
return entry ? extractFilterSelectedValues(entry.value) : new Set()
}
function collectRowValues<TData extends RowData>(
rows: DataTableRow<TData>[],
accessorKey: string,
): Set<string> {
const set = new Set<string>()
for (const row of rows) {
const raw = row.getValue(accessorKey) as unknown
const values: unknown[] = Array.isArray(raw) ? raw : [raw]
for (const v of values) {
if (v == null) continue
const str = String(v)
if (str) set.add(str)
}
}
return set
}
More from niko-table
Data TableblockDataTable AsidecomponentDataTable Clear FiltercomponentDataTable Column Auto-fitcomponentDataTable Column Date FiltercomponentDataTable Column DnDcomponentDataTable Column Faceted FiltercomponentDataTable Column Groupcomponent
Browse all components in this library →