From 2df7b922692623b4f15d1754d1fcfaf1bff47333 Mon Sep 17 00:00:00 2001 From: metonym Date: Sat, 19 Mar 2022 12:01:03 -0700 Subject: [PATCH] feat(toolbar-search): support auto-filterable rows (#1179) Closes #591 --- COMPONENT_INDEX.md | 19 ++++--- docs/src/COMPONENT_API.json | 13 ++++- docs/src/pages/components/DataTable.svx | 16 ++++++ .../DataTable/DataTableFilterCustom.svelte | 55 +++++++++++++++++++ .../DataTable/DataTableFilterable.svelte | 46 ++++++++++++++++ src/DataTable/DataTable.svelte | 30 +++++----- src/DataTable/ToolbarSearch.svelte | 41 +++++++++++++- tests/DataTable.test.svelte | 6 +- types/DataTable/ToolbarSearch.svelte.d.ts | 17 ++++++ 9 files changed, 218 insertions(+), 25 deletions(-) create mode 100644 docs/src/pages/framed/DataTable/DataTableFilterCustom.svelte create mode 100644 docs/src/pages/framed/DataTable/DataTableFilterable.svelte diff --git a/COMPONENT_INDEX.md b/COMPONENT_INDEX.md index e17b3bff..a2b50c50 100644 --- a/COMPONENT_INDEX.md +++ b/COMPONENT_INDEX.md @@ -991,8 +991,8 @@ export interface DataTableCell { | selectable | let | Yes | boolean | false | Set to `true` for the selectable variant
Automatically set to `true` if `radio` or `batchSelection` are `true` | | expandedRowIds | let | Yes | DataTableRowId[] | [] | Specify the row ids to be expanded | | expandable | let | Yes | boolean | false | Set to `true` for the expandable variant
Automatically set to `true` if `batchExpansion` is `true` | -| rows | let | Yes | DataTableRow[] | [] | Specify the rows the data table should render
keys defined in `headers` are used for the row ids | | headers | let | No | DataTableHeader[] | [] | Specify the data table headers | +| rows | let | No | DataTableRow[] | [] | Specify the rows the data table should render
keys defined in `headers` are used for the row ids | | size | let | No | "compact" | "short" | "medium" | "tall" | undefined | Set the size of the data table | | title | let | No | string | "" | Specify the title of the data table | | description | let | No | string | "" | Specify the description of the data table | @@ -4640,14 +4640,15 @@ None. ### Props -| Prop name | Kind | Reactive | Type | Default value | Description | -| :--------- | :--------------- | :------- | :---------------------------------------- | ------------------ | --------------------------------------------- | -| ref | let | Yes | null | HTMLInputElement | null | Obtain a reference to the input HTML element | -| expanded | let | Yes | boolean | false | Set to `true` to expand the search bar | -| value | let | Yes | number | string | "" | Specify the value of the search input | -| persistent | let | No | boolean | false | Set to `true` to keep the search bar expanded | -| disabled | let | No | boolean | false | Set to `true` to disable the search bar | -| tabindex | let | No | string | "0" | Specify the tabindex | +| Prop name | Kind | Reactive | Type | Default value | Description | +| :--------------- | :--------------- | :------- | :---------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ref | let | Yes | null | HTMLInputElement | null | Obtain a reference to the input HTML element | +| expanded | let | Yes | boolean | false | Set to `true` to expand the search bar | +| value | let | Yes | number | string | "" | Specify the value of the search input | +| persistent | let | No | boolean | false | Set to `true` to keep the search bar expanded | +| disabled | let | No | boolean | false | Set to `true` to disable the search bar | +| shouldFilterRows | let | No | boolean | ((rows: import("./DataTable.svelte").DataTableRow, value: number | string) => boolean) | false | Set to `true` to filter table rows using the search value.

If `true`, the default search excludes `id`, `cells` fields and
only does a basic comparison on string and number type cell values.

To implement your own client-side filtering, pass a function
that accepts a row and value and returns a boolean. | +| tabindex | let | No | string | "0" | Specify the tabindex | ### Slots diff --git a/docs/src/COMPONENT_API.json b/docs/src/COMPONENT_API.json index 4384ac97..9db9e5b6 100644 --- a/docs/src/COMPONENT_API.json +++ b/docs/src/COMPONENT_API.json @@ -2282,7 +2282,7 @@ "isFunction": false, "isFunctionDeclaration": false, "constant": false, - "reactive": true + "reactive": false }, { "name": "size", @@ -13218,6 +13218,17 @@ "constant": false, "reactive": false }, + { + "name": "shouldFilterRows", + "kind": "let", + "description": "Set to `true` to filter table rows using the search value.\n\nIf `true`, the default search excludes `id`, `cells` fields and\nonly does a basic comparison on string and number type cell values.\n\nTo implement your own client-side filtering, pass a function\nthat accepts a row and value and returns a boolean.", + "type": "boolean | ((rows: import(\"./DataTable.svelte\").DataTableRow, value: number | string) => boolean)", + "value": "false", + "isFunction": false, + "isFunctionDeclaration": false, + "constant": false, + "reactive": false + }, { "name": "tabindex", "kind": "let", diff --git a/docs/src/pages/components/DataTable.svx b/docs/src/pages/components/DataTable.svx index 9da963e0..fe68f893 100644 --- a/docs/src/pages/components/DataTable.svx +++ b/docs/src/pages/components/DataTable.svx @@ -464,6 +464,22 @@ title="Load balancers" description="Your organization's active load balancers." +### Filterable + +By default, `ToolbarSearch` will not filter `DataTable` rows. + +Set `shouldFilterRows` to `true` to enable client-side filtering. The default filtering performs a basic string comparison on cell values that are of a string or a number type. + +Note that in-memory filtering is not optimal for large data sets, where you might consider using server-side search. + + + +### Filterable (custom) + +`shouldFilterRows` also accepts a function and passes it the current row and value. It expects the function to return a boolean. + + + ### Zebra stripes + import { + DataTable, + Toolbar, + ToolbarContent, + ToolbarSearch, + ToolbarMenu, + ToolbarMenuItem, + Button, + } from "carbon-components-svelte"; + + let rows = Array.from({ length: 10 }).map((_, i) => ({ + id: i, + name: "Load Balancer " + (i + 1), + protocol: "HTTP", + port: 3000 + i * 10, + rule: i % 2 ? "Round robin" : "DNS delegation", + })); + + + + + + + + Restart all + + API documentation + + Stop all + + + + + diff --git a/docs/src/pages/framed/DataTable/DataTableFilterable.svelte b/docs/src/pages/framed/DataTable/DataTableFilterable.svelte new file mode 100644 index 00000000..3d80b057 --- /dev/null +++ b/docs/src/pages/framed/DataTable/DataTableFilterable.svelte @@ -0,0 +1,46 @@ + + + + + + + + Restart all + + API documentation + + Stop all + + + + + diff --git a/src/DataTable/DataTable.svelte b/src/DataTable/DataTable.svelte index 7d0ae252..6f9ff031 100644 --- a/src/DataTable/DataTable.svelte +++ b/src/DataTable/DataTable.svelte @@ -140,6 +140,7 @@ sortDirection: "none", }); const headerItems = writable([]); + const tableRows = writable(rows); const thKeys = derived(headerItems, () => headers .map(({ key }, i) => ({ key, id: key })) @@ -155,6 +156,7 @@ sortHeader, tableSortable, batchSelectedIds, + tableRows, resetSelectedRowIds: () => { selectAll = false; selectedRowIds = []; @@ -173,7 +175,7 @@ let refSelectAll = null; $: batchSelectedIds.set(selectedRowIds); - $: rowIds = rows.map((row) => row.id); + $: rowIds = $tableRows.map((row) => row.id); $: expandableRowIds = rowIds.filter( (id) => !nonExpandableRowIds.includes(id) ); @@ -193,23 +195,25 @@ $: if (radio || batchSelection) selectable = true; $: tableSortable.set(sortable); $: headerKeys = headers.map(({ key }) => key); - $: rows = rows.map((row) => ({ - ...row, - cells: headerKeys.map((key, index) => ({ - key, - value: resolvePath(row, key), - display: headers[index].display, - })), - })); - $: sortedRows = rows; + $: tableRows.set( + rows.map((row) => ({ + ...row, + cells: headerKeys.map((key, index) => ({ + key, + value: resolvePath(row, key), + display: headers[index].display, + })), + })) + ); + $: sortedRows = [...$tableRows]; $: ascending = $sortHeader.sortDirection === "ascending"; $: sortKey = $sortHeader.key; $: sorting = sortable && sortKey != null; $: if (sorting) { if ($sortHeader.sortDirection === "none") { - sortedRows = rows; + sortedRows = $tableRows; } else { - sortedRows = [...rows].sort((a, b) => { + sortedRows = [...$tableRows].sort((a, b) => { const itemA = ascending ? resolvePath(a, sortKey, "") : resolvePath(b, sortKey, ""); @@ -236,7 +240,7 @@ page && pageSize ? rows.slice((page - 1) * pageSize, page * pageSize) : rows; - $: displayedRows = getDisplayedRows(rows, page, pageSize); + $: displayedRows = getDisplayedRows($tableRows, page, pageSize); $: displayedSortedRows = getDisplayedRows(sortedRows, page, pageSize); diff --git a/src/DataTable/ToolbarSearch.svelte b/src/DataTable/ToolbarSearch.svelte index 21c0c256..50ac8c6d 100644 --- a/src/DataTable/ToolbarSearch.svelte +++ b/src/DataTable/ToolbarSearch.svelte @@ -16,6 +16,18 @@ /** Set to `true` to disable the search bar */ export let disabled = false; + /** + * Set to `true` to filter table rows using the search value. + * + * If `true`, the default search excludes `id`, `cells` fields and + * only does a basic comparison on string and number type cell values. + * + * To implement your own client-side filtering, pass a function + * that accepts a row and value and returns a boolean. + * @type {boolean | ((rows: import("./DataTable.svelte").DataTableRow, value: number | string) => boolean)} + */ + export let shouldFilterRows = false; + /** Specify the tabindex */ export let tabindex = "0"; @@ -25,9 +37,36 @@ */ export let ref = null; - import { tick } from "svelte"; + import { tick, getContext } from "svelte"; import Search from "../Search/Search.svelte"; + const { tableRows } = getContext("DataTable"); + + $: originalRows = tableRows ? [...$tableRows] : []; + $: if (shouldFilterRows) { + let rows = originalRows; + + if (value.trim().length > 0) { + if (shouldFilterRows === true) { + rows = rows.filter((row) => { + return Object.entries(row) + .filter(([key]) => !["cells", "id"].includes(key)) + .some(([key, _value]) => { + if (typeof _value === "string" || typeof _value === "number") { + return (_value + "") + ?.toLowerCase() + .includes(value.trim().toLowerCase()); + } + }); + }); + } else if (typeof shouldFilterRows === "function") { + rows = rows.filter((row) => shouldFilterRows(row, value) ?? false); + } + } + + tableRows.set(rows); + } + async function expandSearch() { if (disabled || persistent || expanded) return; expanded = true; diff --git a/tests/DataTable.test.svelte b/tests/DataTable.test.svelte index 1545f3d7..67af1f16 100644 --- a/tests/DataTable.test.svelte +++ b/tests/DataTable.test.svelte @@ -109,7 +109,11 @@ > - + Restart all diff --git a/types/DataTable/ToolbarSearch.svelte.d.ts b/types/DataTable/ToolbarSearch.svelte.d.ts index b3ced787..83653277 100644 --- a/types/DataTable/ToolbarSearch.svelte.d.ts +++ b/types/DataTable/ToolbarSearch.svelte.d.ts @@ -27,6 +27,23 @@ export interface ToolbarSearchProps */ disabled?: boolean; + /** + * Set to `true` to filter table rows using the search value. + * + * If `true`, the default search excludes `id`, `cells` fields and + * only does a basic comparison on string and number type cell values. + * + * To implement your own client-side filtering, pass a function + * that accepts a row and value and returns a boolean. + * @default false + */ + shouldFilterRows?: + | boolean + | (( + rows: import("./DataTable.svelte").DataTableRow, + value: number | string + ) => boolean); + /** * Specify the tabindex * @default "0"