Skip to content

Commit 6de63ad

Browse files
authored
Merge pull request #44 from lambda-curry/codegen-data-table-lc187
Implement Data Table Component with ShadCN and TanStack (LC-187)
2 parents 6727627 + b256fa8 commit 6de63ad

52 files changed

Lines changed: 3604 additions & 927 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,6 @@ jobs:
3434
- name: Install Playwright Chromium
3535
run: npx playwright install chromium
3636

37-
- name: Build Storybook
38-
run: yarn build-storybook
39-
40-
- name: Start Storybook server
41-
run: yarn workspace @lambdacurry/forms-docs storybook --ci --port 6006 &
42-
env:
43-
NODE_OPTIONS: --max-old-space-size=4096
44-
45-
- name: Wait for Storybook to be ready
46-
run: npx wait-on http://localhost:6006
47-
4837
- name: Run tests
4938
run: yarn test
5039

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"hookform",
88
"isbot",
99
"lucide",
10+
"Nuqs",
1011
"shadcn",
1112
"sonner"
1213
],
@@ -20,5 +21,6 @@
2021
"editor.codeActionsOnSave": {
2122
"source.fixAll.biome": "explicit",
2223
"source.organizeImports.biome": "explicit"
23-
}
24+
},
25+
"tailwindCSS.classAttributes": ["class", "className", "ngClass", "class:list", "wrapperClassName"]
2426
}

apps/docs/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
"build": "storybook build",
77
"build-storybook": "storybook build",
88
"storybook": "storybook dev -p 6006",
9-
"test": "test-storybook"
9+
"serve": "http-server ./storybook-static -p 6006 -s",
10+
"test": "start-server-and-test serve http://127.0.0.1:6006 'test-storybook --url http://127.0.0.1:6006'",
11+
"test:local": "test-storybook"
1012
},
1113
"dependencies": {
1214
"@lambdacurry/forms": "*",
@@ -28,12 +30,15 @@
2830
"@typescript-eslint/eslint-plugin": "^6.21.0",
2931
"@typescript-eslint/parser": "^6.21.0",
3032
"autoprefixer": "^10.4.20",
33+
"http-server": "^14.1.1",
3134
"react": "^19.0.0",
3235
"react-router": "^7.0.0",
3336
"react-router-dom": "^7.0.0",
37+
"start-server-and-test": "^2.0.11",
3438
"tailwindcss": "^4.0.0",
3539
"typescript": "^5.7.2",
3640
"vite": "^6.2.2",
37-
"vite-tsconfig-paths": "^5.1.4"
41+
"vite-tsconfig-paths": "^5.1.4",
42+
"wait-on": "^8.0.3"
3843
}
3944
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Example of using the new middleware feature in remix-hook-form v7.0.0
2+
import { Form } from 'react-router';
3+
import { RemixFormProvider, useRemixForm } from 'remix-hook-form';
4+
import { zodResolver } from '@hookform/resolvers/zod';
5+
import * as zod from 'zod';
6+
import { TextField } from '@lambdacurry/forms/remix-hook-form';
7+
import { getValidatedFormData } from 'remix-hook-form/middleware';
8+
import type { ActionFunctionArgs } from 'react-router';
9+
10+
// Define schema and types
11+
const schema = zod.object({
12+
name: zod.string().min(1, "Name is required"),
13+
email: zod.string().email("Invalid email format").min(1, "Email is required"),
14+
});
15+
16+
type FormData = zod.infer<typeof schema>;
17+
const resolver = zodResolver(schema);
18+
19+
// Action function using the new middleware
20+
export const action = async ({ context }: ActionFunctionArgs) => {
21+
// Use the middleware to extract and validate form data
22+
const { errors, data, receivedValues } = await getValidatedFormData<FormData>(
23+
context,
24+
resolver
25+
);
26+
27+
if (errors) {
28+
return { errors, defaultValues: receivedValues };
29+
}
30+
31+
// Process the validated data
32+
console.log('Processing data:', data);
33+
34+
return { success: true, data };
35+
};
36+
37+
// Component
38+
export default function MiddlewareExample() {
39+
const {
40+
handleSubmit,
41+
formState: { errors },
42+
register,
43+
} = useRemixForm<FormData>({
44+
mode: "onSubmit",
45+
resolver,
46+
});
47+
48+
return (
49+
<div className="p-4">
50+
<h1 className="text-2xl font-bold mb-4">Remix Hook Form v7 Middleware Example</h1>
51+
52+
<RemixFormProvider>
53+
<Form method="POST" onSubmit={handleSubmit}>
54+
<div className="space-y-4">
55+
<TextField
56+
label="Name"
57+
{...register("name")}
58+
error={errors.name?.message}
59+
/>
60+
61+
<TextField
62+
label="Email"
63+
type="email"
64+
{...register("email")}
65+
error={errors.email?.message}
66+
/>
67+
68+
<button
69+
type="submit"
70+
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
71+
>
72+
Submit
73+
</button>
74+
</div>
75+
</Form>
76+
</RemixFormProvider>
77+
</div>
78+
);
79+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Example of setting up the middleware in root.tsx
2+
import { unstable_extractFormDataMiddleware } from 'remix-hook-form/middleware';
3+
import { Outlet } from 'react-router-dom';
4+
5+
// Export the middleware for React Router 7
6+
export const unstable_middleware = [unstable_extractFormDataMiddleware()];
7+
8+
export default function Root() {
9+
return (
10+
<html lang="en">
11+
<head>
12+
<meta charSet="utf-8" />
13+
<meta name="viewport" content="width=device-width, initial-scale=1" />
14+
<title>Remix Hook Form v7 Example</title>
15+
</head>
16+
<body>
17+
<div className="container mx-auto">
18+
<Outlet />
19+
</div>
20+
</body>
21+
</html>
22+
);
23+
}

apps/docs/src/lib/storybook/react-router-stub.tsx

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
type LoaderFunction,
88
type MetaFunction,
99
type NonIndexRouteObject,
10-
createRoutesStub,
10+
RouterProvider,
11+
createMemoryRouter,
1112
} from 'react-router-dom';
1213

