{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "auto-form-tanstack", "title": "Protoform AutoForm for TanStack Form", "description": "TanStack Form-native AutoForm with the complete native form API plus Protoform rendering and protobuf helpers.", "dependencies": [ "@tanstack/react-form" ], "registryDependencies": [ "@protoform/auto-form-core", "@protoform/use-proto-form-tanstack" ], "files": [ { "path": "registry/base-nova/protoform/components/auto-form-tanstack/index.tsx", "content": "'use client';\n\nimport {\n type TanStackAutoFormApi,\n TanStackEngine,\n type TanStackFormOptions,\n} from '../auto-form/adapters/tanstack';\nimport { AutoFormCore } from '../auto-form/auto-form-core';\nimport type { AutoFormProps as BaseAutoFormProps } from '../auto-form/types';\n\ntype FormValues = Record;\n\nexport type AutoFormProps<\n T extends FormValues = FormValues,\n TCustomFieldType extends string = never,\n> = Omit<\n BaseAutoFormProps,\n 'resolver'\n>;\n\nexport {\n compileCelExpression,\n DEFAULT_CEL_MAX_COST,\n type CelEvaluation,\n type CompileCelExpressionOptions,\n type CompiledCelExpression,\n} from '../auto-form/cel-runtime';\nexport {\n inspectAutoFormConfiguration,\n type AutoFormConfigurationDiagnostic,\n type AutoFormConfigurationDiagnosticCode,\n type InspectAutoFormConfigurationInput,\n} from '../auto-form/configuration';\nexport { useAutoForm } from '../auto-form/context';\nexport { defaultRegistry } from '../auto-form/fields';\nexport { defaultClassifyField } from '../auto-form/helpers';\nexport {\n type FieldMatchContext,\n type FieldTypeDefinition,\n FieldTypeRegistry,\n} from '../auto-form/registry';\nexport { AutoFormSlot } from '../auto-form/slot';\nexport type {\n AutoFormMode,\n AutoFormRevalidationMode,\n AutoFormRootHeaderMetadata,\n AutoFormRootHeaderMode,\n AutoFormStep,\n AutoFormStepperConfig,\n AutoFormStepperOrientation,\n AutoFormSubmitContext,\n AutoFormValidationMode,\n BuiltInFieldType,\n DeprecatedFieldPolicy,\n FieldTypes,\n} from '../auto-form/types';\nexport { ShadcnAutoFormFieldComponents } from '../auto-form/auto-form-core';\nexport type { AutoFormFieldComponents, AutoFormFieldProps } from '../auto-form/core-types';\nexport type { AutoFormEngineHandle } from '../auto-form/engine';\nexport type { TanStackAutoFormApi, TanStackFormOptions };\n\nexport function AutoForm<\n T extends FormValues = FormValues,\n TCustomFieldType extends string = never,\n>({\n formOptions,\n ...props\n}: AutoFormProps) {\n return (\n \n {...props}\n renderEngine={({ children, defaultValues, values }) => (\n \n {children}\n \n )}\n />\n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/adapters/tanstack.tsx", "content": "'use client';\n\nimport {\n type FormAsyncValidateOrFn,\n type FormOptions,\n type FormValidateOrFn,\n type ReactFormExtendedApi,\n useForm,\n useStore,\n} from '@tanstack/react-form';\nimport React from 'react';\n\nimport type { SchemaValidationError } from '../core-types';\nimport {\n type AutoFormArrayController,\n type AutoFormEngine,\n AutoFormEngineProvider,\n type AutoFormFieldController,\n errorMessages,\n useDirtyStateNotification,\n} from '../engine';\nimport { getPathInObject } from '../field-utils';\n\ntype FormValues = Record;\ntype SyncValidator = FormValidateOrFn | undefined;\ntype AsyncValidator = FormAsyncValidateOrFn | undefined;\n\nexport type TanStackFormOptions = FormOptions<\n FormValues,\n SyncValidator,\n SyncValidator,\n AsyncValidator,\n SyncValidator,\n AsyncValidator,\n SyncValidator,\n AsyncValidator,\n SyncValidator,\n AsyncValidator,\n AsyncValidator,\n unknown\n>;\n\nexport type TanStackAutoFormApi = ReactFormExtendedApi<\n FormValues,\n SyncValidator,\n SyncValidator,\n AsyncValidator,\n SyncValidator,\n AsyncValidator,\n SyncValidator,\n AsyncValidator,\n SyncValidator,\n AsyncValidator,\n AsyncValidator,\n unknown\n>;\n\ntype TanStackSubmitPayload = Parameters>[0];\n\ntype TanStackEngineContextValue = {\n clearFieldErrors: (name: string) => void;\n fieldErrors: Map;\n form: TanStackAutoFormApi;\n registerRef: (name: string, element: HTMLElement | null) => void;\n};\n\nconst TanStackEngineContext = React.createContext(null);\n\nfunction useTanStackEngineContext() {\n const context = React.useContext(TanStackEngineContext);\n if (!context) {\n throw new Error('TanStack AutoForm controls must be rendered inside the TanStack engine.');\n }\n return context;\n}\n\nfunction TanStackFieldController({\n children,\n name,\n}: {\n children: (controller: AutoFormFieldController) => React.ReactNode;\n name: string;\n}) {\n const { clearFieldErrors, fieldErrors, form, registerRef } = useTanStackEngineContext();\n\n return (\n \n {(field) => {\n const messages = [\n ...errorMessages(field.state.meta.errors),\n ...(fieldErrors.get(name) ?? []),\n ];\n return children({\n errors: [...new Set(messages)],\n name,\n onBlur: field.handleBlur,\n onChange: (value, options) => {\n clearFieldErrors(name);\n if (options) {\n form.setFieldValue(name, value, {\n dontUpdateMeta: options.shouldDirty === false && options.shouldTouch === false,\n dontValidate: options.shouldValidate === false,\n });\n return;\n }\n field.handleChange(value);\n },\n ref: (element) => registerRef(name, element),\n value: field.state.value,\n });\n }}\n \n );\n}\n\nfunction TanStackArrayController({\n children,\n name,\n}: {\n children: (controller: AutoFormArrayController) => React.ReactNode;\n name: string;\n}) {\n const { form } = useTanStackEngineContext();\n const value = useStore(form.store, (state) => getPathInObject(state.values, name.split('.')));\n const arrayValue = Array.isArray(value) ? value : [];\n const collectionId = React.useId();\n const nextItemId = React.useRef(0);\n const keys = React.useRef([]);\n\n while (keys.current.length < arrayValue.length) {\n keys.current.push(`${collectionId}-${nextItemId.current++}`);\n }\n if (keys.current.length > arrayValue.length) {\n keys.current.length = arrayValue.length;\n }\n\n return children({\n append: (item) => {\n keys.current.push(`${collectionId}-${nextItemId.current++}`);\n form.setFieldValue(name, [...arrayValue, item]);\n },\n items: arrayValue.map((item, index) => ({\n key: keys.current[index] ?? `auto-form-item-${index}`,\n value: item,\n })),\n remove: (index) => {\n keys.current.splice(index, 1);\n form.setFieldValue(\n name,\n arrayValue.filter((_, itemIndex) => itemIndex !== index)\n );\n },\n });\n}\n\nfunction setErrorAtPath(target: Record, path: string[], messages: string[]) {\n let current = target;\n for (const [index, segment] of path.entries()) {\n if (index === path.length - 1) {\n current[segment] = { message: messages.join('\\n') };\n return;\n }\n const existing = current[segment];\n if (existing && typeof existing === 'object' && !Array.isArray(existing)) {\n current = existing as Record;\n continue;\n }\n const nested: Record = {};\n current[segment] = nested;\n current = nested;\n }\n}\n\nfunction validationErrorsByPath(errors: SchemaValidationError[]): Map {\n const byPath = new Map();\n for (const error of errors) {\n if (error.path.length === 0) {\n continue;\n }\n const path = error.path.join('.');\n byPath.set(path, [...(byPath.get(path) ?? []), error.message]);\n }\n return byPath;\n}\n\nfunction setDirtyAtPath(target: Record, path: string[]) {\n let current = target;\n for (const [index, segment] of path.entries()) {\n if (index === path.length - 1) {\n current[segment] = true;\n return;\n }\n const existing = current[segment];\n if (existing && typeof existing === 'object' && !Array.isArray(existing)) {\n current = existing as Record;\n } else {\n const nested: Record = {};\n current[segment] = nested;\n current = nested;\n }\n }\n}\n\nfunction dirtyFieldsFromMeta(\n fieldMeta: Record\n) {\n const dirtyFields: Record = {};\n for (const [path, meta] of Object.entries(fieldMeta)) {\n if (meta?.isDirty && !meta.isDefaultValue) {\n setDirtyAtPath(dirtyFields, path.split('.'));\n }\n }\n return dirtyFields;\n}\n\nfunction formValuesEqual(left: unknown, right: unknown): boolean {\n if (Object.is(left, right)) {\n return true;\n }\n if (!left || !right || typeof left !== 'object' || typeof right !== 'object') {\n return false;\n }\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() === right.getTime();\n }\n if (left instanceof Map && right instanceof Map) {\n if (left.size !== right.size) {\n return false;\n }\n return [...left].every(\n ([key, value]) => right.has(key) && formValuesEqual(value, right.get(key))\n );\n }\n if (left instanceof Set && right instanceof Set) {\n return left.size === right.size && [...left].every((value) => right.has(value));\n }\n\n const leftRecord = left as Record;\n const rightRecord = right as Record;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every(\n (key) => Object.hasOwn(rightRecord, key) && formValuesEqual(leftRecord[key], rightRecord[key])\n )\n );\n}\n\nexport type TanStackEngineProps = {\n children: (engine: AutoFormEngine) => React.ReactNode;\n defaultValues: FormValues;\n formOptions?: TanStackFormOptions;\n values?: FormValues;\n onDirtyChange?: (isDirty: boolean) => void;\n};\n\nexport function TanStackEngine({\n children,\n defaultValues,\n formOptions,\n values,\n onDirtyChange,\n}: TanStackEngineProps) {\n const submitRef = React.useRef<((values: FormValues) => void | Promise) | undefined>(undefined);\n const nativeSubmissionRef = React.useRef(undefined);\n const formDefaultValuesRef = React.useRef(defaultValues);\n const nativeOnSubmit = formOptions?.onSubmit;\n const form = useForm<\n FormValues,\n SyncValidator,\n SyncValidator,\n AsyncValidator,\n SyncValidator,\n AsyncValidator,\n SyncValidator,\n AsyncValidator,\n SyncValidator,\n AsyncValidator,\n AsyncValidator,\n unknown\n >({\n ...(formOptions ?? {}),\n defaultValues: formDefaultValuesRef.current,\n onSubmit: async (submission) => {\n nativeSubmissionRef.current = submission;\n await submitRef.current?.(submission.value);\n },\n });\n const state = useStore(form.store, (current) => current);\n const cleanValuesRef = React.useRef(defaultValues);\n const [validationErrors, setValidationErrors] = React.useState([]);\n const [submitError, setSubmitError] = React.useState();\n const fieldRefs = React.useRef(new Map());\n const fieldErrors = validationErrorsByPath(validationErrors);\n const dirtyFields = dirtyFieldsFromMeta(state.fieldMeta);\n const isDirty = !formValuesEqual(state.values, cleanValuesRef.current);\n const notifyDirtyChange = useDirtyStateNotification(isDirty, onDirtyChange);\n const errors: Record = {};\n for (const [path, meta] of Object.entries(state.fieldMeta)) {\n const messages = errorMessages(meta?.errors ?? []);\n if (messages.length > 0) {\n setErrorAtPath(errors, path.split('.'), messages);\n }\n }\n for (const [path, messages] of fieldErrors) {\n setErrorAtPath(errors, path.split('.'), messages);\n }\n const validationRootErrors = validationErrors\n .filter((error) => error.path.length === 0)\n .map((error) => error.message);\n const nativeRootErrors = errorMessages(state.errors);\n const rootError = [...nativeRootErrors, ...validationRootErrors, ...(submitError ? [submitError] : [])].join('\\n') ||\n undefined;\n\n React.useEffect(() => {\n if (values) {\n formDefaultValuesRef.current = values;\n cleanValuesRef.current = values;\n form.reset(values);\n notifyDirtyChange(false);\n }\n }, [form, values]);\n\n const clearErrors = (paths?: string[]) => {\n setSubmitError(undefined);\n setValidationErrors((current) => {\n if (!paths) {\n return [];\n }\n const targets = new Set(paths);\n return current.filter((error) => !targets.has(error.path.join('.')));\n });\n };\n\n const contextValue: TanStackEngineContextValue = {\n clearFieldErrors: (name) =>\n setValidationErrors((current) => current.filter((error) => error.path.join('.') !== name)),\n fieldErrors,\n form,\n registerRef: (name, element) => {\n if (element) {\n fieldRefs.current.set(name, element);\n } else {\n fieldRefs.current.delete(name);\n }\n },\n };\n\n const engine: AutoFormEngine = {\n ArrayController: TanStackArrayController,\n FieldController: TanStackFieldController,\n clearErrors,\n defaultValues: cleanValuesRef.current,\n dirtyFields,\n errors,\n focus: (path) => fieldRefs.current.get(path)?.focus(),\n getFieldInvalid: (path) =>\n Boolean(state.fieldMeta[path]?.errors.length || fieldErrors.get(path)?.length),\n getValues: () => form.state.values,\n handleSubmit: (onValid) => (event) => {\n event.preventDefault();\n submitRef.current = onValid;\n void form.handleSubmit();\n },\n isDirty,\n isSubmitting: state.isSubmitting,\n markClean: () => {\n const currentValues = form.state.values;\n formDefaultValuesRef.current = currentValues;\n cleanValuesRef.current = currentValues;\n form.reset(currentValues);\n notifyDirtyChange(false);\n },\n nativeForm: form,\n reset: (nextValues, options) => {\n if (!options?.keepDefaultValues) {\n formDefaultValuesRef.current = nextValues;\n cleanValuesRef.current = nextValues;\n }\n form.reset(nextValues, options);\n },\n rootError,\n runNativeSubmit: nativeOnSubmit\n ? async () => {\n const submission = nativeSubmissionRef.current;\n if (submission) {\n await nativeOnSubmit(submission);\n }\n }\n : undefined,\n setRootError: setSubmitError,\n setValidationErrors,\n setValue: (path, value, options) => {\n setValidationErrors((current) => current.filter((error) => error.path.join('.') !== path));\n form.setFieldValue(path, value, {\n dontUpdateMeta: options?.shouldDirty === false && options.shouldTouch === false,\n dontValidate: options?.shouldValidate === false,\n });\n },\n trigger: async (paths) => {\n const results = paths\n ? await Promise.all(paths.map((path) => form.validateField(path, 'submit')))\n : await form.validateAllFields('submit');\n return results.flat().length === 0;\n },\n validatesSchema: false,\n values: state.values,\n };\n\n return (\n \n {children(engine)}\n \n );\n}\n", "type": "registry:component" } ], "type": "registry:block" }