'How to make a cumulative filter system in svelte (like amazon sidebar)
I have a svelte page to show a list of post, I need to give the user the ability to filter that list by several conditions, eg: year, tags, category, etc. All those conditions should be concurrent, just like the amazon website sidebar.
Using a derived store I got one filter working fine, but still I can't manage to get 2 or 3 filters applied at the same time.
For the "year" filter I have the following code:
import { readable, writable, derived } from "svelte/store";
export let data;
let posts = readable(data.posts);
const extractColumn = (arr, column) => arr.map((x) => x[column]);
const allYears = extractColumn(data.posts, "year");
const years = readable ([...new Set(allYears)]);
const filter = writable({ year: null });
const filteredPosts = derived([filter, posts], ([$filter, $posts]) => {
if (!$filter.year) return $posts;
return $posts.filter((x) => x.year === $filter.year);
});
So, based in the posts list I can get the years array for the filter and then with a simple select trigger the filtering action, and this work pretty fine.
<select bind:value={$filter.year}>
{#each $years as year}
<option value={year}>{year}</option>
{/each}
</select>
As I said at the beginning, the point is how to add complexity to this scenario to turn a simple filter into a system, with a variable number of dynamic cumulative filters.
Thanks for any idea.
Solution 1:[1]
To make this more universal I would start by 'automatically' generating the filter options
const filterOptions = derived(posts, $posts => {
// set the columns you'd like to have a filter for
const columns = ['year', 'language']
// const columns = Object.keys($posts[0]) // or change to this to have a filter for every property
return columns.reduce((filterOptions, column) => {
filterOptions[column] = extractUniqueColumnValues($posts, column)
return filterOptions
}, {})
})
function extractUniqueColumnValues (arr, column) {
return Array.from(new Set(arr.map((x) => x[column])))
}
and display them like this
{#each Object.entries($filterOptions) as [column, values]}
{column}
<select bind:value={$filter[column]}>
<option value={null} ></option>
{#each values as value}
<option value={value}>{value}</option>
{/each}
</select>
{/each}
filter
can simply be
const filter = writable({});
(It is initially set to {year: null, language: null}
via the select value bindings)
One way to filter the posts could be this (or using reduce
like Stephane Vanraes` answer is showing)
const filteredPosts = derived([filter, posts], ([$filter, $posts]) => {
Object.entries($filter).forEach(([key, value]) => {
if(value !== null) $posts = $posts.filter(x => x[key] == value)
})
return $posts
});
Here's a REPL built on this
Solution 2:[2]
You can loop over the props of an object with Object.entries
Combine that with bracket notation to read props obj[prop]
Throw in an array reduce to make it a simpler and you get:
return Object.entries($filter)
.reduce(
(pre, [key, value]) => pre.filter(x => x[key] == value),
$posts
);
As an extra you could actually stores arrays in your filter instead so you can filter on multiple values:
{
"year": [2022],
"colour": ['red', 'green']
}
In that case use pre.filter(x => values.includes(x[key]))
instead.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Corrl |
Solution 2 | Stephane Vanraes |