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+ } ;
0 commit comments