huntabyte / shadcn-svelte

shadcn/ui, but for Svelte. ✨
https://next.shadcn-svelte.com
MIT License
5.42k stars 339 forks source link

Data table becomes empty when email column removed #1117

Closed brucegl closed 5 months ago

brucegl commented 5 months ago

Describe the bug

Hi,

I followed the guide in the Data Table section and when I hide the email column then all the rows are removed and the row selection checkbox is checked.

If I then remove the following code from the id table column plugin section:

                filter: {
                    exclude: true
                }

which is as you've done in the Data Table preview/code section, then I can hide the email column ok, HOWEVER, this then means that the id values are included in the filter email searches (you can see this by typing m5 in the filter email box in the Data Table preview section).

Reproduction

data-table.svelte

<script lang="ts">
    import ArrowUpDown from 'lucide-svelte/icons/arrow-up-down';
    import ChevronDown from 'lucide-svelte/icons/chevron-down';
    import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table';
    import {
        addHiddenColumns,
        addPagination,
        addSelectedRows,
        addSortBy,
        addTableFilter
    } from 'svelte-headless-table/plugins';
    import { readable } from 'svelte/store';
    import DataTableActions from './data-table-actions.svelte';
    import DataTableCheckbox from './data-table-checkbox.svelte';
    import * as Table from '$lib/components/ui/table';
    import { Button } from '$lib/components/ui/button';
    import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
    import { Input } from '$lib/components/ui/input';

    type Payment = {
        id: string;
        amount: number;
        status: 'Pending' | 'Processing' | 'Success' | 'Failed';
        email: string;
    };

    const data: Payment[] = [
        {
            id: 'm5gr84i9',
            amount: 316,
            status: 'Success',
            email: 'ken99@yahoo.com'
        }
        // ...
    ];

    const table = createTable(readable(data), {
        page: addPagination(),
        sort: addSortBy({ disableMultiSort: true }),
        filter: addTableFilter({
            fn: ({ filterValue, value }) => value.includes(filterValue)
        }),
        hide: addHiddenColumns(),
        select: addSelectedRows()
    });

    const columns = table.createColumns([
        table.column({
            accessor: 'id',
            header: (_, { pluginStates }) => {
                const { allPageRowsSelected } = pluginStates.select;
                return createRender(DataTableCheckbox, {
                    checked: allPageRowsSelected
                });
            },
            cell: ({ row }, { pluginStates }) => {
                const { getRowState } = pluginStates.select;
                const { isSelected } = getRowState(row);

                return createRender(DataTableCheckbox, {
                    checked: isSelected
                });
            },
            plugins: {
                sort: {
                    disable: true
                },
                filter: {
                    exclude: true
                }
            }
        }),
        table.column({
            accessor: 'status',
            header: 'Status',
            plugins: {
                sort: {
                    disable: true
                },
                filter: {
                    exclude: true
                }
            }
        }),
        table.column({
            accessor: 'email',
            header: 'Email'
        }),
        table.column({
            accessor: 'amount',
            header: 'Amount',
            cell: ({ value }) => {
                const formatted = new Intl.NumberFormat('en-US', {
                    style: 'currency',
                    currency: 'USD'
                }).format(value);
                return formatted;
            },
            plugins: {
                sort: {
                    disable: true
                },
                filter: {
                    exclude: true
                }
            }
        }),
        table.column({
            accessor: ({ id }) => id,
            header: '',
            cell: (item) => {
                return createRender(DataTableActions, { id: item.value });
            },
            plugins: {
                sort: {
                    disable: true
                },
                filter: {
                    exclude: true
                }
            }
        })
    ]);

    const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates, flatColumns, rows } =
        table.createViewModel(columns);

    const { pageIndex, hasNextPage, hasPreviousPage } = pluginStates.page;
    const { filterValue } = pluginStates.filter;
    const { hiddenColumnIds } = pluginStates.hide;
    const { selectedDataIds } = pluginStates.select;

    const { sortKeys } = pluginStates.sort;

    const ids = flatColumns.map((col) => col.id);
    let hideForId = Object.fromEntries(ids.map((id) => [id, true]));

    $: $hiddenColumnIds = Object.entries(hideForId)
        .filter(([, hide]) => !hide)
        .map(([id]) => id);

    const hideableCols = ['status', 'email', 'amount'];
</script>

