Skip to content

Commit

Permalink
Convert svelte-headless-table into TableWrapper component
Browse files Browse the repository at this point in the history
  • Loading branch information
FyreByrd committed Oct 16, 2024
1 parent 402b017 commit 05b02d5
Show file tree
Hide file tree
Showing 9 changed files with 696 additions and 763 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- This was added solely so a link could be passed into TableWrapper -->
<script lang="ts">
export let href: string;
export let content: string;
export let className: string = '';
</script>

<a href={href} class={className}>{content}</a>

This file was deleted.

296 changes: 296 additions & 0 deletions source/SIL.AppBuilder.Portal/src/lib/components/TableWrapper.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { createTable, Subscribe, Render } from 'svelte-headless-table';
import type { DataColumnInitBase, DataColumnInitFnAndId } from 'svelte-headless-table';
import { addSortBy, addPagination } from 'svelte-headless-table/plugins';
import { superForm } from 'sveltekit-superforms';
import type { SuperValidated, Infer, FormResult } from 'sveltekit-superforms';
import { onDestroy } from 'svelte';
import SearchIcon from '$lib/icons/SearchIcon.svelte';
import type { TableSchema } from '$lib/table';
import ArrowDownIcon from '$lib/icons/ArrowDownIcon.svelte';
import ArrowUpIcon from '$lib/icons/ArrowUpIcon.svelte';
export let data: any[];
export let initialCount: number;
export let dataForm: SuperValidated<Infer<TableSchema>>;
const tableData = writable(data);
const count = writable(initialCount);
// For some reason this gives a duplicate form id warning when there is more than one of these in a page
// given that each one *should* be posting to a separate endpoint, it should be fine?
const { form, enhance, submit } = superForm(dataForm, {
dataType: 'json',
resetForm: false,
onChange(event) {
if (!(event.paths.includes('size') || event.paths.includes('search.text'))) {
submit();
}
},
onUpdate(event) {
const data = event.result.data as FormResult<{ query: { data: any[]; count: number } }>;
if (event.form.valid && data.query) {
tableData.set(data.query.data);
count.set(data.query.count);
}
}
});
const table = createTable(tableData, {
sort: addSortBy({
serverSide: true
}),
page: addPagination({
serverSide: true,
serverItemCount: count
})
});
type Item = (typeof data)[0];
type Value = any;
type Extra = { searchable?: boolean };
export let columns: (DataColumnInitBase<Item, typeof table.plugins, Value> &
DataColumnInitFnAndId<Item, string, Value> &
Extra)[];
export let endpoint: string;
const {
headerRows,
pageRows: rows,
tableAttrs,
tableBodyAttrs,
pluginStates
} = table.createViewModel(table.createColumns(columns.map((c) => table.column(c))));
const { pageIndex, pageCount, pageSize, hasNextPage, hasPreviousPage } = pluginStates.page;
const { sortKeys } = pluginStates.sort;
$: pageSize.set($form.size);
$: pageIndex.set($form.page);
$: collapse = $pageCount > 6;
function index(i: number, page: number): number {
if (page <= 3) return i + 2;
else if (page > $pageCount - 5) return $pageCount + i - 5;
else return page + i - 1;
}
const sortUnsub = sortKeys.subscribe((keys) => {
form.update((data) => ({
...data,
sort: keys.map((k) => ({ field: k.id, direction: k.order }))
}));
});
onDestroy(() => {
sortUnsub();
});
</script>

