vincjo / datatables

A toolkit for creating datatable components with Svelte
https://vincjo.fr/datatables
MIT License
363 stars 15 forks source link

Dynamic filters are not working as expected. The identifier should not be a function parsed to a string. #83

Closed marioaldairsr closed 1 month ago

marioaldairsr commented 5 months ago

I have an application that displays a table with the keys name of an object, and it needs to be dynamically filtered based on those keys but it doesn't work as expected.

To replicate:

Main.svelte

<script>
    import { DataHandler } from '@vincjo/datatables';
    import ThFilter from './ThFilter.svelte';

    const data = [
        {
            name: 'Mario',
            status: 'DRAFT',
            ammount: 41
        },
        {
            name: 'George',
            status: 'OPEN',
            ammount: 54
        },
        {
            name: 'Mario',
            status: 'OPEN',
            ammount: 31
        }
    ];

    const handler = new DataHandler(data, { rowsPerPage: 3 });
    const rows = handler.getRows();

    const keys = Object.keys(data[0]); // ['name', 'status', 'ammount']
    $: console.log(keys);
</script>

<table>
    <thead>
        <tr>
            {#each keys as key}
                <th>{key}</th>
            {/each}
        </tr>
        <tr>
            {#each keys as key}
                <ThFilter {handler} {key} />
            {/each}
        </tr>
    </thead>
    <tbody>
        {#each $rows as row}
            <tr>
                {#each keys as key}
                    <td>{row[key]}</td>
                {/each}
            </tr>
        {/each}
    </tbody>
</table>

ThFilter.svelte

<script lang="ts">
    import type { DataHandler } from '@vincjo/datatables';
    export let handler: DataHandler;
    export let key: any = '';
    let value = '';
</script>

<th>
    <input
        type="text"
        placeholder="Filter"
        bind:value
        on:input={() => handler.filter(value, (row) => row[key])}
    />
</th>

This is not working as expected; every time I filter, it resets everything.

I was looking into the package's source code and I found that it parses the function to a string and sets it as an identifier. So, if I put the function as (row) => row[key], that becomes the identifier. When it tries to filter, it replaces the last function.

utils.js image

There is another solution for this?

vincjo commented 5 months ago

In ThFilter.svelte, since the key is the variable, you should pass it directly as the filter param rather than using a callback:

handler.filter(value, key)

instead of:

handler.filter(value, (row) => row[key])

Then, as you mentioned, the parseField() function will determine that "key" is of type "string", and will create the callback. The identifier will contain the actual value of "key" instead of "row[key]"

marioaldairsr commented 5 months ago

@vincjo, thanks for your reply.

We have a use case where we could have a nested object like this:

const data = [
    {
        name: 'Mario',
        status: 'DRAFT',
        ammount: 41,
        job: {
            id: 5,
            name: 'Developer'
        }
    },
    {
        name: 'George',
        status: 'OPEN',
        ammount: 54,
        job: {
            id: 6,
            name: 'Sales'
        }
    },
    {
        name: 'Mario',
        status: 'OPEN',
        ammount: 31,
        job: {
            id: 7,
            name: 'Engineer'
        }
    }
];

And this is a model that can have more or fewer key properties; it's dynamic, that's why can't have a custom filter for each of them. So we are using the callback like this:

(row) => getPropertyValue(row, key)

where key could be 'jobs.id', 'name', 'jobs.name', etc...

marioaldairsr commented 5 months ago

@vincjo, I was able to solve the problem with the following code, creating a dynamic function to ensure that when converted to a string it's always unique:

    function createNamedFunction(key) {
        return new Function('row', `
        function getValueByPath(obj, path) {
            return path.split('.').reduce((currentObject, key) => {
                return currentObject ? currentObject[key] : undefined;
            }, obj);
        }
        return getValueByPath(row, '${key}');`);
    }

   handler.filter(value, createNamedFunction(key), check.contains);
vincjo commented 5 months ago

Oh, ok. Nice

I also added a workaround in version 1.14.3 (this library needs a better way to name filters). You can choose what best suits your needs.

In ThFilter.svelte

<script lang="ts">
    import type {  DataHandler } from '$lib';
    export let handler: DataHandler;
    export let key: any = '';
    let value = '';
    const filter = handler.createFilter((row) => row[key])
</script>

<th>
    <input
        type="text"
        placeholder="Filter"
        bind:value
        on:input={() => filter.set(value)}
    />
</th>

Instead of handler.filter() shortcut, you declare a new filter instance, by using handler.createFilter(). This instance creates a random string as a unique identifier.

Thanks for raising this point

marioaldairsr commented 5 months ago

Cool. Thank you so much for your help. I'll close this.

marioaldairsr commented 5 months ago

@vincjo, sorry for open it again. Just want to ask if is it the same case with the sort function?

vincjo commented 5 months ago

Indeed, didn't think about it. I added a workaround for column sorting, where you can pass an identifier to the sort method.

Released in 1.14.4.

Examples

Using handler instance:

handler.sort(orderBy, identifier)

Using <Th> component:

<Th orderBy={(row) => row[key]} identifier="th0"/>

Using your code example:

<table>
    <thead>
        <tr>
            {#each keys as key, i}
                <Th {handler} orderBy={key} identifier={'th' + i}>{key}</Th>
            {/each}
        </tr>
[...]
marioaldairsr commented 5 months ago

Okay, it looks nice. Thank you. Is there a way to have multiple sorts?

For example:

Sort by id asc Then by amount desc Then by date asc

To solve this, I just have to create a custom sort function and then reset the rows with handler.setRows(sortedRows)

vincjo commented 5 months ago

There is a way:

handler.sortAsc('id')
handler.sortDesc('amount')
handler.sortAsc('date')

(You can add an identifier as a second parameter)

When you update rows using setRows(), last 3 sorts are played automatically.