{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "hook-form", "title": "Hook Form Basics", "description": "React Hook Form provider, fields, submit/debug/data preview controls, and form state utilities", "dependencies": ["daisyui", "react-hook-form"], "files": [ { "path": "registry/default/ui/hook-form/hook-form-utils.ts", "content": "'use client';\n\nimport type { FieldError, FieldErrors, FieldValues, UseFormReturn } from 'react-hook-form';\n\nexport type FieldErrorItem = {\n\tpath: string;\n\terror: FieldError;\n\tmessage: string;\n\ttype?: string;\n};\n\nexport type FormatFieldErrorsOptions = {\n\tfallback?: string;\n\tincludePath?: boolean;\n\tseparator?: string;\n};\n\nconst missing = Symbol('missing dirty value');\n\nexport function getFieldErrors(errors: FieldErrors | undefined, rootPath = ''): FieldErrorItem[] {\n\tconst output: FieldErrorItem[] = [];\n\tcollectFieldErrors(errors, rootPath, output);\n\treturn output;\n}\n\nexport function formatFieldErrors(\n\terrors: FieldErrors | undefined,\n\t{ fallback = 'Form validation failed', includePath = true, separator = '; ' }: FormatFieldErrorsOptions = {},\n) {\n\tconst messages = getFieldErrors(errors).map(({ path, message }) => {\n\t\tif (!includePath || !path) return message;\n\t\treturn `${path}: ${message}`;\n\t});\n\treturn messages.length ? messages.join(separator) : fallback;\n}\n\nexport function getDirtyValues(\n\tform: Pick, 'formState' | 'getValues'>,\n): Partial {\n\treturn getDirtyFieldValues(form.formState.dirtyFields, form.getValues()) as Partial;\n}\n\nexport function getDirtyFieldValues(dirtyFields: unknown, values: unknown): unknown {\n\tconst result = collectDirtyValue(dirtyFields, values);\n\treturn result === missing ? {} : result;\n}\n\nfunction collectFieldErrors(value: unknown, path: string, output: FieldErrorItem[]) {\n\tif (!isRecord(value)) return;\n\tif (isFieldError(value)) {\n\t\tconst error = value as FieldError;\n\t\tconst message = toMessage(error.message) || toMessage(error.type) || 'Invalid value';\n\t\toutput.push({\n\t\t\terror,\n\t\t\tmessage,\n\t\t\tpath,\n\t\t\ttype: typeof error.type === 'string' ? error.type : undefined,\n\t\t});\n\t\treturn;\n\t}\n\tfor (const [key, child] of Object.entries(value)) {\n\t\tcollectFieldErrors(child, path ? `${path}.${key}` : key, output);\n\t}\n}\n\nfunction collectDirtyValue(dirty: unknown, value: unknown): unknown | typeof missing {\n\tif (dirty === true) return value;\n\tif (Array.isArray(dirty)) {\n\t\tconst source = Array.isArray(value) ? value : [];\n\t\tconst output: unknown[] = [];\n\t\tlet hasDirty = false;\n\t\tfor (const [index, childDirty] of dirty.entries()) {\n\t\t\tconst child = collectDirtyValue(childDirty, source[index]);\n\t\t\tif (child !== missing) {\n\t\t\t\toutput[index] = child;\n\t\t\t\thasDirty = true;\n\t\t\t}\n\t\t}\n\t\treturn hasDirty ? output : missing;\n\t}\n\tif (isRecord(dirty)) {\n\t\tconst source = isRecord(value) ? value : {};\n\t\tconst output: Record = {};\n\t\tfor (const [key, childDirty] of Object.entries(dirty)) {\n\t\t\tconst child = collectDirtyValue(childDirty, source[key]);\n\t\t\tif (child !== missing) output[key] = child;\n\t\t}\n\t\treturn Object.keys(output).length ? output : missing;\n\t}\n\treturn missing;\n}\n\nfunction isFieldError(value: Record) {\n\treturn (\n\t\t('type' in value && typeof value.type !== 'object') ||\n\t\t('message' in value && (typeof value.message === 'string' || typeof value.message === 'number'))\n\t);\n}\n\nfunction isRecord(value: unknown): value is Record {\n\treturn typeof value === 'object' && value !== null;\n}\n\nfunction toMessage(value: unknown) {\n\tif (typeof value === 'string') return value;\n\tif (typeof value === 'number') return String(value);\n\treturn '';\n}\n", "type": "registry:lib", "target": "@components/hook-form/hook-form-utils.ts" }, { "path": "registry/default/ui/hook-form/hook-form.tsx", "content": "'use client';\n\nimport {\n\ttype ChangeEvent,\n\ttype ComponentPropsWithRef,\n\ttype ReactNode,\n\ttype Ref,\n\tuseId,\n\tuseImperativeHandle,\n\tuseRef,\n\tuseState,\n} from 'react';\nimport {\n\ttype ControllerFieldState,\n\ttype ControllerRenderProps,\n\ttype FieldErrors,\n\ttype FieldPath,\n\ttype FieldPathValue,\n\ttype FieldValues,\n\tFormProvider,\n\ttype SubmitErrorHandler,\n\ttype SubmitHandler,\n\ttype UseControllerProps,\n\ttype UseFormProps,\n\ttype UseFormReturn,\n\ttype UseFormStateReturn,\n\tuseController,\n\tuseForm,\n\tuseFormContext,\n} from 'react-hook-form';\nimport { getDirtyValues, getFieldErrors } from './hook-form-utils';\n\nexport type HookFormSubmitHandler = (\n\tdata: TFieldValues,\n\tmethods: UseFormReturn,\n) => void | Promise;\n\nexport type HookFormInvalidHandler = (\n\terrors: FieldErrors,\n\tmethods: UseFormReturn,\n) => void;\n\nexport type HookFormProps = UseFormProps<\n\tTFieldValues,\n\tTContext\n> & {\n\tchildren?: ReactNode;\n\tformProps?: Omit, 'children' | 'onSubmit' | 'ref'>;\n\tformRef?: Ref;\n\tmethodsRef?: Ref>;\n\tonInvalid?: HookFormInvalidHandler;\n\tonSubmit?: HookFormSubmitHandler;\n\tpreventImplicitSubmit?: boolean;\n};\n\nexport type HookFormFieldContext<\n\tTFieldValues extends FieldValues = FieldValues,\n\tTName extends FieldPath = FieldPath,\n> = {\n\tdescribedBy?: string;\n\terrorId: string;\n\tfield: ControllerRenderProps;\n\tfieldState: ControllerFieldState;\n\tformState: UseFormStateReturn;\n\thintId: string;\n\tid: string;\n\tinvalid: boolean;\n};\n\nexport type HookFormFieldProps<\n\tTFieldValues extends FieldValues = FieldValues,\n\tTName extends FieldPath = FieldPath,\n> = UseControllerProps & {\n\tcheckedValue?: unknown;\n\tclassName?: string;\n\tcontrolType?: 'input' | 'textarea';\n\tdescription?: ReactNode;\n\thint?: ReactNode;\n\tinputClassName?: string;\n\tinputProps?: Omit, 'defaultValue' | 'name' | 'onBlur' | 'onChange' | 'ref' | 'value'>;\n\tisChecked?: (value: unknown, checkedValue: unknown) => boolean;\n\tlabel?: ReactNode;\n\tparse?: (value: string, event: ChangeEvent) => unknown;\n\trender?: (context: HookFormFieldContext) => ReactNode;\n\trequired?: boolean | string;\n\tsize?: 'xs' | 'sm' | 'md' | 'lg';\n\ttextareaProps?: Omit<\n\t\tComponentPropsWithRef<'textarea'>,\n\t\t'defaultValue' | 'name' | 'onBlur' | 'onChange' | 'ref' | 'value'\n\t>;\n\ttype?: ComponentPropsWithRef<'input'>['type'];\n\tuncheckedValue?: unknown;\n};\n\nexport type HookFormErrorSummaryProps = ComponentPropsWithRef<'div'> & {\n\terrors?: FieldErrors;\n\tincludePath?: boolean;\n\tmaxItems?: number;\n\ttitle?: ReactNode;\n};\n\nexport type HookFormSubmitButtonProps = ComponentPropsWithRef<'button'> & {\n\tdirtyOnly?: boolean;\n\tloadingText?: ReactNode;\n\tshowSpinner?: boolean;\n};\n\nexport type HookFormDebugButtonProps = ComponentPropsWithRef<'button'> & {\n\tenabled?: boolean;\n};\n\nexport type HookFormDataPreviewButtonProps =\n\tComponentPropsWithRef<'button'> & {\n\t\tdialogTitle?: ReactNode;\n\t\tformat?: (data: TFieldValues) => string;\n\t\tonLoad?: (data: TFieldValues) => void;\n\t\tonParseError?: (error: unknown) => void;\n\t\tparse?: (text: string) => TFieldValues | Promise;\n\t\ttextareaLabel?: string;\n\t};\n\nexport function HookForm({\n\tchildren,\n\tformProps,\n\tformRef,\n\tmethodsRef,\n\tmode = 'onBlur',\n\tonInvalid,\n\tonSubmit,\n\tpreventImplicitSubmit = true,\n\t...options\n}: HookFormProps) {\n\tconst methods = useForm({ mode, ...options });\n\tuseImperativeHandle(methodsRef, () => methods, [methods]);\n\n\tconst handleValid: SubmitHandler = (data) => onSubmit?.(data, methods);\n\tconst handleInvalid: SubmitErrorHandler = (errors) => {\n\t\tonInvalid?.(errors, methods);\n\t};\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t{preventImplicitSubmit && \n\t);\n}\n\nexport function HookFormDebugButton({\n\tchildren = 'Debug',\n\tclassName,\n\tenabled = true,\n\t...props\n}: HookFormDebugButtonProps) {\n\tconst methods = useFormContext();\n\tif (!enabled) return null;\n\treturn (\n\t\t {\n\t\t\t\tprops.onClick?.(event);\n\t\t\t\tif (event.defaultPrevented) return;\n\t\t\t\tconst { formState } = methods;\n\t\t\t\tconsole.log('[HookFormDebug]', {\n\t\t\t\t\tdirty: getDirtyValues(methods),\n\t\t\t\t\tdirtyFields: formState.dirtyFields,\n\t\t\t\t\tdisabled: formState.disabled,\n\t\t\t\t\terrors: formState.errors,\n\t\t\t\t\tisDirty: formState.isDirty,\n\t\t\t\t\tisLoading: formState.isLoading,\n\t\t\t\t\tisSubmitSuccessful: formState.isSubmitSuccessful,\n\t\t\t\t\tisSubmitted: formState.isSubmitted,\n\t\t\t\t\tisSubmitting: formState.isSubmitting,\n\t\t\t\t\tisValid: formState.isValid,\n\t\t\t\t\tisValidating: formState.isValidating,\n\t\t\t\t\tsubmitCount: formState.submitCount,\n\t\t\t\t\ttouchedFields: formState.touchedFields,\n\t\t\t\t\tvalues: methods.getValues(),\n\t\t\t\t});\n\t\t\t}}\n\t\t>\n\t\t\t{children}\n\t\t\n\t);\n}\n\nexport function HookFormDataPreviewButton({\n\tchildren = 'Data preview',\n\tclassName,\n\tdialogTitle = 'Form data',\n\tformat = (data) => JSON.stringify(data, null, 2),\n\tonLoad,\n\tonParseError,\n\tparse = (text) => JSON.parse(text) as TFieldValues,\n\ttextareaLabel = 'Form data JSON',\n\t...props\n}: HookFormDataPreviewButtonProps) {\n\tconst dialogRef = useRef(null);\n\tconst [error, setError] = useState('');\n\tconst [text, setText] = useState('');\n\tconst methods = useFormContext();\n\tconst titleId = useId();\n\tconst open = () => {\n\t\tsetError('');\n\t\tsetText(format(methods.getValues()));\n\t\tdialogRef.current?.showModal();\n\t};\n\tconst close = () => dialogRef.current?.close();\n\tconst load = async () => {\n\t\ttry {\n\t\t\tconst next = await parse(text);\n\t\t\tmethods.reset(next);\n\t\t\tonLoad?.(next);\n\t\t\tclose();\n\t\t} catch (cause) {\n\t\t\tconst message = cause instanceof Error ? cause.message : String(cause);\n\t\t\tsetError(message);\n\t\t\tonParseError?.(cause);\n\t\t}\n\t};\n\treturn (\n\t\t<>\n\t\t\t {\n\t\t\t\t\tprops.onClick?.(event);\n\t\t\t\t\tif (!event.defaultPrevented) open();\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t{dialogTitle}\n\t\t\t\t\t

\n\t\t\t\t\t setText(event.currentTarget.value)}\n\t\t\t\t\t/>\n\t\t\t\t\t{error && (\n\t\t\t\t\t\t

\n\t\t\t\t\t\t\t{error}\n\t\t\t\t\t\t

\n\t\t\t\t\t)}\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t setText(format(methods.getValues()))}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tRefresh\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t\n\t);\n}\n\nfunction renderDefaultInput<\n\tTFieldValues extends FieldValues = FieldValues,\n\tTName extends FieldPath = FieldPath,\n>(\n\t{ describedBy, field, id, invalid }: HookFormFieldContext,\n\t{\n\t\tcheckedValue,\n\t\tcontrolType,\n\t\tinputClassName,\n\t\tinputProps,\n\t\tisChecked,\n\t\tparse,\n\t\tsize,\n\t\ttextareaProps,\n\t\ttype,\n\t\tuncheckedValue,\n\t}: Pick<\n\t\tHookFormFieldProps,\n\t\t| 'checkedValue'\n\t\t| 'controlType'\n\t\t| 'inputClassName'\n\t\t| 'inputProps'\n\t\t| 'isChecked'\n\t\t| 'parse'\n\t\t| 'size'\n\t\t| 'textareaProps'\n\t\t| 'type'\n\t\t| 'uncheckedValue'\n\t>,\n) {\n\tconst className = joinClassNames(\n\t\tcontrolType === 'textarea' ? 'textarea textarea-bordered w-full' : 'input input-bordered w-full',\n\t\tsize && (controlType === 'textarea' ? `textarea-${size}` : `input-${size}`),\n\t\tinputClassName,\n\t);\n\tconst common = {\n\t\t'aria-describedby': describedBy,\n\t\t'aria-invalid': invalid || undefined,\n\t\tdisabled: field.disabled,\n\t\tid,\n\t\tname: field.name,\n\t\tonBlur: field.onBlur,\n\t\tref: field.ref,\n\t};\n\tif (controlType === 'textarea') {\n\t\treturn (\n\t\t\t\n\t\t\t\t\tfield.onChange(parse ? parse(event.currentTarget.value, event) : event.currentTarget.value)\n\t\t\t\t}\n\t\t\t/>\n\t\t);\n\t}\n\tif (type === 'checkbox') {\n\t\tconst checked = isChecked ? isChecked(field.value, checkedValue) : Object.is(field.value, checkedValue);\n\t\treturn (\n\t\t\t field.onChange(event.currentTarget.checked ? checkedValue : uncheckedValue)}\n\t\t\t/>\n\t\t);\n\t}\n\treturn (\n\t\t field.onChange(parse ? parse(event.currentTarget.value, event) : event.currentTarget.value)}\n\t\t/>\n\t);\n}\n\nfunction withRequiredRule>(\n\trules: HookFormFieldProps['rules'],\n\trequired: HookFormFieldProps['required'],\n\tisRequiredValue?: (value: FieldPathValue) => boolean,\n) {\n\tif (!required) return rules;\n\tconst message = typeof required === 'string' ? required : 'Required';\n\tif (isRequiredValue) {\n\t\treturn {\n\t\t\t...rules,\n\t\t\tvalidate: mergeValidate(rules?.validate, (value) => isRequiredValue(value) || message),\n\t\t};\n\t}\n\tif (rules?.required) return rules;\n\treturn {\n\t\t...rules,\n\t\trequired: message,\n\t};\n}\n\nfunction mergeValidate>(\n\tvalidate: NonNullable['rules']>['validate'],\n\trequiredValidate: (value: FieldPathValue) => true | string,\n): NonNullable['rules']>['validate'] {\n\tif (!validate) return requiredValidate;\n\tif (typeof validate === 'function') {\n\t\treturn (value, formValues) => {\n\t\t\tconst result = validate(value, formValues);\n\t\t\tif (isPromiseLike(result)) return result.then((next) => (next === true ? requiredValidate(value) : next));\n\t\t\treturn result === true ? requiredValidate(value) : result;\n\t\t};\n\t}\n\tconst requiredKey = '__hookFormRequired' in validate ? '__hookFormRequiredFallback' : '__hookFormRequired';\n\treturn {\n\t\t...validate,\n\t\t[requiredKey]: requiredValidate,\n\t};\n}\n\nfunction isPromiseLike(value: T | PromiseLike): value is PromiseLike {\n\treturn typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function';\n}\n\nfunction joinClassNames(...values: Array) {\n\treturn values.filter(Boolean).join(' ');\n}\n\nfunction joinIds(...values: Array) {\n\tconst output = values.filter(Boolean).join(' ');\n\treturn output || undefined;\n}\n", "type": "registry:component", "target": "@components/hook-form/hook-form.tsx" }, { "path": "registry/default/ui/hook-form/index.ts", "content": "export * from './hook-form';\nexport * from './hook-form-utils';\n", "type": "registry:component", "target": "@components/hook-form/index.ts" } ], "type": "registry:component" }