Live demo: data grid filter/sort (query params)
| | | |
|---|
| Adze | Tools | 18 | 7 |
| Amber Pendant | Ornaments | 38 | 5 |
| Iron Axe | Weapons | 24 | 6 |
| Iron Nails (bag) | Tools | 5 | 25 |
| Linen Tunic | Textiles | 12 | 15 |
| Mead (cask) | Provisions | 16 | 12 |
| Round Shield | Weapons | 32 | 3 |
| Rye Bread | Provisions | 2 | 30 |
| Sailcloth (bolt) | Textiles | 45 | 4 |
| Salted Herring (barrel) | Provisions | 9 | 20 |
| Seax Knife | Weapons | 14 | 11 |
| Silver Arm-ring | Ornaments | 60 | 2 |
| Whetstone | Tools | 3 | 40 |
| Wool Cloak | Textiles | 21 | 9 |
import { useEffect } from "react";
import { atom, useAtom, useAtomValue } from "jotai";
import { rootRouteAtom, queryParamRouteAtom, transformRouteAtom } from "jarl-atoms";
import { Table } from "./DataGridTable";
import { Ware, wares } from "./wares";
const sortColumns = [
{ key: "name", label: "Name" },
{ key: "category", label: "Category" },
{ key: "price", label: "Price (silver)" },
{ key: "stock", label: "Stock" },
] as const;
type SortKey = (typeof sortColumns)[number]["key"];
type SortDirection = "asc" | "desc";
const defaultSort = { key: "name", direction: "asc" } as const;
const parseSort = (sort: string | undefined) => {
if (!sort) {
return defaultSort;
}
const direction: SortDirection = sort.startsWith("-") ? "desc" : "asc";
const key = direction === "desc" ? sort.slice(1) : sort;
return sortColumns.some((column) => column.key === key) ? { key: key as SortKey, direction } : defaultSort;
};
const stringifySort = (key: SortKey, direction: SortDirection) => (direction === "desc" ? `-${key}` : key);
const filterWares = (rows: Ware[], filter: string | undefined) => {
if (!filter) return rows;
const needle = filter.toLowerCase();
return rows.filter(
(ware) => ware.name.toLowerCase().includes(needle) || ware.category.toLowerCase().includes(needle),
);
};
const sortWares = (rows: Ware[], key: SortKey, direction: SortDirection) => {
const sorted = rows
.slice()
.sort((a, b) =>
typeof a[key] === "number"
? (a[key] as number) - (b[key] as number)
: String(a[key]).localeCompare(String(b[key])),
);
return direction === "desc" ? sorted.reverse() : sorted;
};
const gridRoot = rootRouteAtom({ basePath: "/demos/data-grid" });
const sortParam = queryParamRouteAtom("sort", { parent: gridRoot });
const sortRoute = transformRouteAtom(
sortParam,
(values) => parseSort(values.sort),
(values) => ({ sort: stringifySort(values.key, values.direction) }),
);
const filterRoute = queryParamRouteAtom("filter", { parent: sortRoute });
const rowsAtom = atom((get) => {
const values = get(filterRoute).values ?? { ...parseSort(undefined), filter: undefined };
return sortWares(filterWares(wares, values.filter), values.key, values.direction);
});
const filterInputAtom = atom("");
const defaultFilter = { ...defaultSort, filter: undefined };
export const DataGridApp = () => {
const [filter, setFilter] = useAtom(filterRoute);
const rows = useAtomValue(rowsAtom);
const [filterInput, setFilterInput] = useAtom(filterInputAtom);
const currentFilter = filter.values ?? defaultFilter;
useEffect(() => setFilterInput(currentFilter.filter ?? ""), [currentFilter.filter, setFilterInput]);
const commitFilter = () => setFilter({ ...currentFilter, filter: filterInput || undefined });
const toggleSort = (key: SortKey) =>
setFilter({
...currentFilter,
key,
direction: currentFilter.key === key && currentFilter.direction === "asc" ? "desc" : "asc",
});
return (
<div>
<form
onSubmit={(event) => {
event.preventDefault();
commitFilter();
}}
>
<label>
Filter{" "}
<input
type="text"
value={filterInput}
onChange={(event) => setFilterInput(event.target.value)}
placeholder="name or category"
/>
</label>
<button type="submit">Search</button>
</form>
<Table>
<thead>
<tr>
{sortColumns.map((column) => (
<th key={column.key}>
<button type="button" onClick={() => toggleSort(column.key)}>
{column.label}
{currentFilter.key === column.key ? (currentFilter.direction === "asc" ? " ▲" : " ▼") : ""}
</button>
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((ware) => (
<tr key={ware.id}>
<td>{ware.name}</td>
<td>{ware.category}</td>
<td>{ware.price}</td>
<td>{ware.stock}</td>
</tr>
))}
{rows.length === 0 && (
<tr>
<td colSpan={sortColumns.length}>No wares match “{currentFilter.filter ?? ""}”.</td>
</tr>
)}
</tbody>
</Table>
</div>
);
};
export default DataGridApp;