Search code examples
javascriptsveltesvelte-3svelte-store

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

  • 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