Dynamic filters are not working as expected. The identifier should not be a function parsed to a string.
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
There is another solution for this?
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]"
@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...
@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);
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
Cool. Thank you so much for your help. I'll close this.
@vincjo, sorry for open it again. Just want to ask if is it the same case with the sort function?
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>
[...]
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)
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.