Claude-skill-registry form
Generate forms with React Hook Form and Zod validation following established patterns. Use when creating forms, input validation, modal forms, or handling user input.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/form" ~/.claude/skills/majiayu000-claude-skill-registry-form && rm -rf "$T"
manifest:
skills/data/form/SKILL.mdsource content
Form Generator
Generate forms using React Hook Form with Zod validation following established patterns.
Directory Structure
src/ ├── schema/ │ └── myFeature.ts # Zod schemas + default values ├── constants/ │ └── message.ts # Validation error messages └── components/ └── MyForm/ └── index.tsx
Schema File Pattern
// src/schema/user.ts import { z } from 'zod'; import { FORM_VALIDATION } from '@/constants/message'; const amountAboveZeroRefiner = (value: string) => 0 < Number(value); export const userSchema = z.object({ name: z.string().min(1, FORM_VALIDATION.NAME_REQUIRED), email: z.string().email(FORM_VALIDATION.INVALID_EMAIL), amount: z.string() .min(1, FORM_VALIDATION.AMOUNT_REQUIRED) .refine(amountAboveZeroRefiner, { message: FORM_VALIDATION.AMOUNT_MUST_BE_POSITIVE, }), frequency: z.enum(['weekly', 'fortnightly', 'monthly']), startDate: z.date(), endDate: z.date().optional(), }); export type UserFormData = z.infer<typeof userSchema>; export const userDefaultValues: UserFormData = { name: '', email: '', amount: '', frequency: 'monthly', startDate: new Date(), endDate: undefined, };
Validation Messages
// src/constants/message.ts export enum FORM_VALIDATION { NAME_REQUIRED = 'Name is required', INVALID_EMAIL = 'Please enter a valid email address', AMOUNT_REQUIRED = 'Amount is required', AMOUNT_MUST_BE_POSITIVE = 'Amount must be greater than zero', DATE_REQUIRED = 'Date is required', DATE_MUST_BE_FUTURE = 'Date must be in the future', PASSWORD_MIN_LENGTH = 'Password must be at least 8 characters', }
Basic Form Pattern
import React from 'react'; import { View } from 'react-native'; import { useForm, Controller, FormProvider } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button, Input } from '@/components'; import { userSchema, userDefaultValues, UserFormData } from '@/schema/user'; interface UserFormProps { onSubmit: (data: UserFormData) => void; initialValues?: Partial<UserFormData>; } const UserForm: React.FC<UserFormProps> = ({ onSubmit, initialValues }) => { const form = useForm<UserFormData>({ resolver: zodResolver(userSchema), defaultValues: { ...userDefaultValues, ...initialValues }, }); const { control, handleSubmit, formState: { errors, isSubmitting } } = form; return ( <FormProvider {...form}> <View className="gap-4"> <Controller control={control} name="name" render={({ field: { onChange, onBlur, value } }) => ( <Input label="Name" placeholder="Enter name" value={value} onChangeText={onChange} onBlur={onBlur} error={errors.name} /> )} /> <Controller control={control} name="email" render={({ field: { onChange, onBlur, value } }) => ( <Input label="Email" placeholder="Enter email" keyboardType="email-address" autoCapitalize="none" value={value} onChangeText={onChange} onBlur={onBlur} error={errors.email} /> )} /> <Button title="Submit" onPress={handleSubmit(onSubmit)} isLoading={isSubmitting} /> </View> </FormProvider> ); }; export default UserForm;
Modal Form Pattern
import React, { useState } from 'react'; import { View, Text, TouchableOpacity } from 'react-native'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import Modal from 'react-native-modal'; import { SafeAreaView } from 'react-native-safe-area-context'; import { X } from 'lucide-react-native'; import { Button, Input, DatePicker, Picker } from '@/components'; import { MODAL_ANIMATION_DURATION } from '@/constants/common'; import { paymentSchema, paymentDefaultValues, PaymentFormData } from '@/schema/payment'; interface PaymentModalProps { isVisible: boolean; minDate: Date; onClose: () => void; onSubmit: (data: PaymentFormData) => void; } const PaymentModal: React.FC<PaymentModalProps> = ({ isVisible, minDate, onClose, onSubmit, }) => { const [showDatePicker, setShowDatePicker] = useState(false); const form = useForm<PaymentFormData>({ resolver: zodResolver(paymentSchema), defaultValues: { ...paymentDefaultValues, date: minDate }, }); const { control, handleSubmit, reset, formState: { errors, isSubmitting } } = form; const handleClose = () => { setShowDatePicker(false); reset(); onClose(); }; const handleFormSubmit = (data: PaymentFormData) => { setTimeout(() => onSubmit(data), MODAL_ANIMATION_DURATION); handleClose(); }; return ( <Modal isVisible={isVisible} onBackdropPress={handleClose} animationIn="fadeInUp" animationOut="fadeOutDown" backdropTransitionOutTiming={0} hideModalContentWhileAnimating={true} useNativeDriverForBackdrop={true} backdropOpacity={0.5} style={{ justifyContent: 'flex-end', margin: 0 }} > <SafeAreaView className="rounded-t-2xl bg-background p-6"> <View className="mb-4 flex-row items-center justify-between"> <Text className="text-lg font-semibold text-foreground">Add Payment</Text> <TouchableOpacity onPress={handleClose} className="p-1"> <X color="hsl(var(--muted-foreground))" size={20} /> </TouchableOpacity> </View> <View className="gap-4"> <Controller control={control} name="amount" render={({ field: { onChange, onBlur, value } }) => ( <Input label="Amount" placeholder="Enter amount" keyboardType="numeric" value={value} onChangeText={onChange} onBlur={onBlur} error={errors.amount} /> )} /> <Controller control={control} name="date" render={({ field: { onChange, value } }) => ( <View className="gap-2"> <Text className="text-base font-medium text-foreground">Date</Text> <TouchableOpacity onPress={() => setShowDatePicker(true)} className="flex-row items-center justify-between rounded-2xl border-2 border-border bg-background px-4 py-3" > <Text className="text-base text-foreground"> {value.toLocaleDateString()} </Text> </TouchableOpacity> {showDatePicker && ( <DatePicker initialDate={value} minimumDate={minDate} onDateSelected={(date) => { onChange(date); setShowDatePicker(false); }} onClose={() => setShowDatePicker(false)} /> )} </View> )} /> <Button title="Add Payment" onPress={handleSubmit(handleFormSubmit)} isLoading={isSubmitting} /> </View> </SafeAreaView> </Modal> ); }; export default PaymentModal;
Dynamic Field Dependencies
const [lockedField, setLockedField] = useState<'propertyValue' | 'loanAmount' | 'lvr'>('loanAmount'); const updateCalculatedFields = () => { const propertyValue = parseFloat(form.getValues('propertyValue') || '0'); const loanAmount = parseFloat(form.getValues('loanAmount') || '0'); const lvr = form.getValues('lvr'); if (lockedField === 'loanAmount') { form.setValue('loanAmount', ((propertyValue * lvr) / 100).toString()); } else if (lockedField === 'propertyValue') { form.setValue('propertyValue', ((loanAmount / lvr) * 100).toString()); } else { form.setValue('lvr', Math.round((loanAmount / propertyValue) * 100)); } }; <Controller control={control} name="propertyValue" render={({ field: { onChange, ...rest } }) => ( <Input {...rest} onChangeText={(value) => { onChange(value); if (lockedField !== 'propertyValue') updateCalculatedFields(); }} /> )} />
Form with Picker
<Controller control={control} name="frequency" render={({ field: { onChange, value } }) => ( <Picker label="Frequency" placeholder="Select frequency" value={value} options={[ { value: 'weekly', label: 'Weekly', description: 'Every 7 days' }, { value: 'fortnightly', label: 'Fortnightly', description: 'Every 14 days' }, { value: 'monthly', label: 'Monthly', description: 'Once a month' }, ]} onValueChange={onChange} error={errors.frequency} /> )} />
Common Zod Patterns
// String as number const amountString = z.string() .min(1, 'Required') .refine((v) => !isNaN(parseFloat(v)), 'Must be a number') .refine((v) => parseFloat(v) > 0, 'Must be positive'); // Date range validation const dateRangeSchema = z.object({ startDate: z.date(), endDate: z.date(), }).refine((data) => data.endDate > data.startDate, { message: 'End date must be after start date', path: ['endDate'], }); // Conditional validation const conditionalSchema = z.object({ hasEndDate: z.boolean(), endDate: z.date().optional(), }).refine( (data) => !data.hasEndDate || data.endDate !== undefined, { message: 'End date required', path: ['endDate'] } );
Form Utilities
form.reset(); form.reset(newDefaultValues); form.setValue('field', value); form.setError('field', { message: 'Error' }); form.clearErrors(); form.watch('field'); form.getValues(); form.trigger();
Checklist
- Schema in
with type + defaults exportsrc/schema/ - Messages in
src/constants/message.ts -
withuseFormzodResolver -
for each fieldController - Error passed to components
-
for loading stateisSubmitting -
on modal closereset() -
delay before callbacksMODAL_ANIMATION_DURATION