Skip to content

Commit d6ca858

Browse files
Implement DataTableRouterForm and remove Input component (LC-187)
1 parent 38ddefa commit d6ca858

8 files changed

Lines changed: 566 additions & 30 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { DataTableRouterForm } from '@lambdacurry/forms/ui/data-table/data-table-router-form';
2+
import { DataTableColumnHeader } from '@lambdacurry/forms/ui/data-table/data-table-column-header';
3+
import type { Meta, StoryObj } from '@storybook/react';
4+
import { type ActionFunctionArgs, useLoaderData, useNavigation } from 'react-router';
5+
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
6+
import { z } from 'zod';
7+
import { useEffect, useState } from 'react';
8+
9+
// Define the data schema
10+
const userSchema = z.object({
11+
id: z.string(),
12+
name: z.string(),
13+
email: z.string().email(),
14+
role: z.enum(['admin', 'user', 'editor']),
15+
status: z.enum(['active', 'inactive', 'pending']),
16+
createdAt: z.string().datetime(),
17+
});
18+
19+
type User = z.infer<typeof userSchema>;
20+
21+
// Sample data
22+
const users: User[] = Array.from({ length: 100 }).map((_, i) => ({
23+
id: `user-${i + 1}`,
24+
name: `User ${i + 1}`,
25+
email: `user${i + 1}@example.com`,
26+
role: i % 3 === 0 ? 'admin' : i % 3 === 1 ? 'user' : 'editor',
27+
status: i % 3 === 0 ? 'active' : i % 3 === 1 ? 'inactive' : 'pending',
28+
createdAt: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString(),
29+
}));
30+
31+
// Define the columns
32+
const columns = [
33+
{
34+
accessorKey: 'id',
35+
header: ({ column }) => (
36+
<DataTableColumnHeader column={column} title="ID" />
37+
),
38+
cell: ({ row }) => <div className="font-medium">{row.getValue('id')}</div>,
39+
enableSorting: false,
40+
enableHiding: false,
41+
},
42+
{
43+
accessorKey: 'name',
44+
header: ({ column }) => (
45+
<DataTableColumnHeader column={column} title="Name" />
46+
),
47+
cell: ({ row }) => <div>{row.getValue('name')}</div>,
48+
},
49+
{
50+
accessorKey: 'email',
51+
header: ({ column }) => (
52+
<DataTableColumnHeader column={column} title="Email" />
53+
),
54+
cell: ({ row }) => <div>{row.getValue('email')}</div>,
55+
},
56+
{
57+
accessorKey: 'role',
58+
header: ({ column }) => (
59+
<DataTableColumnHeader column={column} title="Role" />
60+
),
61+
cell: ({ row }) => <div className="capitalize">{row.getValue('role')}</div>,
62+
filterFn: (row, id, value) => {
63+
return value.includes(row.getValue(id));
64+
},
65+
},
66+
{
67+
accessorKey: 'status',
68+
header: ({ column }) => (
69+
<DataTableColumnHeader column={column} title="Status" />
70+
),
71+
cell: ({ row }) => (
72+
<div className="capitalize">{row.getValue('status')}</div>
73+
),
74+
filterFn: (row, id, value) => {
75+
return value.includes(row.getValue(id));
76+
},
77+
},
78+
{
79+
accessorKey: 'createdAt',
80+
header: ({ column }) => (
81+
<DataTableColumnHeader column={column} title="Created At" />
82+
),
83+
cell: ({ row }) => (
84+
<div>{new Date(row.getValue('createdAt')).toLocaleDateString()}</div>
85+
),
86+
},
87+
];
88+
89+
// Mock API handler for data fetching with filters and pagination
90+
const handleDataFetch = async (request: Request) => {
91+
// Simulate server delay
92+
await new Promise(resolve => setTimeout(resolve, 500));
93+
94+
// Get form data
95+
const formData = await request.formData();
96+
97+
// Get query parameters
98+
const page = parseInt(formData.get('page')?.toString() || '0');
99+
const pageSize = parseInt(formData.get('pageSize')?.toString() || '10');
100+
const sortField = formData.get('sortField')?.toString();
101+
const sortOrder = formData.get('sortOrder')?.toString();
102+
const roleFilter = formData.getAll('role').map(val => val.toString());
103+
const statusFilter = formData.getAll('status').map(val => val.toString());
104+
const search = formData.get('search')?.toString();
105+
106+
// Apply filters
107+
let filteredData = [...users];
108+
109+
if (roleFilter.length > 0) {
110+
filteredData = filteredData.filter(user => roleFilter.includes(user.role));
111+
}
112+
113+
if (statusFilter.length > 0) {
114+
filteredData = filteredData.filter(user => statusFilter.includes(user.status));
115+
}
116+
117+
if (search) {
118+
const searchLower = search.toLowerCase();
119+
filteredData = filteredData.filter(
120+
user =>
121+
user.name.toLowerCase().includes(searchLower) ||
122+
user.email.toLowerCase().includes(searchLower)
123+
);
124+
}
125+
126+
// Apply sorting
127+
if (sortField && sortOrder) {
128+
filteredData.sort((a, b) => {
129+
const aValue = a[sortField as keyof User];
130+
const bValue = b[sortField as keyof User];
131+
132+
if (aValue < bValue) return sortOrder === 'asc' ? -1 : 1;
133+
if (aValue > bValue) return sortOrder === 'asc' ? 1 : -1;
134+
return 0;
135+
});
136+
}
137+
138+
// Apply pagination
139+
const start = page * pageSize;
140+
const paginatedData = filteredData.slice(start, start + pageSize);
141+
142+
return {
143+
data: paginatedData,
144+
meta: {
145+
total: filteredData.length,
146+
page,
147+
pageSize,
148+
pageCount: Math.ceil(filteredData.length / pageSize),
149+
}
150+
};
151+
};
152+
153+
// Component to display the data table with router form integration
154+
const DataTableRouterFormExample = () => {
155+
const [data, setData] = useState<User[]>([]);
156+
const [pageCount, setPageCount] = useState(0);
157+
const navigation = useNavigation();
158+
159+
// Get data from the router action
160+
const actionData = useLoaderData() as {
161+
data: User[];
162+
meta: {
163+
total: number;
164+
page: number;
165+
pageSize: number;
166+
pageCount: number;
167+
}
168+
} | null;
169+
170+
// Update state when action data changes
171+
useEffect(() => {
172+
if (actionData) {
173+
setData(actionData.data);
174+
setPageCount(actionData.meta.pageCount);
175+
}
176+
}, [actionData]);
177+
178+
return (
179+
<div className="container mx-auto py-10">
180+
<h1 className="text-2xl font-bold mb-4">Users Table (React Router Form Integration)</h1>
181+
<p className="mb-4">
182+
This example demonstrates integration with React Router forms, including:
183+
</p>
184+
<ul className="list-disc pl-5 mb-4">
185+
<li>Form-based filtering with automatic submission</li>
186+
<li>Loading state while waiting for data</li>
187+
<li>Server-side filtering and pagination</li>
188+
<li>URL-based state management</li>
189+
</ul>
190+
<DataTableRouterForm
191+
columns={columns}
192+
data={data}
193+
pageCount={pageCount}
194+
formAction="/api/users"
195+
formMethod="post"
196+
filterableColumns={[
197+
{
198+
id: 'role',
199+
title: 'Role',
200+
options: [
201+
{ label: 'Admin', value: 'admin' },
202+
{ label: 'User', value: 'user' },
203+
{ label: 'Editor', value: 'editor' },
204+
],
205+
},
206+
{
207+
id: 'status',
208+
title: 'Status',
209+
options: [
210+
{ label: 'Active', value: 'active' },
211+
{ label: 'Inactive', value: 'inactive' },
212+
{ label: 'Pending', value: 'pending' },
213+
],
214+
},
215+
]}
216+
searchableColumns={[
217+
{
218+
id: 'name',
219+
title: 'Name',
220+
},
221+
]}
222+
/>
223+
</div>
224+
);
225+
};
226+
227+
const meta: Meta<typeof DataTableRouterForm> = {
228+
title: 'UI/DataTableRouterForm',
229+
component: DataTableRouterForm,
230+
parameters: {
231+
layout: 'fullscreen',
232+
},
233+
tags: ['autodocs'],
234+
};
235+
236+
export default meta;
237+
type Story = StoryObj<typeof meta>;
238+
239+
export const Default: Story = {
240+
render: () => <DataTableRouterFormExample />,
241+
decorators: [
242+
withReactRouterStubDecorator({
243+
routes: [
244+
{
245+
path: '/api/users',
246+
action: async ({ request }: ActionFunctionArgs) => handleDataFetch(request),
247+
},
248+
],
249+
}),
250+
],
251+
};

