vincjo / datatables

A toolkit for creating datatable components with Svelte
https://vincjo.fr/datatables
MIT License
475 stars 18 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 5 months ago

marioaldairsr commented 10 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 10 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 10 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 10 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 10 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 10 months ago

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

marioaldairsr commented 10 months ago

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

vincjo commented 10 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 10 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 10 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.