Skip to content

Commit 303ac5e

Browse files
authored
Merge pull request #3 from lambda-curry/codegen-bot/integrate-lambda-curry-forms-1754514167
2 parents ab94507 + dce96c1 commit 303ac5e

File tree

9 files changed

+855
-67
lines changed

9 files changed

+855
-67
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
---
2+
description: Guidelines for using @lambdacurry/forms library with Remix Hook Form and Zod validation
3+
globs: apps/**/app/**/*.tsx, apps/**/app/**/*.ts, packages/**/*.tsx, packages/**/*.ts
4+
alwaysApply: true
5+
---
6+
7+
**Summary:**
8+
This document provides essential guidelines for using `@lambdacurry/forms` with `remix-hook-form` and Zod v4. Key benefits include progressive enhancement, type safety, consistent UI, and WCAG 2.1 AA accessibility compliance. Always prefer `remix-hook-form` over other form libraries in Remix applications.
9+
10+
**Reference Documentation:** https://raw.githubusercontent.com/lambda-curry/forms/refs/heads/main/llms.txt
11+
12+
## Core Architecture & Setup
13+
14+
**@lambdacurry/forms** provides form-aware wrapper components that automatically integrate with React Router and Remix Hook Form context, eliminating boilerplate while maintaining full customization.
15+
16+
### Essential Imports & Setup
17+
```typescript
18+
import { zodResolver } from '@hookform/resolvers/zod';
19+
import { RemixFormProvider, useRemixForm, getValidatedFormData } from 'remix-hook-form';
20+
import { z } from 'zod';
21+
import { useFetcher, type ActionFunctionArgs } from 'react-router';
22+
import { TextField, Checkbox, RadioGroup, DatePicker, FormError } from '@lambdacurry/forms';
23+
import { Button } from '@lambdacurry/forms/ui';
24+
```
25+
26+
### Zod Schema Patterns
27+
```typescript
28+
const formSchema = z.object({
29+
email: z.string().email('Invalid email address'),
30+
password: z.string().min(8, 'Password must be at least 8 characters'),
31+
terms: z.boolean().refine(val => val === true, 'You must accept terms'),
32+
});
33+
34+
type FormData = z.infer<typeof formSchema>;
35+
```
36+
37+
## Standard Form Implementation Pattern
38+
39+
### Complete Login Form Example with FormError
40+
```typescript
41+
const LoginForm = () => {
42+
const fetcher = useFetcher<{
43+
message?: string;
44+
errors?: Record<string, { message: string }>
45+
}>();
46+
47+
const methods = useRemixForm<FormData>({
48+
resolver: zodResolver(formSchema),
49+
defaultValues: { email: '', password: '' },
50+
fetcher,
51+
submitConfig: { action: '/login', method: 'post' },
52+
});
53+
54+
const isSubmitting = fetcher.state === 'submitting';
55+
56+
return (
57+
<RemixFormProvider {...methods}>
58+
<fetcher.Form onSubmit={methods.handleSubmit}>
59+
<TextField
60+
name="email"
61+
type="email"
62+
label="Email Address"
63+
/>
64+
65+
<TextField
66+
name="password"
67+
type="password"
68+
label="Password"
69+
/>
70+
71+
{/* Place FormError before the submit button for critical errors */}
72+
<FormError />
73+
74+
<Button type="submit" disabled={isSubmitting}>
75+
{isSubmitting ? 'Signing In...' : 'Sign In'}
76+
</Button>
77+
</fetcher.Form>
78+
</RemixFormProvider>
79+
);
80+
};
81+
```
82+
83+
### Server Action Handler with FormError Support
84+
```typescript
85+
export const action = async ({ request }: ActionFunctionArgs) => {
86+
const { data, errors } = await getValidatedFormData<FormData>(
87+
request,
88+
zodResolver(formSchema)
89+
);
90+
91+
if (errors) return { errors };
92+
93+
try {
94+
const user = await authenticateUser(data.email, data.password);
95+
return { message: 'Login successful!' };
96+
} catch (error) {
97+
// Multiple error types - field-level and form-level
98+
return {
99+
errors: {
100+
email: { message: 'Account may be suspended' },
101+
_form: { message: 'Invalid credentials. Please try again.' }
102+
}
103+
};
104+
}
105+
};
106+
```
107+
108+
## Advanced Patterns
109+
110+
### Conditional Fields
111+
```typescript
112+
const watchAccountType = methods.watch('accountType');
113+
114+
{watchAccountType === 'business' && (
115+
<TextField name="companyName" label="Company Name" />
116+
)}
117+
```
118+
119+
## Available Form Components
120+
121+
### TextField Component
122+
```typescript
123+
<TextField
124+
name="fieldName"
125+
label="Field Label"
126+
type="text|email|password|number"
127+
placeholder="Enter text"
128+
/>
129+
```
130+
131+
### Checkbox Component
132+
```typescript
133+
<Checkbox
134+
name="terms"
135+
label="Accept Terms and Conditions"
136+
/>
137+
```
138+
139+
### RadioGroup Component
140+
```typescript
141+
// Pattern 1: Using options prop
142+
<RadioGroup
143+
name="size"
144+
label="Select Size"
145+
options={[
146+
{ value: 'sm', label: 'Small' },
147+
{ value: 'lg', label: 'Large' },
148+
]}
149+
/>
150+
151+
// Pattern 2: Using RadioGroupItem children
152+
<RadioGroup name="type" label="Type">
153+
<RadioGroupItem value="personal" label="Personal" />
154+
<RadioGroupItem value="business" label="Business" />
155+
</RadioGroup>
156+
```
157+
158+
### Other Components
159+
```typescript
160+
<Textarea name="message" label="Message" rows={4} />
161+
<DatePicker name="birthDate" label="Birth Date" />
162+
<DropdownMenuSelect name="country" label="Country">
163+
<DropdownMenuSelectItem value="us">United States</DropdownMenuSelectItem>
164+
</DropdownMenuSelect>
165+
<OTPInput name="otp" label="Verification Code" maxLength={6} />
166+
```
167+
168+
## Advanced Patterns
169+
170+
### Custom Submit Handlers
171+
```typescript
172+
const methods = useRemixForm<FormData>({
173+
resolver: zodResolver(formSchema),
174+
fetcher,
175+
submitConfig: { action: '/', method: 'post' },
176+
submitHandlers: {
177+
onValid: (data) => {
178+
const transformedData = {
179+
...data,
180+
timestamp: new Date().toISOString(),
181+
};
182+
fetcher.submit(createFormData(transformedData), { method: 'post', action: '/' });
183+
},
184+
},
185+
});
186+
```
187+
188+
### Conditional Fields
189+
```typescript
190+
const watchAccountType = methods.watch('accountType');
191+
192+
{watchAccountType === 'business' && (
193+
<TextField name="companyName" label="Company Name" />
194+
)}
195+
```
196+
197+
### Component Customization
198+
```typescript
199+
const CustomInput = (props: React.InputHTMLAttributes<HTMLInputElement>) => (
200+
<input
201+
{...props}
202+
className="w-full rounded-lg border-2 border-purple-300 bg-purple-50"
203+
/>
204+
);
205+
206+
<TextField
207+
name="email"
208+
components={{ Input: CustomInput }}
209+
/>
210+
```
211+
212+
## Error Handling
213+
214+
### FormError Component
215+
The `FormError` component automatically displays form-level errors from the `_form` key in server responses.
216+
217+
**Error Handling:**
218+
- Form-level errors: Use `<FormError />` component (place before submit button)
219+
- Field errors: Display automatically via `FormMessage` component
220+
- Server validation: Return `{ errors: { _form: { message: 'Error message' } } }`
221+
222+
### Testing FormError
223+
```typescript
224+
// Test form error display
225+
test('displays form error', async ({ page }) => {
226+
await page.fill('[name="email"]', '[email protected]');
227+
await page.click('button[type="submit"]');
228+
await expect(page.locator('[data-testid="form-error"]')).toBeVisible();
229+
});
230+
```
231+
232+
## Best Practices
233+
234+
### ✅ DO
235+
- Use `remix-hook-form` over plain React Hook Form
236+
- Leverage `getValidatedFormData()` for server-side validation
237+
- Use `createFormData()` for custom submissions
238+
- Import components from `@lambdacurry/forms`
239+
- Use `FormError` for form-level error display (place before submit button)
240+
- Handle both client and server validation
241+
242+
### ❌ DON'T
243+
- Mix form libraries (avoid `useForm` from react-hook-form directly)
244+
- Use manual FormData parsing with remix-hook-form
245+
- Pass `control` props manually (components access context automatically)
246+
- Place FormError at inconsistent locations
247+
248+
## Performance & Accessibility
249+
250+
- **Accessibility**: WCAG 2.1 AA compliance built-in
251+
- **Performance**: Client-side filtering for <1000 items, server-side for larger datasets
252+
- **Progressive Enhancement**: Forms work without JavaScript
253+
- **Type Safety**: Full TypeScript integration with Zod inference
254+
255+
## Testing Patterns
256+
```typescript
257+
// Unit testing
258+
test('form validation works', async () => {
259+
const { errors, data } = await getValidatedFormData(request, zodResolver(schema));
260+
expect(errors).toBeNull();
261+
expect(data.email).toBe('[email protected]');
262+
});
263+
264+
// E2E testing with FormError
265+
test('form works without JS', async ({ page }) => {
266+
await page.context().setJavaScriptEnabled(false);
267+
await page.fill('[name="email"]', '[email protected]');
268+
await page.click('button[type="submit"]');
269+
await expect(page.locator('[data-testid="form-error"]')).toBeVisible();
270+
});
271+
```
272+
273+
This modern form framework provides type-safe, accessible, and performant forms with minimal boilerplate while maintaining full customization capabilities.

0 commit comments

Comments
 (0)