Skip to content

Commit

Permalink
feat: implement sorting and searching in Table component (#1425)
Browse files Browse the repository at this point in the history
* Implement sorting and searching in Table component

* Update docs and component data

---------

Co-authored-by: Shinichi Okada <[email protected]>
  • Loading branch information
aarondoet and shinokada authored Sep 21, 2024
1 parent 8c4adbb commit aff76e3
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 46 deletions.
48 changes: 44 additions & 4 deletions src/lib/table/Table.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
export let color: TableColorType = 'default';
export let customeColor: string = '';
export let items: T[] = [];
export let filter: ((t: T, term: string) => boolean) | null = null;
export let placeholder: string = 'Search';
export let innerDivClass: string = 'p-4';
export let searchClass: string = 'relative mt-1';
export let svgDivClass: string = 'absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none';
export let svgClass: string = 'w-5 h-5 text-gray-500 dark:text-gray-400';
export let inputClass: string = 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-80 p-2.5 ps-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500';
let searchTerm = '';
let inputCls = twMerge(inputClass, $$props.classInput);
let svgDivCls = twMerge(svgDivClass, $$props.classSvgDiv);
const colors = {
default: 'text-gray-500 dark:text-gray-400',
Expand All @@ -29,13 +40,35 @@
$: setContext('hoverable', hoverable);
$: setContext('noborder', noborder);
$: setContext('color', color);
const sorting = writable({ items, direction: 1, sorter: '' });
setContext('sorting', sorting);
$: sorting.update(({direction, sorter}) => ({ items, direction, sorter }));
$: setContext('items', items);
const searchTermStore = writable(searchTerm);
const filterStore = writable(filter);
setContext('searchTerm', searchTermStore);
setContext('filter', filterStore);
$: searchTermStore.set(searchTerm);
$: filterStore.set(filter);
setContext('sorter', writable(null));
</script>

<div class={twJoin(divClass, shadow && 'shadow-md sm:rounded-lg')}>
{#if filter}
<slot name="search">
<div class={innerDivClass}>
<label for="table-search" class="sr-only">Search</label>
<div class={searchClass}>
<div class={svgDivCls}>
<slot name="svgSearch">
<svg class={svgClass} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
</slot>
</div>
<input bind:value={searchTerm} type="text" id="table-search" class={inputCls} {placeholder} />
</div>
<slot name="header" />
</div>
</slot>
{/if}
<table {...$$restProps} class={twMerge('w-full text-left text-sm', colors[color], $$props.class)}>
<slot />
</table>
Expand All @@ -53,4 +86,11 @@
@prop export let color: TableColorType = 'default';
@prop export let customeColor: string = '';
@prop export let items: T[] = [];
@prop export let filter: ((t: T, term: string) => boolean) | null = null;
@prop export let placeholder: string = 'Search';
@prop export let innerDivClass: string = 'p-4';
@prop export let searchClass: string = 'relative mt-1';
@prop export let svgDivClass: string = 'absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none';
@prop export let svgClass: string = 'w-5 h-5 text-gray-500 dark:text-gray-400';
@prop export let inputClass: string = 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-80 p-2.5 ps-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500';
-->
11 changes: 8 additions & 3 deletions src/lib/table/TableBody.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
export let tableBodyClass: string | undefined = undefined;
let sorting = getContext('sorting') as Writable<{ items: T[]; direction: -1 | 1; sorter: string }>;
$: items = getContext('items') as T[] || [];
let filter = getContext('filter') as Writable<((t: T, term: string) => boolean) | null>;
let searchTerm = getContext('searchTerm') as Writable<string>;
$: filtered = $filter ? items.filter(item => $filter(item, $searchTerm)) : items;
let sorter = getContext('sorter') as Writable<{id: string, sort: (a: T, b: T) => number, sortDirection: -1 | 1} | null>;
$: sorted = $sorter ? filtered.toSorted((a, b) => $sorter.sortDirection * $sorter.sort(a, b)) : filtered;
</script>

<tbody class={tableBodyClass}>
<slot />
{#each $sorting?.items || [] as item}
<slot name="row" {item} />
{#each sorted as item}
<slot name="row" {item} />
{/each}
</tbody>

Expand Down
34 changes: 15 additions & 19 deletions src/lib/table/TableHeadCell.svelte
Original file line number Diff line number Diff line change
@@ -1,39 +1,35 @@
<script lang="ts" generics="T">
import type { Writable } from 'svelte/store';
import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
import { twMerge } from 'tailwind-merge';
export let padding: string = 'px-6 py-3';
export let sort: ((a: T, b: T) => number) | undefined = undefined;
export let sort: ((a: T, b: T) => number) | null = null;
export let defaultDirection: 'asc' | 'desc' = 'asc';
export let defaultSort: boolean = false;
export let direction: 'asc' | 'desc' | null = defaultSort ? defaultDirection : null;
const sorting = getContext('sorting') as Writable<{ items: T[]; direction: -1 | 1; sorter: string }>;
const sortId = Math.random().toString(36).substring(2);
let sorter = getContext('sorter') as Writable<{id: string, sort: (a: T, b: T) => number, sortDirection: -1 | 1} | null>;
let sortId = Math.random().toString(36).substring(2);
$: direction = $sorter?.id === sortId ? $sorter.sortDirection === 1 ? 'asc' : 'desc' : null;
if(defaultSort) {
sortItems();
}
$: direction = $sorting?.sorter === sortId ? $sorting.direction === 1 ? 'asc' : 'desc' : null;
function sortItems() {
sorting.update(({ items, direction, sorter }) => {
if(!sort) return { items, direction, sorter };
if(sorter === sortId) {
direction = -direction as -1 | 1;
} else {
direction = defaultDirection === 'asc' ? 1 : -1;
}
if(!sort || !sorter) return;
sorter.update(sorter => {
return {
items: items.sort((a, b) => direction * sort(a, b)),
direction,
sorter: sortId
id: sortId,
sort,
sortDirection: (sorter?.id === sortId ? -sorter.sortDirection : defaultDirection === 'asc' ? 1 : -1) as -1 | 1
};
});
}
</script>

{#if sort && sorting}
{#if sort && sorter}
<th {...$$restProps} class={$$props.class} on:click on:focus on:keydown on:keypress on:keyup on:mouseenter on:mouseleave on:mouseover aria-sort={direction ? `${direction}ending` : undefined}>
<button class={twMerge('w-full text-left', 'after:absolute after:pl-3', direction === 'asc' && 'after:content-["▲"]', direction === 'desc' && 'after:content-["▼"]', padding)} on:click={sortItems}>
<slot />
Expand All @@ -50,7 +46,7 @@
[Go to docs](https://flowbite-svelte.com/)
## Props
@prop export let padding: string = 'px-6 py-3';
@prop export let sort: ((a: T, b: T) => number) | undefined = undefined;
@prop export let sort: ((a: T, b: T) => number) | null = null;
@prop export let defaultDirection: 'asc' | 'desc' = 'asc';
@prop export let defaultSort: boolean = false;
@prop export let direction: 'asc' | 'desc' | null = defaultSort ? defaultDirection : null;
Expand Down
2 changes: 1 addition & 1 deletion src/routes/component-data/Table.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"name":"Table","slots":[],"events":[],"props":[["divClass","string","'relative overflow-x-auto'"],["striped","boolean","false"],["hoverable","boolean","false"],["noborder","boolean","false"],["shadow","boolean","false"],["color","TableColorType","'default'"],["customeColor","string","''"],["items","T[]","[]"]]}
{"name":"Table","slots":["search","svgSearch","header"],"events":[],"props":[["divClass","string","'relative overflow-x-auto'"],["striped","boolean","false"],["hoverable","boolean","false"],["noborder","boolean","false"],["shadow","boolean","false"],["color","TableColorType","'default'"],["customeColor","string","''"],["items","T[]","[]"],["filter","((t: T, term: string) => boolean) | null = null;",""],["placeholder","string","'Search'"],["innerDivClass","string","'p-4'"],["searchClass","string","'relative mt-1'"],["svgDivClass","string","'absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none'"],["svgClass","string","'w-5 h-5 text-gray-500 dark:text-gray-400'"],["inputClass","string","'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-80 p-2.5 ps-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'"]]}
2 changes: 1 addition & 1 deletion src/routes/component-data/TableHeadCell.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"name":"TableHeadCell","slots":[],"events":["on:click","on:focus","on:keydown","on:keypress","on:keyup","on:mouseenter","on:mouseleave","on:mouseover","on:click","on:focus","on:keydown","on:keypress","on:keyup","on:mouseenter","on:mouseleave","on:mouseover"],"props":[["padding","string","'px-6 py-3'"],["sort","((a: T, b: T) => number) | undefined = undefined;",""],["defaultDirection","'asc' | 'desc'","'asc'"],["defaultSort","boolean","false"],["direction","'asc' | 'desc' | null","defaultSort ? defaultDirection : null"]]}
{"name":"TableHeadCell","slots":[],"events":["on:click","on:focus","on:keydown","on:keypress","on:keyup","on:mouseenter","on:mouseleave","on:mouseover","on:click","on:focus","on:keydown","on:keypress","on:keyup","on:mouseenter","on:mouseleave","on:mouseover"],"props":[["padding","string","'px-6 py-3'"],["sort","((a: T, b: T) => number) | null = null;",""],["defaultDirection","'asc' | 'desc'","'asc'"],["defaultSort","boolean","false"],["direction","'asc' | 'desc' | null","defaultSort ? defaultDirection : null"]]}
30 changes: 12 additions & 18 deletions src/routes/docs/components/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,44 +252,38 @@ Checkboxes can be used inside table data rows to select multiple data sets and a

```svelte example
<script>
import { Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, TableSearch } from 'flowbite-svelte';
let searchTerm = '';
import { Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from 'flowbite-svelte';
let items = [
{ id: 1, maker: 'Toyota', type: 'ABC', make: 2017 },
{ id: 2, maker: 'Ford', type: 'CDE', make: 2018 },
{ id: 3, maker: 'Volvo', type: 'FGH', make: 2019 },
{ id: 4, maker: 'Saab', type: 'IJK', make: 2020 }
];
$: filteredItems = items.filter((item) => item.maker.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1);
</script>
<TableSearch placeholder="Search by maker name" hoverable={true} bind:inputValue={searchTerm}>
<Table {items} placeholder="Search by maker name" hoverable={true} filter={(item, searchTerm) => item.maker.toLowerCase().includes(searchTerm.toLowerCase())}>
<TableHead>
<TableHeadCell>ID</TableHeadCell>
<TableHeadCell>Maker</TableHeadCell>
<TableHeadCell>Type</TableHeadCell>
<TableHeadCell>Make</TableHeadCell>
</TableHead>
<TableBody tableBodyClass="divide-y">
{#each filteredItems as item}
<TableBodyRow>
<TableBodyCell>{item.id}</TableBodyCell>
<TableBodyCell>{item.maker}</TableBodyCell>
<TableBodyCell>{item.type}</TableBodyCell>
<TableBodyCell>{item.make}</TableBodyCell>
</TableBodyRow>
{/each}
<TableBodyRow slot="row" let:item>
<TableBodyCell>{item.id}</TableBodyCell>
<TableBodyCell>{item.maker}</TableBodyCell>
<TableBodyCell>{item.type}</TableBodyCell>
<TableBodyCell>{item.make}</TableBodyCell>
</TableBodyRow>
</TableBody>
</TableSearch>
</Table>
```

## Sorting by column

```svelte example
<script>
import { Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from 'flowbite-svelte';
import { writable } from 'svelte/store';
let items = [
{ id: 1, maker: 'Toyota', type: 'ABC', make: 2017 },
{ id: 2, maker: 'Ford', type: 'CDE', make: 2018 },
Expand All @@ -305,7 +299,7 @@ Checkboxes can be used inside table data rows to select multiple data sets and a
<TableHeadCell sort={(a, b) => a.type.localeCompare(b.type)}>Type</TableHeadCell>
<TableHeadCell sort={(a, b) => a.make - b.make} defaultDirection="desc">Make</TableHeadCell>
<TableHeadCell>
<span class="sr-only">Edit</span>
<span class="sr-only">Buy</span>
</TableHeadCell>
</TableHead>
<TableBody tableBodyClass="divide-y">
Expand All @@ -315,7 +309,7 @@ Checkboxes can be used inside table data rows to select multiple data sets and a
<TableBodyCell>{item.type}</TableBodyCell>
<TableBodyCell>{item.make}</TableBodyCell>
<TableBodyCell>
<a href="/tables" class="font-medium text-primary-600 hover:underline dark:text-primary-500">Edit</a>
<a href="/tables" class="font-medium text-primary-600 hover:underline dark:text-primary-500">Buy</a>
</TableBodyCell>
</TableBodyRow>
</TableBody>
Expand Down Expand Up @@ -918,4 +912,4 @@ The component has the following props, type, and default values. See [types page

- [Flowbite Tables](https://flowbite.com/docs/components/tables/)

<GitHubCompoLinks />
<GitHubCompoLinks />

0 comments on commit aff76e3

Please sign in to comment.