packages/components/src/ui/data-table/data-table-faceted-filter.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,22 @@ interface DataTableFacetedFilterProps<TData, TValue> {
2525
value: string;
2626
icon?: React.ComponentType<{ className?: string }>;
2727
}[];
28+
formMode?: boolean;
2829
}
2930

3031
export function DataTableFacetedFilter<TData, TValue>({
3132
column,
3233
title,
3334
options,
35+
formMode = false,
3436
}: DataTableFacetedFilterProps<TData, TValue>) {
3537
const facets = column?.getFacetedUniqueValues();
3638
const selectedValues = new Set(column?.getFilterValue() as string[]);
3739

3840
return (
3941
<Popover>
4042
<PopoverTrigger asChild>
41-
<Button variant="outline" size="sm" className="h-8 border-dashed">
43+
<Button variant="outline" size="sm" className="h-8 border-dashed" type="button">
4244
<PlusCircle className="mr-2 h-4 w-4" />
4345
{title}
4446
{selectedValues?.size > 0 && (
@@ -138,6 +140,19 @@ export function DataTableFacetedFilter<TData, TValue>({
138140
</CommandList>
139141
</Command>
140142
</PopoverContent>
143+
144+
{formMode && selectedValues.size > 0 && column && (
145+
<div className="hidden">
146+
{Array.from(selectedValues).map((value) => (
147+
<input
148+
key={`${column.id}-${value}`}
149+
type="hidden"
150+
name={column.id as string}
151+
value={value}
152+
/>
153+
))}
154+
</div>
155+
)}
141156
</Popover>
142157
);
143158
}

0 commit comments

Comments
 (0)