0 rows selected.
Id | Date of Birth | Organisation | ||
|---|---|---|---|---|
1 - 50
<script lang="ts">
import { type Column } from "$lib/components/ui/shgrid/index.js";
import { processClientSideData } from "$lib/components/ui/shgrid/clientSideDataProcessor.js";
import Shgrid from "$lib/components/ui/shgrid/shgrid.svelte";
import { SvelteSet } from "svelte/reactivity";
type Row = {
id: string;
name: string;
dob: Date;
organisation: {
id: string;
name: string;
};
};
let columns: Column<Row>[] = $state([
{
key: "id",
label: "Id",
cellRender: (row) => row.id
},
{
key: "name",
label: "Name",
cellRender: (row) => row.name,
sortable: true,
filter: {
type: "string"
}
},
{
key: "dob",
label: "Date of Birth",
cellRender: (row) => row.dob.toLocaleString(),
filter: {
type: "dateRange"
}
},
{
key: "organisation",
label: "Organisation",
cellRender: {
type: "text",
label: (row) => row.organisation.name,
highlightLink: true,
// in a real app this would go to a different page
link: (row) => `#${row.id}`
}
}
]);
const data: Row[] = [
{
id: "u-001",
name: "Alice Johnson",
dob: new Date("1990-04-12"),
organisation: {
id: "org-1",
name: "Acme Corp"
}
},
{
id: "u-002",
name: "Brian Smith",
dob: new Date("1985-09-27"),
organisation: {
id: "org-2",
name: "Globex Ltd"
}
},
{
id: "u-003",
name: "Carla Nguyen",
dob: new Date("1998-01-05"),
organisation: {
id: "org-1",
name: "Acme Corp"
}
}
];
let selected = new SvelteSet<number>();
</script>
<Shgrid
bind:columns
fetcher={({ limit, offset, sorter, columns }) =>
processClientSideData({
data,
columns,
sorter,
limit,
offset
})}
debounceMs={0}
bind:selected
/> Introduction
Data tables are difficult to componentize because of the wide variety of features they support, and the uniqueness of every data set.
There is a native data table offered by Shadcn, this functions and will offer everything you need but it is over complicated for what it does.
Instead have shgrid. It is simple, dependacy free and focused around doing the things we actually do with tables at Exe-Squared.
Tip: If you find yourself using the same table in multiple places, you can always extract it into a reusable component. Only do this if you actually use the component in multiple places, otherwise you're just adding complexity.
Table of Contents
Alternatives
There are many different alteratives for data tables. All with different pros and cons.
- Shadcn Data Tables
- Good but overly complicated. Required user to learn multiple dependacies.
- Native to shadcn, just follow the docs for data tables.
- Ok support for fetching data from an api.
- Grid js
- Primarily designed for client side data which is rarely what we want in real projects.
- It can do server side fetching but it requires the api to be setup in their format and limits what information can be sent per request.
- Limits filters and sorting.
- Cannot expand it to add new features without creating a fork.
- Not svelte native, some things are much more verbose to setup than required because of this.
- AG Grid
- Basically the same as grid js but it's more complicated and can do more. Same pros and cons apply.
- Shgrid, in comparison, is written in svelte 5, uses shadcn and is designed to integrate more easily with any backend. It can do most things your project will need already and can easily be expanded to add new functionality. It is also easy to import the latest changes into your project.
Theres a lot more alternatives, google it.
Installation
- Add the
<Shgrid />component to your project.
Filtering
Inside each column there is the property "filter". This is an object with a field "type". The type definition for it can be found in shgrid/index.ts, however here is a potentionally out of date version.
export type Column<TData = Row> = {
key: string;
label?: string;
filter?: Filter;
sortable?: boolean;
cellRender: CellRender<TData>;
};
export type Filter =
| StringFilter
| NumberFilter
| SelectFilter
| DateRangeFilter;
export type NumberFilter = {
type: "number";
value?: number;
step?: string;
min?: number;
max?: number;
};
export type StringFilter = {
type: "string";
value?: string;
};
export type SelectFilter = {
type: "select";
value?: string;
options: Option[];
multiple?: boolean;
} & (
| {
value?: string[];
multiple: true;
}
| {
value?: string;
multiple?: false;
}
);
export type DateRangeFilter = {
type: "dateRange";
value?: DateRange;
dateRangeProps?: Omit<DateRangeProps, "value">;
}; These options can filter by number, string, select and date range. It is reasonably easy to add a new filter type. Add it to the typescript definition. Then in the shgrid/th.svelte add the case to handle the new filter key. If you are using this with the premade client side filter function you will need to add the functionality for this.
Sorting
Sorting is disabled by default for all columns, since it is very easy to miss adding sorting to a column on the backend. Simply set "sortable: true" within the column.
let columns: Column<Row>[] = $state([
// does not have sorting
{
key: "id",
label: "Id",
cellRender: (row) => row.id,
},
// has sorting
{
key: "name",
label: "Name",
cellRender: (row) => row.name,
sortable: true,
filter: {
type: "string",
},
},
]); Cell Rendering
The different methods for cell rendering are somewhat described in the shgrid/index.ts file. Either it is just a function that takes in the row and returns a string or it is an object with a property type. The type is either custom, this then expects a snippet which takes in a row and column (enables you to get the filters) and then you can render what ever you want. You'll probably want to put The TD base component in for consistency with other cells, as well as maybe the link. THis might get added as some properties in the future. Or the other is a text type which then takes the function like this simple version, but it also has properties for other things such as link.
Select Rows
Selecting rows is an ambiguous thing. In shgrids case it means that you can select rows, store their id in a set and then use this set however you want. The selected items are kept in the set if filters or pagination changes.
In the "shgrid" component there is a property called "selected", this is a SvelteSet that will store all of the ids of the records. There might be a function or something to get the id from a row, I'm pretty sure one is provided by default that just uses the field "id". This is only useful if your data is not uniquly id'ed by an id column.
You might decide you want to add functionality to be able to view the selected items somewhere else on the page. You could either switch the set to a map, this will use substanitially more memory so don't do this if you plan on select a lot of very big things. Or you could create an event function in the select thing that passes the row when the column is selected. This might already exist, I don't really remeber.
Interactivity
There are some event like functions as props to shgrid. You can pass functions to these and it will call it.
Refetch
Sometimes you need to force the table to refetch when something external to the table component has happened. You can do this by binding the function refetch to an empty $state defined variable like this.
Id | Date of Birth | |
|---|---|---|
1 - 50
<script lang="ts">
import { type Column } from "$lib/components/ui/shgrid/index.js";
import { processClientSideData } from "$lib/components/ui/shgrid/clientSideDataProcessor.js";
import Shgrid from "$lib/components/ui/shgrid/shgrid.svelte";
import Button from "$lib/components/ui/button/button.svelte";
type Row = {
id: string;
name: string;
dob: Date;
};
let columns: Column<Row>[] = $state([
{
key: "id",
label: "Id",
cellRender: (row) => row.id
},
{
key: "name",
label: "Name",
cellRender: (row) => row.name,
sortable: true,
filter: {
type: "string"
}
},
{
key: "dob",
label: "Date of Birth",
cellRender: (row) => row.dob.toLocaleString(),
filter: {
type: "dateRange"
}
}
]);
const data: Row[] = [
{
id: "u-001",
name: "Alice Johnson",
dob: new Date("1990-04-12")
},
{
id: "u-002",
name: "Brian Smith",
dob: new Date("1985-09-27")
},
{
id: "u-003",
name: "Carla Nguyen",
dob: new Date("1998-01-05")
}
];
// refetch is defined here, but the function is empty
let refetch = $state(() => {});
</script>
<div class="space-y-8">
<Button onclick={refetch}>Refetch</Button>
<Shgrid
bind:columns
fetcher={async ({ limit, offset, sorter, columns }) =>
// in this getter function we are going to fake server side
// fetching time so that it is obvious when the function is called
new Promise((res) =>
setTimeout(
() =>
res(
processClientSideData({
data,
columns,
sorter,
limit,
offset
})
),
1000
)
)}
bind:refetch
/>
</div>