1314
export type StubRouteObject = StubIndexRouteObject | StubNonIndexRouteObject;
@@ -36,21 +37,49 @@ interface StubIndexRouteObject
3637

3738
interface RemixStubOptions {
3839
routes: StubRouteObject[];
40+
initialPath?: string;
3941
}
4042

4143
export const withReactRouterStubDecorator = (options: RemixStubOptions): Decorator => {
42-
const { routes } = options;
43-
return (Story) => {
44-
// Map routes to include Story component as fallback if no Component provided
44+
const { routes, initialPath = '/' } = options;
45+
// This outer function runs once when Storybook loads the story meta
46+
47+
return (Story, context) => {
48+
// This inner function runs when the story component actually renders
4549
const mappedRoutes = routes.map((route) => ({
4650
...route,
47-
Component: route.Component ?? (() => <Story />),
51+
Component: route.Component ?? (() => <Story {...context.args} />),
4852
}));
4953

50-
// Use more specific type assertion to fix the incompatibility
51-
// @ts-ignore - Types from createRoutesStub are incompatible but the code works at runtime
52-
const RemixStub = createRoutesStub(mappedRoutes);
54+
// Get the base path (without existing query params from options)
55+
const basePath = initialPath.split('?')[0];
56+
57+
// Get the current search string from the actual browser window, if available
58+
// If not available, use a default search string with parameters needed for the data table
59+
const currentWindowSearch = typeof window !== 'undefined'
60+
? window.location.search
61+
: '?page=0&pageSize=10';
62+
63+
// Combine them for the initial entry
64+
const actualInitialPath = `${basePath}${currentWindowSearch}`;
65+
66+
// Create a memory router, initializing it with the path derived from the window's search params
67+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
68+
const router = createMemoryRouter(mappedRoutes as any, {
69+
initialEntries: [actualInitialPath], // Use the path combined with window.location.search
70+
});
5371

54-
return <RemixStub initialEntries={['/']} />;
72+
return <RouterProvider router={router} />;
5573
};
5674
};
75+
76+
/**
77+
* A decorator that provides URL state management for stories
78+
* Use this when you need URL query parameters in your stories
79+
*/
80+
export const withURLState = (initialPath = '/'): Decorator => {
81+
return withReactRouterStubDecorator({
82+
routes: [{ path: '/' }],
83+
initialPath,
84+
});
85+
};

0 commit comments

Comments
 (0)