Tatask

Building a dynamically filterable list component in Svelte

Max Girkins

3 minute read

8 months ago

Setup

We need some example tasks so you can see what sort of data structure we’re working with.

{
    text: 'Buy milk',
    tags: ['shopping', 'home']
},
{
    text: 'Repaint fence',
    tags: ['home', 'long']
},
{
    text: 'Redesign landing page',
    tags: ['design', 'long']
}

We’ll store those in a tasks array.

We need a $: all_tags array to display the tags to filter by. We can set this using the following:

$: all_tags = [...new Set(tasks.map((x) => x.tags).flat())];

And finally, we need a filters data structure for which I’ll use a Map.

let filters = new Map();

Displaying the list

Here’s the basic structure of the list, note the filtered array is being referenced instead of the tasks array. We’ll need that in the next step.

{#each filtered as task}
	<div class="task">
		<p>{task.text}</p>
		{#each task.tags as tag}
			<p>{tag}</p>
		{/each}
	</div>
{/each}

Hooking up the filtered array

We again make use of Svelte’s reactive $: variables like so:

$: filtered = tasks.filter(
	(x) =>
		(filters['tags'] != null &&
			Object.values(filters['tags']).every((func) => func == null || func(x))) ||
		!filters['tags']
);

It’s not super straightforward so let me explain. We need to filter the tasks array by whatever filters are set in the filters Map. To do that we first check if the tags filter is set, then if it is set we run our task through every filter the tags map contains and check if the task produces a truthy value or not. If the tags filter is not set then we return true for this part of the filtering.

The dynamic bit

The above code is all fairly standard, I think the clever bit is how we dynamically create filtering functions. Here’s some code and I’ll explain it afterwards.

{#each all_tags as tag}
	<button
		class="tag"
		on:click={() => {
			if (filters['tags'] == null) filters['tags'] = new Map();
			if (filters['tags'][tag] == null) {
				let f = (x) => {
					return x.tags != null && x.tags.includes(tag) != null;
				};
				// This is how you give the above function a dynamic
				// name so we can edit/delete it in the filters Map
				Object.defineProperty(f, 'name', {
					value: tag,
					writable: false
				});
				filters['tags'][tag] = f;
			} else {
				// Remove this tag filter if you click the tag again
				filters['tags'][tag] = null;
			}
		}}>{tag}</button
	>
{/each}

This code simply produces a list of clickable tags. When you click a tag it adds the tag’s filter to the filters Map as filters["tag_name"] which will then filter the task list. We can do this as we are filtering the task list using every() which ensures that all added filters are run. We need to name the function so that we can remove the filter by clicking the tag button again while keeping any other added tags.

When you click on a tag you’re essentially turning the task filtering function into this:

$: filtered = tasks.filter((x) => x.tags != null && x.tags.includes(tag));

You can of course add other filters as well, in Tatask you can filter by due dates, tags, assigned_to etc. You simply need to add further logical expressions to the filtered array.

Conclusion

I hope this tutorial is helpful, there was some demand for it on Reddit after I shared a demo video of filtering in Tatask that I added on Wednesday. I’ve never written a tutorial before so I’m not sure how intelligible or otherwise it is. Please reach out to me on Twitter if you have any suggestions or improvements. And if you want to see a more complete version of this filtering in action then do give Tatask a try. It’s an incredibly powerful yet surprisingly simple way of managing tasks.