{#if data.length > 0}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<form
method="POST"
action="?/{endpoint}"
use:enhance
class="flex flex-row gap-2 flex-wrap"
on:keydown={(event) => {
if (event.key === 'Enter') submit();
}}
>
<!-- TODO: Use full-text search in Prisma once it's stable? -->
<!-- TODO: Add Date specific search? -->
<select bind:value={$form.search.field} class="select select-bordered w-full max-w-xs">
<option value={null} selected>Anywhere</option>
{#each columns as col}
{#if col.searchable !== false}
<option value={col.id}>{col.header}</option>
{/if}
{/each}
</select>
<span class="input input-bordered flex items-center gap-2 max-w-xs">
<input type="text" name="search.text" bind:value={$form.search.text} />
<SearchIcon />
</span>
<span class="input input-bordered flex items-center gap-2 max-w-xs">
Show: <!-- TODO: i18n -->
<input type="number" name="size" bind:value={$form.size} />
/ {$count}
</span>
{#if $pageCount > 1}
<div class="join">
<label
class="join-item btn btn-square form-control {$hasPreviousPage ? '' : 'btn-disabled'}"
>
<span>«</span>
<input
class="hidden"
type="radio"
bind:group={$form.page}
name="page"
value={$form.page - 1}
/>
</label>
<label class="join-item btn btn-square form-control {$form.page === 0 ? 'bg-primary' : ''}">
<span>{1}</span>
<input class="hidden" type="radio" bind:group={$form.page} name="page" value={0} />
</label>
{#if collapse}
{#if $form.page > 3}
<button class="join-item btn btn-disabled">...</button>
{:else}
<label
class="join-item btn btn-square form-control {$form.page === 1 ? 'bg-primary' : ''}"
>
<span>{2}</span>
<input class="hidden" type="radio" bind:group={$form.page} name="page" value={1} />
</label>
{/if}
{#each Array.from({ length: 3 }) as _, i}
<label
class="join-item btn btn-square form-control {$form.page === index(i, $form.page)
? 'bg-primary'
: ''}"
>
<span>{index(i, $form.page) + 1}</span>
<input
class="hidden"
type="radio"
bind:group={$form.page}
name="page"
value={index(i, $form.page)}
/>
</label>
{/each}
{#if $form.page < $pageCount - 4}
<button class="join-item btn btn-disabled">...</button>
{:else}
<label
class="join-item btn btn-square form-control {$form.page === $pageCount - 2
? 'bg-primary'
: ''}"
>
<span>{$pageCount - 1}</span>
<input
class="hidden"
type="radio"
bind:group={$form.page}
name="page"
value={$pageCount - 2}
/>
</label>
{/if}
{:else}
{#each Array.from({ length: $pageCount - 2 }) as _, i}
<label
class="join-item btn btn-square form-control {$form.page === i + 1
? 'bg-primary'
: ''}"
>
<span>{i + 2}</span>
<input
class="hidden"
type="radio"
bind:group={$form.page}
name="page"
value={i + 1}
/>
</label>
{/each}
{/if}
<label
class="join-item btn btn-square form-control {$form.page === $pageCount - 1
? 'bg-primary'
: ''}"
>
<span>{$pageCount}</span>
<input
class="hidden"
type="radio"
bind:group={$form.page}
name="page"
value={$pageCount - 1}
/>
</label>
<label class="join-item btn btn-square form-control {$hasNextPage ? '' : 'btn-disabled'}">
<span>»</span>
<input
class="hidden"
type="radio"
bind:group={$form.page}
name="page"
value={$form.page + 1}
/>
</label>
</div>
{/if}
</form>
<table class="w-full" {...$tableAttrs}>
<thead>
{#each $headerRows as headerRow (headerRow.id)}
<Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs>
<tr class="border-b-2 text-left" {...rowAttrs}>
{#each headerRow.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
<th
{...attrs}
on:click={props.sort.toggle}
>
<div>
<Render of={cell.render()} />
</div>
<div>
{#if props.sort.order === 'asc'}
<ArrowUpIcon />
{:else if props.sort.order === 'desc'}
<ArrowDownIcon />
{:else}
&nbsp;
{/if}
</div>
</th>
</Subscribe>
{/each}
</tr>
</Subscribe>
{/each}
</thead>
<tbody {...$tableBodyAttrs}>
{#each $rows as row (row.id)}
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
<tr {...rowAttrs}>
{#each row.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs>
<td {...attrs}>
<Render of={cell.render()} />
</td>
</Subscribe>
{/each}
</tr>
</Subscribe>
{/each}
</tbody>
</table>
{/if}

<style lang="postcss">
tr {
@apply cursor-pointer select-none;
}
tbody > tr:hover {
@apply bg-info;
}
thead {
/* this helps prevent the vertical jankiness */
line-height: inherit;
}
th > div {
display: inline-block;
/* these help mitigate some of the visual jankiness */
min-width: 24px;
min-height: 26px;
}
</style>
Loading

0 comments on commit 05b02d5

Please sign in to comment.