{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "auto-form-tanstack-v2", "title": "Experimental Protoform AutoForm for TanStack Form v2", "description": "Experimental AutoForm adapter for the TanStack Form v2 alpha with native atoms, validator arrays, ArrayField state, and submit validation errors.", "dependencies": [ "@tanstack/react-form-v2@npm:@tanstack/react-form@2.0.0-alpha.0" ], "registryDependencies": [ "@protoform/auto-form-core", "@protoform/use-proto-form-tanstack-v2" ], "files": [ { "path": "registry/base-nova/protoform/components/auto-form-tanstack-v2/index.tsx", "content": "'use client';\n\nimport {\n type TanStackFormV2Options,\n type TanStackV2AutoFormApi,\n TanStackV2Engine,\n} from '../auto-form/adapters/tanstack-v2';\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 T,\n TanStackV2AutoFormApi,\n TanStackFormV2Options,\n never,\n TCustomFieldType\n >,\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 { 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 {\n AutoFormFieldComponents,\n AutoFormFieldProps,\n} from '../auto-form/core-types';\nexport type { AutoFormEngineHandle } from '../auto-form/engine';\nexport type { TanStackFormV2Options, TanStackV2AutoFormApi };\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={({\n children,\n defaultValues,\n validateSchema,\n values,\n }) => (\n \n {children}\n \n )}\n />\n );\n}\n", "type": "registry:component" }, { "path": "registry/base-nova/protoform/components/auto-form/adapters/tanstack-v2.tsx", "content": "'use client';\n\nimport {\n type DefaultReactFormComponentMap,\n type FormOptions,\n type FormValidator,\n type FormValidators,\n type ReactFormApi,\n type ToFormErrorTypes,\n type ValidationErrorMap,\n useForm,\n useSelector,\n} from '@tanstack/react-form-v2';\nimport React from 'react';\n\nimport { dirtyFieldsFromValues } from '../../../lib/protobuf-provider';\nimport type { SchemaValidation, SchemaValidationError } from '../core-types';\nimport {\n type AutoFormArrayController,\n type AutoFormEngine,\n AutoFormEngineProvider,\n type AutoFormFieldController,\n errorMessages,\n useDirtyStateNotification,\n} from '../engine';\nimport type {\n AutoFormRevalidationMode,\n AutoFormValidationMode,\n} from '../types';\n\ntype FormValues = Record;\ntype V2Validators = FormValidators;\ntype V2ErrorTypes = ToFormErrorTypes;\ntype DynamicArrayFieldApi = {\n pushValue: (value: unknown) => void;\n removeValue: (index: number) => void;\n value: unknown;\n};\ntype DynamicArrayFieldComponent = React.ComponentType<{\n children: (field: DynamicArrayFieldApi) => React.ReactNode;\n name: string;\n}>;\n\nexport type TanStackFormV2Options = Omit<\n FormOptions,\n 'defaultValues'\n>;\n\nexport type TanStackV2AutoFormApi = ReactFormApi<\n FormValues,\n V2ErrorTypes,\n DefaultReactFormComponentMap\n>;\n\ntype TanStackV2EngineContextValue = {\n clearFieldErrors: (name: string) => void;\n fieldErrors: Map;\n form: TanStackV2AutoFormApi;\n nativeFieldErrors: Map;\n registerRef: (name: string, element: HTMLElement | null) => void;\n setNativeFieldErrors: React.Dispatch<\n React.SetStateAction>\n >;\n};\n\nconst TanStackV2EngineContext =\n React.createContext(null);\n\nfunction toTanStackV2Path(path: string): string {\n return path\n .split('.')\n .map((segment, index) =>\n /^\\d+$/.test(segment) ? `[${segment}]` : index === 0 ? segment : `.${segment}`\n )\n .join('');\n}\n\nfunction sameMessages(left: string[] | undefined, right: string[]) {\n const current = left ?? [];\n return (\n current.length === right.length &&\n current.every((message, index) => message === right[index])\n );\n}\n\nfunction useTanStackV2EngineContext() {\n const context = React.useContext(TanStackV2EngineContext);\n if (!context) {\n throw new Error(\n 'TanStack Form v2 AutoForm controls must be rendered inside the v2 engine.'\n );\n }\n return context;\n}\n\nfunction NativeFieldErrorsRegistration({\n messages,\n name,\n}: {\n messages: string[];\n name: string;\n}) {\n const { setNativeFieldErrors } = useTanStackV2EngineContext();\n\n React.useEffect(\n function syncNativeFieldErrors() {\n setNativeFieldErrors((current) => {\n if (sameMessages(current.get(name), messages)) {\n return current;\n }\n const next = new Map(current);\n if (messages.length > 0) {\n next.set(name, messages);\n } else {\n next.delete(name);\n }\n return next;\n });\n },\n [messages, name, setNativeFieldErrors]\n );\n\n React.useEffect(\n function unregisterNativeFieldErrors() {\n return () => {\n setNativeFieldErrors((current) => {\n if (!current.has(name)) {\n return current;\n }\n const next = new Map(current);\n next.delete(name);\n return next;\n });\n };\n },\n [name, setNativeFieldErrors]\n );\n\n return null;\n}\n\nfunction TanStackV2FieldController({\n children,\n name,\n}: {\n children: (controller: AutoFormFieldController) => React.ReactNode;\n name: string;\n}) {\n const {\n clearFieldErrors,\n fieldErrors,\n form,\n registerRef,\n } = useTanStackV2EngineContext();\n const fieldName = toTanStackV2Path(name);\n\n return (\n \n {(field) => {\n const nativeMessages = errorMessages(field.errors);\n const messages = [\n ...nativeMessages,\n ...(fieldErrors.get(name) ?? []),\n ];\n return (\n <>\n \n {children({\n errors: [...new Set(messages)],\n name,\n onBlur: field.handleBlur,\n onChange: (value, options) => {\n clearFieldErrors(name);\n field.handleChange(value, {\n causeValidation: options?.shouldValidate,\n markAsDirty: options?.shouldDirty,\n markAsTouched: options?.shouldTouch,\n });\n },\n ref: (element) => registerRef(name, element),\n value: field.value,\n })}\n \n );\n }}\n \n );\n}\n\nfunction TanStackV2ArrayController({\n children,\n name,\n}: {\n children: (controller: AutoFormArrayController) => React.ReactNode;\n name: string;\n}) {\n const { form } = useTanStackV2EngineContext();\n const ArrayField = form.ArrayField as unknown as DynamicArrayFieldComponent;\n const collectionId = React.useId();\n const nextItemId = React.useRef(0);\n const keys = React.useRef([]);\n const fieldName = toTanStackV2Path(name);\n\n return (\n \n {(field) => {\n const arrayValue = Array.isArray(field.value) ? field.value : [];\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 field.pushValue(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 field.removeValue(index);\n },\n });\n }}\n \n );\n}\n\nfunction validationErrorsByPath(\n errors: SchemaValidationError[]\n): 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 setErrorAtPath(\n target: Record,\n path: string[],\n messages: string[]\n) {\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 toV2ValidationError(\n errors: SchemaValidationError[]\n): ValidationErrorMap {\n const fieldEntries = validationErrorsByPath(errors);\n const formErrors = errors\n .filter((error) => error.path.length === 0)\n .map((error) => error.message);\n return {\n ...(formErrors.length > 0 ? { form: formErrors } : {}),\n fields: Object.fromEntries(\n [...fieldEntries].map(([path, messages]) => [\n toTanStackV2Path(path),\n messages,\n ])\n ),\n };\n}\n\nfunction shouldValidateForMode(\n mode: AutoFormValidationMode | AutoFormRevalidationMode,\n event: 'blur' | 'change'\n) {\n return mode === event;\n}\n\nexport type TanStackV2EngineProps = {\n children: (engine: AutoFormEngine) => React.ReactNode;\n defaultValues: FormValues;\n formOptions?: TanStackFormV2Options;\n onDirtyChange?: (isDirty: boolean) => void;\n revalidationMode?: AutoFormRevalidationMode;\n validateSchema: (\n values: FormValues,\n signal: AbortSignal\n ) => Promise;\n validationMode?: AutoFormValidationMode;\n values?: FormValues;\n};\n\nexport function TanStackV2Engine({\n children,\n defaultValues,\n formOptions,\n onDirtyChange,\n revalidationMode = 'change',\n validateSchema,\n validationMode = 'submit',\n values,\n}: TanStackV2EngineProps) {\n const formDefaultValuesRef = React.useRef(defaultValues);\n const cleanValuesRef = React.useRef(defaultValues);\n const hasNormalizedDefaultsRef = React.useRef(false);\n const validatedValuesRef = React.useRef(undefined);\n const manualValidationRef = React.useRef(false);\n const [validationErrors, setValidationErrors] = React.useState<\n SchemaValidationError[]\n >([]);\n const [submitError, setSubmitError] = React.useState();\n const [isAutoFormSubmitting, setIsAutoFormSubmitting] = React.useState(false);\n const [nativeFieldErrors, setNativeFieldErrors] = React.useState(\n new Map()\n );\n const fieldRefs = React.useRef(new Map());\n\n const providerValidator: FormValidator = {\n run: async ({ signal, value }) => {\n const result = await validateSchema(value, signal);\n if (signal.aborted) {\n return;\n }\n if (result.success) {\n validatedValuesRef.current = result.data as FormValues;\n setValidationErrors([]);\n return;\n }\n setValidationErrors(result.errors);\n return toV2ValidationError(result.errors);\n },\n triggers: [\n {\n trigger: 'change',\n when: ({ formApi }) =>\n manualValidationRef.current ||\n shouldValidateForMode(\n formApi.state.submissionAttempts > 0\n ? revalidationMode\n : validationMode,\n 'change'\n ),\n },\n {\n trigger: 'blur',\n when: ({ formApi }) =>\n manualValidationRef.current ||\n shouldValidateForMode(\n formApi.state.submissionAttempts > 0\n ? revalidationMode\n : validationMode,\n 'blur'\n ),\n },\n ],\n };\n const form = useForm({\n ...(formOptions ?? {}),\n defaultValues: formDefaultValuesRef.current,\n validators: [...(formOptions?.validators ?? []), providerValidator],\n });\n const state = useSelector(form.atom, (current) => current);\n const fieldErrors = validationErrorsByPath(validationErrors);\n const dirtyFields = dirtyFieldsFromValues(\n state.values,\n cleanValuesRef.current\n );\n const isDirty = !state.isDefaultValue;\n const notifyDirtyChange = useDirtyStateNotification(isDirty, onDirtyChange);\n const errors: Record = {};\n for (const [path, messages] of [\n ...fieldErrors,\n ...nativeFieldErrors,\n ]) {\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 rootError =\n [\n ...new Set([\n ...errorMessages(state.errors),\n ...validationRootErrors,\n ...(submitError ? [submitError] : []),\n ]),\n ].join('\\n') || undefined;\n\n React.useEffect(function normalizeMountedDefaults() {\n if (!hasNormalizedDefaultsRef.current) {\n hasNormalizedDefaultsRef.current = true;\n const normalizedDefaults = form.state.values;\n formDefaultValuesRef.current = normalizedDefaults;\n cleanValuesRef.current = normalizedDefaults;\n form.reset(normalizedDefaults);\n notifyDirtyChange(false);\n }\n }, [form]);\n\n React.useEffect(function syncControlledValues() {\n if (values) {\n validatedValuesRef.current = undefined;\n setSubmitError(undefined);\n setValidationErrors([]);\n setNativeFieldErrors(new Map());\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(\n (error) => !targets.has(error.path.join('.'))\n );\n });\n };\n\n const contextValue: TanStackV2EngineContextValue = {\n clearFieldErrors: (name) =>\n setValidationErrors((current) =>\n current.filter((error) => error.path.join('.') !== name)\n ),\n fieldErrors,\n form,\n nativeFieldErrors,\n registerRef: (name, element) => {\n if (element) {\n fieldRefs.current.set(name, element);\n } else {\n fieldRefs.current.delete(name);\n }\n },\n setNativeFieldErrors,\n };\n\n const engine: AutoFormEngine = {\n ArrayController: TanStackV2ArrayController,\n FieldController: TanStackV2FieldController,\n clearErrors,\n defaultValues: cleanValuesRef.current,\n dirtyFields,\n errors,\n focus: (path) => fieldRefs.current.get(path)?.focus(),\n getFieldInvalid: (path) =>\n Boolean(\n fieldErrors.get(path)?.length ||\n nativeFieldErrors.get(path)?.length\n ),\n getValues: () => form.state.values,\n handleSubmit: (onValid) => (event) => {\n event.preventDefault();\n setIsAutoFormSubmitting(true);\n validatedValuesRef.current = undefined;\n void form\n .handleSubmit()\n .then(async (submitErrors) => {\n if (submitErrors.length === 0) {\n await onValid(\n validatedValuesRef.current ?? form.state.values\n );\n }\n })\n .catch((error: unknown) => {\n setSubmitError(\n error instanceof Error ? error.message : 'Submission failed.'\n );\n })\n .finally(() => setIsAutoFormSubmitting(false));\n },\n isDirty,\n isSubmitting: state.isSubmitting || isAutoFormSubmitting,\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 validatedValuesRef.current = undefined;\n setSubmitError(undefined);\n setValidationErrors([]);\n setNativeFieldErrors(new Map());\n if (!options?.keepDefaultValues) {\n formDefaultValuesRef.current = nextValues;\n cleanValuesRef.current = nextValues;\n }\n form.reset(nextValues, {\n updateDefaultValues: !options?.keepDefaultValues,\n });\n },\n rootError,\n setRootError: setSubmitError,\n setValidationErrors,\n setValue: (path, value, options) => {\n setValidationErrors((current) =>\n current.filter((error) => error.path.join('.') !== path)\n );\n Reflect.apply(form.setFieldValue, form, [\n toTanStackV2Path(path),\n value,\n {\n causeValidation: options?.shouldValidate,\n markAsDirty: options?.shouldDirty,\n markAsTouched: options?.shouldTouch,\n },\n ]);\n },\n trigger: async () => {\n manualValidationRef.current = true;\n try {\n const results = await form.validate('change');\n return results.length === 0;\n } finally {\n manualValidationRef.current = false;\n }\n },\n validatesSchema: true,\n values: state.values,\n };\n\n return (\n \n \n {children(engine)}\n \n \n );\n}\n", "type": "registry:component" } ], "type": "registry:block" }