Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 273 additions & 0 deletions .cursorrules/lambda-curry-forms.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
---
description: Guidelines for using @lambdacurry/forms library with Remix Hook Form and Zod validation
globs: apps/**/app/**/*.tsx, apps/**/app/**/*.ts, packages/**/*.tsx, packages/**/*.ts
alwaysApply: true
---

**Summary:**
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.

**Reference Documentation:** https://raw.githubusercontent.com/lambda-curry/forms/refs/heads/main/llms.txt

## Core Architecture & Setup

**@lambdacurry/forms** provides form-aware wrapper components that automatically integrate with React Router and Remix Hook Form context, eliminating boilerplate while maintaining full customization.

### Essential Imports & Setup
```typescript
import { zodResolver } from '@hookform/resolvers/zod';
import { RemixFormProvider, useRemixForm, getValidatedFormData } from 'remix-hook-form';
import { z } from 'zod';
import { useFetcher, type ActionFunctionArgs } from 'react-router';
import { TextField, Checkbox, RadioGroup, DatePicker, FormError } from '@lambdacurry/forms';
import { Button } from '@lambdacurry/forms/ui';
```

### Zod Schema Patterns
```typescript
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
terms: z.boolean().refine(val => val === true, 'You must accept terms'),
});

type FormData = z.infer<typeof formSchema>;
```

## Standard Form Implementation Pattern

### Complete Login Form Example with FormError
```typescript
const LoginForm = () => {
const fetcher = useFetcher<{
message?: string;
errors?: Record<string, { message: string }>
}>();

const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: { email: '', password: '' },
fetcher,
submitConfig: { action: '/login', method: 'post' },
});

const isSubmitting = fetcher.state === 'submitting';

return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
<TextField
name="email"
type="email"
label="Email Address"
/>

<TextField
name="password"
type="password"
label="Password"
/>

{/* Place FormError before the submit button for critical errors */}
<FormError />

<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Signing In...' : 'Sign In'}
</Button>
</fetcher.Form>
</RemixFormProvider>
);
};
```

### Server Action Handler with FormError Support
```typescript
export const action = async ({ request }: ActionFunctionArgs) => {
const { data, errors } = await getValidatedFormData<FormData>(
request,
zodResolver(formSchema)
);

if (errors) return { errors };

try {
const user = await authenticateUser(data.email, data.password);
return { message: 'Login successful!' };
} catch (error) {
// Multiple error types - field-level and form-level
return {
errors: {
email: { message: 'Account may be suspended' },
_form: { message: 'Invalid credentials. Please try again.' }
}
};
}
};
```

## Advanced Patterns

### Conditional Fields
```typescript
const watchAccountType = methods.watch('accountType');

{watchAccountType === 'business' && (
<TextField name="companyName" label="Company Name" />
)}
```

Comment on lines +108 to +118
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Remove duplicate "Advanced Patterns" section

The "Advanced Patterns" section appears twice in the document (lines 108-118 and 169-196). The content should be consolidated into a single section to avoid confusion.

Remove the first occurrence (lines 108-118) and keep the more comprehensive second section (lines 169-196) which includes additional patterns like custom submit handlers and component customization.

Also applies to: 169-196

πŸ€– Prompt for AI Agents
In .cursorrules/lambda-curry-forms.mdc between lines 108 and 118, remove the
entire first "Advanced Patterns" section to eliminate duplication. Retain the
second, more comprehensive "Advanced Patterns" section found between lines 169
and 196, which includes additional patterns. This consolidation avoids confusion
by having only one complete "Advanced Patterns" section in the document.

## Available Form Components

### TextField Component
```typescript
<TextField
name="fieldName"
label="Field Label"
type="text|email|password|number"
placeholder="Enter text"
/>
```

### Checkbox Component
```typescript
<Checkbox
name="terms"
label="Accept Terms and Conditions"
/>
```

### RadioGroup Component
```typescript
// Pattern 1: Using options prop
<RadioGroup
name="size"
label="Select Size"
options={[
{ value: 'sm', label: 'Small' },
{ value: 'lg', label: 'Large' },
]}
/>

// Pattern 2: Using RadioGroupItem children
<RadioGroup name="type" label="Type">
<RadioGroupItem value="personal" label="Personal" />
<RadioGroupItem value="business" label="Business" />
</RadioGroup>
```

### Other Components
```typescript
<Textarea name="message" label="Message" rows={4} />
<DatePicker name="birthDate" label="Birth Date" />
<DropdownMenuSelect name="country" label="Country">
<DropdownMenuSelectItem value="us">United States</DropdownMenuSelectItem>
</DropdownMenuSelect>
<OTPInput name="otp" label="Verification Code" maxLength={6} />
```

## Advanced Patterns

### Custom Submit Handlers
```typescript
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
fetcher,
submitConfig: { action: '/', method: 'post' },
submitHandlers: {
onValid: (data) => {
const transformedData = {
...data,
timestamp: new Date().toISOString(),
};
fetcher.submit(createFormData(transformedData), { method: 'post', action: '/' });
},
},
});
```

### Conditional Fields
```typescript
const watchAccountType = methods.watch('accountType');

{watchAccountType === 'business' && (
<TextField name="companyName" label="Company Name" />
)}
```

### Component Customization
```typescript
const CustomInput = (props: React.InputHTMLAttributes<HTMLInputElement>) => (
<input
{...props}
className="w-full rounded-lg border-2 border-purple-300 bg-purple-50"
/>
);

<TextField
name="email"
components={{ Input: CustomInput }}
/>
```

## Error Handling

### FormError Component
The `FormError` component automatically displays form-level errors from the `_form` key in server responses.

**Error Handling:**
- Form-level errors: Use `<FormError />` component (place before submit button)
- Field errors: Display automatically via `FormMessage` component
- Server validation: Return `{ errors: { _form: { message: 'Error message' } } }`

### Testing FormError
```typescript
// Test form error display
test('displays form error', async ({ page }) => {
await page.fill('[name="email"]', 'invalid@email.com');
await page.click('button[type="submit"]');
await expect(page.locator('[data-testid="form-error"]')).toBeVisible();
});
```

## Best Practices

### βœ… DO
- Use `remix-hook-form` over plain React Hook Form
- Leverage `getValidatedFormData()` for server-side validation
- Use `createFormData()` for custom submissions
- Import components from `@lambdacurry/forms`
- Use `FormError` for form-level error display (place before submit button)
- Handle both client and server validation

### ❌ DON'T
- Mix form libraries (avoid `useForm` from react-hook-form directly)
- Use manual FormData parsing with remix-hook-form
- Pass `control` props manually (components access context automatically)
- Place FormError at inconsistent locations

## Performance & Accessibility

- **Accessibility**: WCAG 2.1 AA compliance built-in
- **Performance**: Client-side filtering for <1000 items, server-side for larger datasets
- **Progressive Enhancement**: Forms work without JavaScript
- **Type Safety**: Full TypeScript integration with Zod inference

## Testing Patterns
```typescript
// Unit testing
test('form validation works', async () => {
const { errors, data } = await getValidatedFormData(request, zodResolver(schema));
expect(errors).toBeNull();
expect(data.email).toBe('test@example.com');
});

// E2E testing with FormError
test('form works without JS', async ({ page }) => {
await page.context().setJavaScriptEnabled(false);
await page.fill('[name="email"]', 'test@example.com');
await page.click('button[type="submit"]');
await expect(page.locator('[data-testid="form-error"]')).toBeVisible();
});
```

This modern form framework provides type-safe, accessible, and performant forms with minimal boilerplate while maintaining full customization capabilities.
Loading