<div>
    <div class="flex items-center py-4">
        <Input class="max-w-sm" placeholder="Filter emails..." type="text" bind:value={$filterValue} />
        <DropdownMenu.Root>
            <DropdownMenu.Trigger asChild let:builder>
                <Button variant="outline" class="ml-auto" builders={[builder]}>
                    Columns <ChevronDown class="ml-2 h-4 w-4" />
                </Button>
            </DropdownMenu.Trigger>
            <DropdownMenu.Content>
                {#each flatColumns as col}
                    {#if hideableCols.includes(col.id)}
                        <DropdownMenu.CheckboxItem bind:checked={hideForId[col.id]}>
                            {col.header}
                        </DropdownMenu.CheckboxItem>
                    {/if}
                {/each}
            </DropdownMenu.Content>
        </DropdownMenu.Root>
    </div>
    <div class="rounded-md border">
        <Table.Root {...$tableAttrs}>
            <Table.Header>
                {#each $headerRows as headerRow}
                    <Subscribe rowAttrs={headerRow.attrs()}>
                        <Table.Row>
                            {#each headerRow.cells as cell (cell.id)}
                                <Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
                                    <Table.Head {...attrs} class="[&:has([role=checkbox])]:pl-3">
                                        {#if cell.id === 'amount'}
                                            <div class="text-right">
                                                <Render of={cell.render()} />
                                            </div>
                                        {:else if cell.id === 'email'}
                                            <Button variant="ghost" on:click={props.sort.toggle}>
                                                <Render of={cell.render()} />
                                                <ArrowUpDown class={'ml-2 h-4 w-4'} />
                                            </Button>
                                        {:else}
                                            <Render of={cell.render()} />
                                        {/if}
                                    </Table.Head>
                                </Subscribe>
                            {/each}
                        </Table.Row>
                    </Subscribe>
                {/each}
            </Table.Header>
            <Table.Body {...$tableBodyAttrs}>
                {#each $pageRows as row (row.id)}
                    <Subscribe rowAttrs={row.attrs()} let:rowAttrs>
                        <Table.Row {...rowAttrs} data-state={$selectedDataIds[row.id] && 'selected'}>
                            {#each row.cells as cell (cell.id)}
                                <Subscribe attrs={cell.attrs()} let:attrs>
                                    <Table.Cell {...attrs} class="[&:has([role=checkbox])]:pl-3">
                                        {#if cell.id === 'amount'}
                                            <div class="text-right font-medium">
                                                <Render of={cell.render()} />
                                            </div>
                                        {:else if cell.id === 'status'}
                                            <div class="capitalize">
                                                <Render of={cell.render()} />
                                            </div>
                                        {:else}
                                            <Render of={cell.render()} />
                                        {/if}
                                    </Table.Cell>
                                </Subscribe>
                            {/each}
                        </Table.Row>
                    </Subscribe>
                {/each}
            </Table.Body>
        </Table.Root>
    </div>
    <div class="flex items-center justify-end space-x-4 py-4">
        <div class="flex-1 text-sm text-muted-foreground">
            {Object.keys($selectedDataIds).length} of{' '}
            {$rows.length} row(s) selected.
        </div>
        <Button
            variant="outline"
            size="sm"
            on:click={() => ($pageIndex = $pageIndex - 1)}
            disabled={!$hasPreviousPage}>Previous</Button
        >
        <Button
            variant="outline"
            size="sm"
            disabled={!$hasNextPage}
            on:click={() => ($pageIndex = $pageIndex + 1)}>Next</Button
        >
    </div>
</div>

and then with the id filter not excluded...

<script lang="ts">
    import ArrowUpDown from 'lucide-svelte/icons/arrow-up-down';
    import ChevronDown from 'lucide-svelte/icons/chevron-down';
    import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table';
    import {
        addHiddenColumns,
        addPagination,
        addSelectedRows,
        addSortBy,
        addTableFilter
    } from 'svelte-headless-table/plugins';
    import { readable } from 'svelte/store';
    import DataTableActions from './data-table-actions.svelte';
    import DataTableCheckbox from './data-table-checkbox.svelte';
    import * as Table from '$lib/components/ui/table';
    import { Button } from '$lib/components/ui/button';
    import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
    import { Input } from '$lib/components/ui/input';

    type Payment = {
        id: string;
        amount: number;
        status: 'Pending' | 'Processing' | 'Success' | 'Failed';
        email: string;
    };

    const data: Payment[] = [
        {
            id: 'm5gr84i9',
            amount: 316,
            status: 'Success',
            email: 'ken99@yahoo.com'
        }
        // ...
    ];

    const table = createTable(readable(data), {
        page: addPagination(),
        sort: addSortBy({ disableMultiSort: true }),
        filter: addTableFilter({
            fn: ({ filterValue, value }) => value.includes(filterValue)
        }),
        hide: addHiddenColumns(),
        select: addSelectedRows()
    });

    const columns = table.createColumns([
        table.column({
            accessor: 'id',
            header: (_, { pluginStates }) => {
                const { allPageRowsSelected } = pluginStates.select;
                return createRender(DataTableCheckbox, {
                    checked: allPageRowsSelected
                });
            },
            cell: ({ row }, { pluginStates }) => {
                const { getRowState } = pluginStates.select;
                const { isSelected } = getRowState(row);

                return createRender(DataTableCheckbox, {
                    checked: isSelected
                });
            },
            plugins: {
                sort: {
                    disable: true
                },
                filter: {
                    exclude: true
                }
            }
        }),
        table.column({
            accessor: 'status',
            header: 'Status',
            plugins: {
                sort: {
                    disable: true
                },
                filter: {
                    exclude: true
                }
            }
        }),
        table.column({
            accessor: 'email',
            header: 'Email'
        }),
        table.column({
            accessor: 'amount',
            header: 'Amount',
            cell: ({ value }) => {
                const formatted = new Intl.NumberFormat('en-US', {
                    style: 'currency',
                    currency: 'USD'
                }).format(value);
                return formatted;
            },
            plugins: {
                sort: {
                    disable: true
                },
                filter: {
                    exclude: true
                }
            }
        }),
        table.column({
            accessor: ({ id }) => id,
            header: '',
            cell: (item) => {
                return createRender(DataTableActions, { id: item.value });
            },
            plugins: {
                sort: {
                    disable: true
                }
            }
        })
    ]);

    const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates, flatColumns, rows } =
        table.createViewModel(columns);

    const { pageIndex, hasNextPage, hasPreviousPage } = pluginStates.page;
    const { filterValue } = pluginStates.filter;
    const { hiddenColumnIds } = pluginStates.hide;
    const { selectedDataIds } = pluginStates.select;

    const { sortKeys } = pluginStates.sort;

    const ids = flatColumns.map((col) => col.id);
    let hideForId = Object.fromEntries(ids.map((id) => [id, true]));

    $: $hiddenColumnIds = Object.entries(hideForId)
        .filter(([, hide]) => !hide)
        .map(([id]) => id);

    const hideableCols = ['status', 'email', 'amount'];
</script>

<div>
    <div class="flex items-center py-4">
        <Input class="max-w-sm" placeholder="Filter emails..." type="text" bind:value={$filterValue} />
        <DropdownMenu.Root>
            <DropdownMenu.Trigger asChild let:builder>
                <Button variant="outline" class="ml-auto" builders={[builder]}>
                    Columns <ChevronDown class="ml-2 h-4 w-4" />
                </Button>
            </DropdownMenu.Trigger>
            <DropdownMenu.Content>
                {#each flatColumns as col}
                    {#if hideableCols.includes(col.id)}
                        <DropdownMenu.CheckboxItem bind:checked={hideForId[col.id]}>
                            {col.header}
                        </DropdownMenu.CheckboxItem>
                    {/if}
                {/each}
            </DropdownMenu.Content>
        </DropdownMenu.Root>
    </div>
    <div class="rounded-md border">
        <Table.Root {...$tableAttrs}>
            <Table.Header>
                {#each $headerRows as headerRow}
                    <Subscribe rowAttrs={headerRow.attrs()}>
                        <Table.Row>
                            {#each headerRow.cells as cell (cell.id)}
                                <Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
                                    <Table.Head {...attrs} class="[&:has([role=checkbox])]:pl-3">
                                        {#if cell.id === 'amount'}
                                            <div class="text-right">
                                                <Render of={cell.render()} />
                                            </div>
                                        {:else if cell.id === 'email'}
                                            <Button variant="ghost" on:click={props.sort.toggle}>
                                                <Render of={cell.render()} />
                                                <ArrowUpDown class={'ml-2 h-4 w-4'} />
                                            </Button>
                                        {:else}
                                            <Render of={cell.render()} />
                                        {/if}
                                    </Table.Head>
                                </Subscribe>
                            {/each}
                        </Table.Row>
                    </Subscribe>
                {/each}
            </Table.Header>
            <Table.Body {...$tableBodyAttrs}>
                {#each $pageRows as row (row.id)}
                    <Subscribe rowAttrs={row.attrs()} let:rowAttrs>
                        <Table.Row {...rowAttrs} data-state={$selectedDataIds[row.id] && 'selected'}>
                            {#each row.cells as cell (cell.id)}
                                <Subscribe attrs={cell.attrs()} let:attrs>
                                    <Table.Cell {...attrs} class="[&:has([role=checkbox])]:pl-3">
                                        {#if cell.id === 'amount'}
                                            <div class="text-right font-medium">
                                                <Render of={cell.render()} />
                                            </div>
                                        {:else if cell.id === 'status'}
                                            <div class="capitalize">
                                                <Render of={cell.render()} />
                                            </div>
                                        {:else}
                                            <Render of={cell.render()} />
                                        {/if}
                                    </Table.Cell>
                                </Subscribe>
                            {/each}
                        </Table.Row>
                    </Subscribe>
                {/each}
            </Table.Body>
        </Table.Root>
    </div>
    <div class="flex items-center justify-end space-x-4 py-4">
        <div class="flex-1 text-sm text-muted-foreground">
            {Object.keys($selectedDataIds).length} of{' '}
            {$rows.length} row(s) selected.
        </div>
        <Button
            variant="outline"
            size="sm"
            on:click={() => ($pageIndex = $pageIndex - 1)}
            disabled={!$hasPreviousPage}>Previous</Button
        >
        <Button
            variant="outline"
            size="sm"
            disabled={!$hasNextPage}
            on:click={() => ($pageIndex = $pageIndex + 1)}>Next</Button
        >
    </div>
</div>

Logs

No response

System Info

System:
    OS: Linux 5.15 Linux Mint 21.1 (Vera)
    CPU: (16) x64 12th Gen Intel(R) Core(TM) i7-1260P
    Memory: 21.88 GB / 30.59 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 18.14.2 - /opt/node-v18.14.2-linux-x64/bin/node
    npm: 9.7.2 - /opt/node-v18.14.2-linux-x64/bin/npm
  Browsers:
    Chrome: 125.0.6422.112
  npmPackages:
    @sveltejs/kit: ^2.0.0 => 2.5.1 
    bits-ui: ^0.18.6 => 0.18.6 
    formsnap: ^0.5.1   System:
    OS: Linux 5.15 Linux Mint 21.1 (Vera)
    CPU: (16) x64 12th Gen Intel(R) Core(TM) i7-1260P
    Memory: 21.88 GB / 30.59 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 18.14.2 - /opt/node-v18.14.2-linux-x64/bin/node
    npm: 9.7.2 - /opt/node-v18.14.2-linux-x64/bin/npm
  Browsers:
    Chrome: 125.0.6422.112
  npmPackages:
    @sveltejs/kit: ^2.0.0 => 2.5.1 
    bits-ui: ^0.18.6 => 0.18.6 
    formsnap: ^0.5.1 => 0.5.1 
    lucide-svelte: ^0.379.0 => 0.379.0 
    svelte: ^4.2.7 => 4.2.12 
    svelte-radix: ^1.1.0 => 1.1.0 
    svelte-sonner: ^0.3.19 => 0.3.19 
    sveltekit-superforms: ^2.10.6 => 2.10.6 => 0.5.1 
    lucide-svelte: ^0.379.0 => 0.379.0 
    svelte: ^4.2.7 => 4.2.12 
    svelte-radix: ^1.1.0 => 1.1.0 
    svelte-sonner: ^0.3.19 => 0.3.19 
    sveltekit-superforms: ^2.10.6 => 2.10.6

Severity

annoyance

brucegl commented 5 months ago

I think I've figured out what the issue is. If we exclude the filter for the id (as shown in the tutorial code - first example above), then when the column is hidden, there is no longer any data to filter on, hence all the rows are removed from the table!

To overcome this, add the "includeHiddenColumns" option to the table filter as below:

    const table = createTable(readable(data), {
        page: addPagination(),
        sort: addSortBy({ disableMultiSort: true }),
        filter: addTableFilter({
            includeHiddenColumns: true,
            fn: ({ filterValue, value }) => value.includes(filterValue)
        }),
        hide: addHiddenColumns(),
        select: addSelectedRows()
    });

Could someone add this to the tutorial code please (and make the adjustments to the preview/code section also). Thanks

github-actions[bot] commented 5 months ago

Please provide a reproduction.

More info ### Why do I need to provide a reproduction? This project is maintained by a very small team, and we simply don't have the bandwidth to investigate issues that we can't easily replicate. Reproductions enable us to fix issues faster and more efficiently. If you care about getting your issue resolved, providing a reproduction is the best way to do that. ### I've provided a reproduction - what happens now? Once a reproduction is provided, we'll remove the `needs reproduction` label and review the issue to determine how to resolve it. If we can confirm it's a bug, we'll label it as such and prioritize it based on its severity. If `needs reproduction` labeled issues don't receive any activity (e.g., a comment with a reproduction link), they'll be closed. Feel free to comment with a reproduction at any time and the issue will be reopened. ### How can I create a reproduction? You can use [this template](https://shadcn-svelte.com/repro) to create a minimal reproduction. You can also link to a GitHub repository with the reproduction. Please ensure that the reproduction is as **minimal** as possible. If there is a ton of custom logic in your reproduction, it is difficult to determine if the issue is with your code or with the library. The more minimal the reproduction, the more likely it is that we'll be able to assist. You might also find these other articles interesting and/or helpful: - [The Importance of Reproductions](https://antfu.me/posts/why-reproductions-are-required) - [How to Generate a Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/mcve)
github-actions[bot] commented 5 months ago

This issue was closed because it was open for 7 days without a reproduction.