Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions src/lib/table/Table.svelte
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
<script lang="ts">
<script lang="ts" generics="T">
import type { HTMLTableAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
import { twMerge, twJoin } from 'tailwind-merge';
import { setContext } from 'svelte';
import { type TableColrType, type TableCtxType } from '.';
import { writable } from 'svelte/store';

interface Props extends HTMLTableAttributes {
children: Snippet;
header?: Snippet;
footer?: Snippet;
divClass?: string;
striped?: boolean;
hoverable?: boolean;
noborder?: boolean;
shadow?: boolean;
color?: TableColrType;
customeColor?: string;
innerDivClass?: string;
inputClass?: string;
searchClass?: string;
searchPlaceholder?: string;
svgDivClass?: string;
svgClass?: string;
items?: T[];
filter?: (item: T, searchTerm: string) => boolean;
searchTerm?: string;
}
let { children, divClass = 'relative overflow-x-auto', striped, hoverable, noborder, shadow, color = 'default', customeColor, ...restProps }: Props = $props();
let { children, header, footer, divClass = 'relative overflow-x-auto', striped, hoverable, noborder, shadow, color = 'default', customeColor, innerDivClass = 'p-4', inputClass, searchClass = 'relative mt-1', searchPlaceholder, svgDivClass, svgClass = 'w-5 h-5 text-gray-500 dark:text-gray-400', items, searchTerm = $bindable(''), filter, ...restProps }: Props = $props();

let inputCls = twMerge('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', inputClass);
let svgDivCls = twMerge('absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none', svgDivClass);

const colors = {
default: 'text-gray-500 dark:text-gray-400',
Expand All @@ -29,33 +44,77 @@
custom: customeColor
};

let tableCtx: TableCtxType = {
let itemStore: TableCtxType<T>["items"] = writable(items);
let searchTermStore: TableCtxType<T>["searchTerm"] = writable(searchTerm);
let filterStore: TableCtxType<T>["filter"] = writable(filter);
let sorterStore: TableCtxType<T>["sorter"] = writable(undefined);
$effect(() => itemStore.set(items));
$effect(() => searchTermStore.set(searchTerm));
$effect(() => filterStore.set(filter));

let tableCtx: TableCtxType<T> = {
striped,
hoverable,
noborder,
color
color,
items: itemStore,
searchTerm: searchTermStore,
filter: filterStore,
sorter: sorterStore
};

setContext('tableCtx', tableCtx);
</script>

<div class={twJoin(divClass, shadow && 'shadow-md sm:rounded-lg')}>
{#if filter}
<div class={innerDivClass}>
<label for="table-search" class="sr-only">Search</label>
<div class={searchClass}>
<div class={svgDivCls}>
<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>
</div>
<input bind:value={searchTerm} placeholder={searchPlaceholder} type="text" id="table-search" class={inputCls} />
</div>
{#if header}
{@render header()}
{/if}
</div>
{:else if header}
{@render header()}
{/if}
<table {...restProps} class={twMerge('w-full text-left text-sm', colors[color])}>
{@render children()}
</table>
{#if footer}
{@render footer()}
{/if}
</div>

<!--
@component
[Go to docs](https://svelte-5-ui-lib.codewithshin.com/)
## Props
@prop children
@prop header
@prop footer
@prop divClass = 'relative overflow-x-auto'
@prop striped
@prop hoverable
@prop noborder
@prop shadow
@prop color = 'default'
@prop customeColor
@prop innerDivClass = 'p-4'
@prop inputClass
@prop searchClass = 'relative mt-1'
@prop searchPlaceholder
@prop svgDivClass
@prop svgClass = 'w-5 h-5 text-gray-500 dark:text-gray-400'
@prop items
@prop searchTerm = $bindable('')
@prop filter
@prop ...restProps
-->
22 changes: 19 additions & 3 deletions src/lib/table/TableBody.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
<script lang="ts">
import type { Snippet } from 'svelte';
<script lang="ts" generics="T">
import { getContext, onMount, type Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { TableCtxType } from '.';

interface Props extends HTMLAttributes<HTMLTableSectionElement> {
children?: Snippet;
class?: string;
row?: Snippet<[{item: T, index: number}]>;
}
let { children, class: className, ...restProps }: Props = $props();
let { children, class: className, row, ...restProps }: Props = $props();

let tableCtx: TableCtxType<T> = getContext('tableCtx');
let { items, searchTerm, filter, sorter } = tableCtx;
let sorted = $state($items);
onMount(() => sorter.subscribe(sorter => {
sorted = sorter ? sorted?.toSorted((a, b) => sorter.direction * sorter.sort(a, b)) : $items;
}));
let filtered = $derived($filter ? sorted?.filter(item => $filter(item, $searchTerm)) : sorted);
</script>

<tbody class={className} {...restProps}>
{#if children}
{@render children()}
{/if}
{#if row && filtered}
{#each filtered as item, index}
{@render row({item, index})}
{/each}
{/if}
</tbody>

<!--
Expand All @@ -21,5 +36,6 @@
## Props
@prop children
@prop class: className
@prop row
@prop ...restProps
-->
4 changes: 2 additions & 2 deletions src/lib/table/TableBodyCell.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script lang="ts">
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
import { twMerge } from 'tailwind-merge';
import { getContext } from 'svelte';
Expand All @@ -14,7 +14,7 @@

let { children, class: className, colspan, onclick, ...restProps }: Props = $props();

const tableCtx: TableCtxType = getContext('tableCtx');
const tableCtx: TableCtxType<T> = getContext('tableCtx');

let color = $state(tableCtx.color ? tableCtx.color : 'default');

Expand Down
4 changes: 2 additions & 2 deletions src/lib/table/TableBodyRow.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script lang="ts">
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
import { twMerge } from 'tailwind-merge';
import { getContext } from 'svelte';
Expand All @@ -13,7 +13,7 @@
}
let { children, class: className, color, ...restProps }: Props = $props();

const tableCtx: TableCtxType = getContext('tableCtx');
const tableCtx: TableCtxType<T> = getContext('tableCtx');
let rowColor: string = $state(color ? color : tableCtx.color || 'default');
const hoverable = tableCtx.hoverable;
const striped = tableCtx.striped;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/table/TableHead.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script lang="ts">
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
import { twMerge } from 'tailwind-merge';
import { getContext } from 'svelte';
Expand All @@ -13,7 +13,7 @@

let { children, class: className, defaultRow = true, ...restProps }: Props = $props();

const tableCtx: TableCtxType = getContext('tableCtx');
const tableCtx: TableCtxType<T> = getContext('tableCtx');
const color = tableCtx.color;
const noborder: boolean = tableCtx.noborder;
const striped: boolean = tableCtx.striped;
Expand Down
47 changes: 43 additions & 4 deletions src/lib/table/TableHeadCell.svelte
Original file line number Diff line number Diff line change
@@ -1,26 +1,65 @@
<script lang="ts">
import type { Snippet } from 'svelte';
<script lang="ts" generics="T">
import { getContext, type Snippet } from 'svelte';
import { twMerge } from 'tailwind-merge';
import type { HTMLThAttributes } from 'svelte/elements';
import type { TableCtxType } from '.';

interface Props extends HTMLThAttributes {
children?: Snippet;
padding?: string;
class?: string;
btnClass?: string;
sort?: (a: T, b: T) => number;
defaultDirection?: 'asc' | 'desc';
sortDirection?: 'asc' | 'desc' | null;
defaultSort?: boolean;
}
let { children, padding = 'px-6 py-3', class: className, btnClass, sort, defaultDirection = 'asc', sortDirection = $bindable(), defaultSort, ...restProps }: Props = $props();
let sortId = Math.random().toString(36).substring(2);

let { sorter }: TableCtxType<T> = getContext('tableCtx');
$effect(() => {
sortDirection = $sorter?.id === sortId ? $sorter.direction === 1 ? 'asc' : 'desc' : null;
});

if(defaultSort) sortItems();

function sortItems() {
if(!sort) return;
sorter.update(prev => {
const direction = (prev?.id === sortId ? -prev.direction : defaultDirection === 'asc' ? 1 : -1) as -1 | 1;
return {id: sortId, direction, sort};
});
}
let { children, class: className, ...restProps }: Props = $props();
</script>

<th {...restProps} class={twMerge('px-6 py-3', className)}>
{#if sort}
<th {...restProps} class={className} aria-sort={sortDirection ? `${sortDirection}ending` : undefined}>
<button class={twMerge(padding, 'w-full text-left after:absolute after:pl-3', sortDirection === 'asc' && 'after:content-["▲"]', sortDirection === 'desc' && 'after:content-["▼"]', btnClass)} onclick={sortItems}>
{#if children}
{@render children()}
{/if}
</button>
</th>
{:else}
<th {...restProps} class={twMerge(padding, className)}>
{#if children}
{@render children()}
{/if}
</th>
{/if}

<!--
@component
[Go to docs](https://svelte-5-ui-lib.codewithshin.com/)
## Props
@prop children
@prop padding = 'px-6 py-3'
@prop class: className
@prop btnClass
@prop sort
@prop defaultDirection = 'asc'
@prop sortDirection = $bindable()
@prop defaultSort
@prop ...restProps
-->
39 changes: 32 additions & 7 deletions src/lib/table/TableSearch.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
<script lang="ts">
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
import { twMerge } from 'tailwind-merge';
import { setContext } from 'svelte';
import type { HTMLTableAttributes } from 'svelte/elements';
import type { TableCtxType } from '.';
import { writable, type Writable } from 'svelte/store';

type TableSearchType = {
type TableSearchType<T> = {
striped?: boolean;
hoverable?: boolean;
color?: string;
items: Writable<T[] | undefined>;
searchTerm: Writable<string>;
filter: Writable<((item: T, searchTerm: string) => boolean) | undefined>;
sorter: Writable<{sort: (a: T, b: T) => number, direction: -1 | 1, id: string} | undefined>;
};
interface Props extends HTMLTableAttributes {
children?: Snippet;
header?: Snippet;
footer?: Snippet;
divClass?: string;
inputValue?: string;
placeholder?: string;
striped?: boolean;
hoverable?: boolean;
customColor?: string;
Expand All @@ -26,9 +33,11 @@
svgClass?: string;
tableClass?: string;
class?: string;
items?: T[];
filter?: (item: T, searchTerm: string) => boolean;
}

let { children, header, footer, divClass = 'relative overflow-x-auto shadow-md sm:rounded-lg', inputValue = $bindable(), striped, hoverable, customColor = '', color = 'default', innerDivClass = 'p-4', inputClass, searchClass = 'relative mt-1', svgDivClass, svgClass = 'w-5 h-5 text-gray-500 dark:text-gray-400', tableClass = 'w-full text-left text-sm', class: className, ...restProps }: Props = $props();
let { children, header, footer, divClass = 'relative overflow-x-auto shadow-md sm:rounded-lg', inputValue = $bindable(''), placeholder, striped, hoverable, customColor = '', color = 'default', innerDivClass = 'p-4', inputClass, searchClass = 'relative mt-1', svgDivClass, svgClass = 'w-5 h-5 text-gray-500 dark:text-gray-400', tableClass = 'w-full text-left text-sm', class: className, items, filter, ...restProps }: Props = $props();

let inputCls = twMerge('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', inputClass);
let svgDivCls = twMerge('absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none', svgDivClass);
Expand All @@ -44,10 +53,23 @@
pink: 'text-pink-100 dark:text-pink-100',
custom: customColor
};
const tableSearchCtx: TableSearchType = {

let itemStore: TableCtxType<T>["items"] = writable(items);
let searchTermStore: TableCtxType<T>["searchTerm"] = writable(inputValue);
let filterStore: TableCtxType<T>["filter"] = writable(filter);
let sorterStore: TableCtxType<T>["sorter"] = writable(undefined);
$effect(() => itemStore.set(items));
$effect(() => searchTermStore.set(inputValue));
$effect(() => filterStore.set(filter));

const tableSearchCtx: TableSearchType<T> = {
striped,
hoverable,
color
color,
items: itemStore,
searchTerm: searchTermStore,
filter: filterStore,
sorter: sorterStore
};

setContext('tableCtx', tableSearchCtx);
Expand All @@ -62,7 +84,7 @@
<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>
</div>
<input bind:value={inputValue} type="text" id="table-search" class={inputCls} />
<input bind:value={inputValue} {placeholder} type="text" id="table-search" class={inputCls} />
</div>
{#if header}
{@render header()}
Expand All @@ -86,7 +108,8 @@
@prop header
@prop footer
@prop divClass = 'relative overflow-x-auto shadow-md sm:rounded-lg'
@prop inputValue = $bindable()
@prop inputValue = $bindable('')
@prop placeholder
@prop striped
@prop hoverable
@prop customColor = ''
Expand All @@ -98,5 +121,7 @@
@prop svgClass = 'w-5 h-5 text-gray-500 dark:text-gray-400'
@prop tableClass = 'w-full text-left text-sm'
@prop class: className
@prop items
@prop filter
@prop ...restProps
-->
7 changes: 6 additions & 1 deletion src/lib/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import TableBodyRow from './TableBodyRow.svelte';
import TableHeadCell from './TableHeadCell.svelte';
import TableHead from './TableHead.svelte';
import TableSearch from './TableSearch.svelte';
import type { Writable } from 'svelte/store';

type TableCtxType = {
type TableCtxType<T=unknown> = {
striped?: boolean;
hoverable?: boolean;
noborder?: boolean;
color?: TableColrType;
items: Writable<T[] | undefined>;
searchTerm: Writable<string>;
filter: Writable<((item: T, searchTerm: string) => boolean) | undefined>;
sorter: Writable<{sort: (a: T, b: T) => number, direction: -1 | 1, id: string} | undefined>;
};

type TableColrType = 'blue' | 'green' | 'red' | 'yellow' | 'purple' | 'default' | 'indigo' | 'pink' | 'custom';
Expand Down
Loading