@assistant-ui/react-hook-form 0.12.32 → 0.12.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,7 +88,8 @@ const useAssistantForm = (props) => {
88
88
  if (field?._f) {
89
89
  const fieldReference = Array.isArray(field._f.refs) ? field._f.refs[0] : field._f.ref;
90
90
  if (fieldReference instanceof HTMLElement) {
91
- formElement = fieldReference.closest("form");
91
+ if ("form" in fieldReference) formElement = fieldReference.form instanceof HTMLFormElement ? fieldReference.form : null;
92
+ else formElement = fieldReference.closest("form");
92
93
  if (formElement) break;
93
94
  }
94
95
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useAssistantForm.js","names":[],"sources":["../src/useAssistantForm.ts"],"sourcesContent":["\"use client\";\n\nimport { type ModelContext, tool } from \"@assistant-ui/core\";\nimport type {} from \"@assistant-ui/core/store\";\nimport { type ToolCallMessagePartComponent } from \"@assistant-ui/core/react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport {\n type Field,\n type FieldValues,\n type Path,\n type PathValue,\n type UseFormProps,\n type UseFormReturn,\n get,\n useForm,\n} from \"react-hook-form\";\nimport type { z } from \"zod\";\nimport { formTools } from \"./formTools\";\n\ntype PendingAssistantSubmit = {\n dispatching: boolean;\n event: unknown;\n handlerInvoked: boolean;\n outcome: boolean | undefined;\n resolve: (outcome: boolean) => void;\n reject: (error: unknown) => void;\n};\n\nexport type UseAssistantFormProps<\n TFieldValues extends FieldValues,\n TContext,\n TTransformedValues,\n> = UseFormProps<TFieldValues, TContext, TTransformedValues> & {\n assistant?:\n | {\n tools?:\n | {\n set_form_field?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<\n (typeof formTools.set_form_field)[\"parameters\"]\n >,\n unknown\n >\n | undefined;\n }\n | undefined;\n submit_form?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<(typeof formTools.submit_form)[\"parameters\"]>,\n unknown\n >\n | undefined;\n }\n | undefined;\n reset_form?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<(typeof formTools.reset_form)[\"parameters\"]>,\n unknown\n >\n | undefined;\n }\n | undefined;\n }\n | undefined;\n }\n | undefined;\n};\n\nexport const useAssistantForm = <\n TFieldValues extends FieldValues = FieldValues,\n TContext = any,\n TTransformedValues = TFieldValues,\n>(\n props?: UseAssistantFormProps<TFieldValues, TContext, TTransformedValues>,\n): UseFormReturn<TFieldValues, TContext, TTransformedValues> => {\n const form = useForm<TFieldValues, TContext, TTransformedValues>(props);\n const {\n control,\n getValues,\n setValue,\n reset,\n handleSubmit: baseHandleSubmit,\n formState: { isSubmitting },\n } = form;\n\n const pendingAssistantSubmitRef = useRef<PendingAssistantSubmit | null>(null);\n const settleAssistantSubmit = useCallback((outcome: boolean) => {\n const pending = pendingAssistantSubmitRef.current;\n if (!pending) return;\n\n pendingAssistantSubmitRef.current = null;\n pending.resolve(outcome);\n }, []);\n const rejectAssistantSubmit = useCallback((error: unknown) => {\n const pending = pendingAssistantSubmitRef.current;\n if (!pending) return;\n\n pendingAssistantSubmitRef.current = null;\n pending.reject(error);\n }, []);\n\n const handleSubmit = useCallback<\n UseFormReturn<TFieldValues, TContext, TTransformedValues>[\"handleSubmit\"]\n >(\n (onValid, onInvalid) => {\n const submit = baseHandleSubmit(\n (...args) => {\n const pending = pendingAssistantSubmitRef.current;\n const event = args[1]?.nativeEvent ?? args[1];\n if (pending && pending.event === event) pending.outcome = true;\n return onValid(...args);\n },\n (...args) => {\n const pending = pendingAssistantSubmitRef.current;\n const event = args[1]?.nativeEvent ?? args[1];\n if (pending && pending.event === event) pending.outcome = false;\n return onInvalid?.(...args);\n },\n );\n\n return (async (event) => {\n const pending = pendingAssistantSubmitRef.current;\n const nativeEvent = event?.nativeEvent ?? event;\n const assistantSubmit =\n pending &&\n (pending.event === nativeEvent ||\n (pending.dispatching &&\n pending.event === undefined &&\n nativeEvent !== undefined))\n ? pending\n : null;\n if (assistantSubmit) {\n assistantSubmit.event = nativeEvent;\n assistantSubmit.handlerInvoked = true;\n }\n\n try {\n const result = await submit(event);\n if (\n assistantSubmit &&\n pendingAssistantSubmitRef.current === assistantSubmit\n ) {\n settleAssistantSubmit(assistantSubmit.outcome ?? true);\n }\n return result;\n } catch (error) {\n if (pendingAssistantSubmitRef.current === assistantSubmit) {\n rejectAssistantSubmit(error);\n }\n throw error;\n }\n }) as typeof submit;\n },\n [baseHandleSubmit, rejectAssistantSubmit, settleAssistantSubmit],\n );\n const assistantForm = useMemo<\n UseFormReturn<TFieldValues, TContext, TTransformedValues>\n >(\n () => ({\n ...form,\n handleSubmit,\n get formState() {\n return form.formState;\n },\n }),\n [form, handleSubmit],\n );\n\n const aui = useAui();\n useEffect(() => {\n const value: ModelContext = {\n tools: {\n set_form_field: tool({\n ...formTools.set_form_field,\n parameters: formTools.set_form_field.parameters,\n execute: async (args) => {\n setValue(\n args.name as Path<TFieldValues>,\n args.value as PathValue<TFieldValues, Path<TFieldValues>>,\n );\n\n return { success: true };\n },\n }),\n submit_form: tool({\n ...formTools.submit_form,\n execute: async () => {\n if (isSubmitting || pendingAssistantSubmitRef.current) {\n return {\n success: false,\n message: \"The form is already submitting.\",\n };\n }\n const { _names, _fields } = control;\n let formElement: HTMLFormElement | null = null;\n for (const name of _names.mount) {\n const field: Field | undefined = get(_fields, name);\n if (field?._f) {\n const fieldReference = Array.isArray(field._f.refs)\n ? field._f.refs[0]\n : field._f.ref;\n\n if (fieldReference instanceof HTMLElement) {\n formElement = fieldReference.closest(\"form\");\n if (formElement) break;\n }\n }\n }\n\n if (formElement) {\n if (!formElement.noValidate && !formElement.reportValidity()) {\n return {\n success: false,\n message:\n \"The form contains invalid fields and was not submitted.\",\n };\n }\n\n let resolveSubmission: (outcome: boolean) => void = () => {};\n let rejectSubmission: (error: unknown) => void = () => {};\n const submissionResult = new Promise<boolean>(\n (resolve, reject) => {\n resolveSubmission = resolve;\n rejectSubmission = reject;\n },\n );\n const assistantSubmit: PendingAssistantSubmit = {\n dispatching: true,\n event: undefined,\n handlerInvoked: false,\n outcome: undefined,\n resolve: resolveSubmission,\n reject: rejectSubmission,\n };\n pendingAssistantSubmitRef.current = assistantSubmit;\n const onSubmit = (event: SubmitEvent) => {\n if (\n pendingAssistantSubmitRef.current === assistantSubmit &&\n assistantSubmit.event === undefined\n ) {\n assistantSubmit.event = event;\n }\n queueMicrotask(() => {\n if (\n pendingAssistantSubmitRef.current === assistantSubmit &&\n !assistantSubmit.handlerInvoked\n ) {\n settleAssistantSubmit(true);\n }\n });\n };\n\n formElement.addEventListener(\"submit\", onSubmit, { once: true });\n try {\n formElement.requestSubmit();\n } catch (error) {\n settleAssistantSubmit(false);\n throw error;\n } finally {\n assistantSubmit.dispatching = false;\n formElement.removeEventListener(\"submit\", onSubmit);\n }\n\n const dispatched = assistantSubmit.event !== undefined;\n if (!dispatched) {\n settleAssistantSubmit(false);\n }\n\n if (await submissionResult) return { success: true };\n return {\n success: false,\n message: dispatched\n ? \"The form contains invalid fields and was not submitted.\"\n : \"The form did not accept the submission.\",\n };\n }\n\n return {\n success: false,\n message:\n \"Unable retrieve the form element. This is a coding error.\",\n };\n },\n }),\n reset_form: tool({\n ...formTools.reset_form,\n execute: async () => {\n reset();\n return { success: true };\n },\n }),\n },\n };\n\n return aui.modelContext.register({\n getModelContext: () => ({\n ...value,\n system: `Form State:\\n${JSON.stringify(getValues())}`,\n }),\n });\n }, [\n control,\n setValue,\n getValues,\n aui,\n reset,\n isSubmitting,\n rejectAssistantSubmit,\n settleAssistantSubmit,\n ]);\n\n const renderFormFieldTool = props?.assistant?.tools?.set_form_field?.render;\n useEffect(() => {\n if (!renderFormFieldTool) return undefined;\n return aui.tools.setToolUI(\"set_form_field\", renderFormFieldTool);\n }, [aui, renderFormFieldTool]);\n\n const renderSubmitFormTool = props?.assistant?.tools?.submit_form?.render;\n useEffect(() => {\n if (!renderSubmitFormTool) return undefined;\n return aui.tools.setToolUI(\"submit_form\", renderSubmitFormTool);\n }, [aui, renderSubmitFormTool]);\n\n const renderResetFormTool = props?.assistant?.tools?.reset_form?.render;\n useEffect(() => {\n if (!renderResetFormTool) return undefined;\n return aui.tools.setToolUI(\"reset_form\", renderResetFormTool);\n }, [aui, renderResetFormTool]);\n\n return assistantForm;\n};\n"],"mappings":";;;;;;;;AA4EA,MAAa,oBAKX,UAC8D;CAC9D,MAAM,OAAO,QAAoD,KAAK;CACtE,MAAM,EACJ,SACA,WACA,UACA,OACA,cAAc,kBACd,WAAW,EAAE,mBACX;CAEJ,MAAM,4BAA4B,OAAsC,IAAI;CAC5E,MAAM,wBAAwB,aAAa,YAAqB;EAC9D,MAAM,UAAU,0BAA0B;EAC1C,IAAI,CAAC,SAAS;EAEd,0BAA0B,UAAU;EACpC,QAAQ,QAAQ,OAAO;CACzB,GAAG,CAAC,CAAC;CACL,MAAM,wBAAwB,aAAa,UAAmB;EAC5D,MAAM,UAAU,0BAA0B;EAC1C,IAAI,CAAC,SAAS;EAEd,0BAA0B,UAAU;EACpC,QAAQ,OAAO,KAAK;CACtB,GAAG,CAAC,CAAC;CAEL,MAAM,eAAe,aAGlB,SAAS,cAAc;EACtB,MAAM,SAAS,kBACZ,GAAG,SAAS;GACX,MAAM,UAAU,0BAA0B;GAC1C,MAAM,QAAQ,KAAK,EAAE,EAAE,eAAe,KAAK;GAC3C,IAAI,WAAW,QAAQ,UAAU,OAAO,QAAQ,UAAU;GAC1D,OAAO,QAAQ,GAAG,IAAI;EACxB,IACC,GAAG,SAAS;GACX,MAAM,UAAU,0BAA0B;GAC1C,MAAM,QAAQ,KAAK,EAAE,EAAE,eAAe,KAAK;GAC3C,IAAI,WAAW,QAAQ,UAAU,OAAO,QAAQ,UAAU;GAC1D,OAAO,YAAY,GAAG,IAAI;EAC5B,CACF;EAEA,QAAQ,OAAO,UAAU;GACvB,MAAM,UAAU,0BAA0B;GAC1C,MAAM,cAAc,OAAO,eAAe;GAC1C,MAAM,kBACJ,YACC,QAAQ,UAAU,eAChB,QAAQ,eACP,QAAQ,UAAU,KAAA,KAClB,gBAAgB,KAAA,KAChB,UACA;GACN,IAAI,iBAAiB;IACnB,gBAAgB,QAAQ;IACxB,gBAAgB,iBAAiB;GACnC;GAEA,IAAI;IACF,MAAM,SAAS,MAAM,OAAO,KAAK;IACjC,IACE,mBACA,0BAA0B,YAAY,iBAEtC,sBAAsB,gBAAgB,WAAW,IAAI;IAEvD,OAAO;GACT,SAAS,OAAO;IACd,IAAI,0BAA0B,YAAY,iBACxC,sBAAsB,KAAK;IAE7B,MAAM;GACR;EACF;CACF,GACA;EAAC;EAAkB;EAAuB;CAAqB,CACjE;CACA,MAAM,gBAAgB,eAGb;EACL,GAAG;EACH;EACA,IAAI,YAAY;GACd,OAAO,KAAK;EACd;CACF,IACA,CAAC,MAAM,YAAY,CACrB;CAEA,MAAM,MAAM,OAAO;CACnB,gBAAgB;EACd,MAAM,QAAsB,EAC1B,OAAO;GACL,gBAAgB,KAAK;IACnB,GAAG,UAAU;IACb,YAAY,UAAU,eAAe;IACrC,SAAS,OAAO,SAAS;KACvB,SACE,KAAK,MACL,KAAK,KACP;KAEA,OAAO,EAAE,SAAS,KAAK;IACzB;GACF,CAAC;GACD,aAAa,KAAK;IAChB,GAAG,UAAU;IACb,SAAS,YAAY;KACnB,IAAI,gBAAgB,0BAA0B,SAC5C,OAAO;MACL,SAAS;MACT,SAAS;KACX;KAEF,MAAM,EAAE,QAAQ,YAAY;KAC5B,IAAI,cAAsC;KAC1C,KAAK,MAAM,QAAQ,OAAO,OAAO;MAC/B,MAAM,QAA2B,IAAI,SAAS,IAAI;MAClD,IAAI,OAAO,IAAI;OACb,MAAM,iBAAiB,MAAM,QAAQ,MAAM,GAAG,IAAI,IAC9C,MAAM,GAAG,KAAK,KACd,MAAM,GAAG;OAEb,IAAI,0BAA0B,aAAa;QACzC,cAAc,eAAe,QAAQ,MAAM;QAC3C,IAAI,aAAa;OACnB;MACF;KACF;KAEA,IAAI,aAAa;MACf,IAAI,CAAC,YAAY,cAAc,CAAC,YAAY,eAAe,GACzD,OAAO;OACL,SAAS;OACT,SACE;MACJ;MAGF,IAAI,0BAAsD,CAAC;MAC3D,IAAI,yBAAmD,CAAC;MACxD,MAAM,mBAAmB,IAAI,SAC1B,SAAS,WAAW;OACnB,oBAAoB;OACpB,mBAAmB;MACrB,CACF;MACA,MAAM,kBAA0C;OAC9C,aAAa;OACb,OAAO,KAAA;OACP,gBAAgB;OAChB,SAAS,KAAA;OACT,SAAS;OACT,QAAQ;MACV;MACA,0BAA0B,UAAU;MACpC,MAAM,YAAY,UAAuB;OACvC,IACE,0BAA0B,YAAY,mBACtC,gBAAgB,UAAU,KAAA,GAE1B,gBAAgB,QAAQ;OAE1B,qBAAqB;QACnB,IACE,0BAA0B,YAAY,mBACtC,CAAC,gBAAgB,gBAEjB,sBAAsB,IAAI;OAE9B,CAAC;MACH;MAEA,YAAY,iBAAiB,UAAU,UAAU,EAAE,MAAM,KAAK,CAAC;MAC/D,IAAI;OACF,YAAY,cAAc;MAC5B,SAAS,OAAO;OACd,sBAAsB,KAAK;OAC3B,MAAM;MACR,UAAU;OACR,gBAAgB,cAAc;OAC9B,YAAY,oBAAoB,UAAU,QAAQ;MACpD;MAEA,MAAM,aAAa,gBAAgB,UAAU,KAAA;MAC7C,IAAI,CAAC,YACH,sBAAsB,KAAK;MAG7B,IAAI,MAAM,kBAAkB,OAAO,EAAE,SAAS,KAAK;MACnD,OAAO;OACL,SAAS;OACT,SAAS,aACL,4DACA;MACN;KACF;KAEA,OAAO;MACL,SAAS;MACT,SACE;KACJ;IACF;GACF,CAAC;GACD,YAAY,KAAK;IACf,GAAG,UAAU;IACb,SAAS,YAAY;KACnB,MAAM;KACN,OAAO,EAAE,SAAS,KAAK;IACzB;GACF,CAAC;EACH,EACF;EAEA,OAAO,IAAI,aAAa,SAAS,EAC/B,wBAAwB;GACtB,GAAG;GACH,QAAQ,gBAAgB,KAAK,UAAU,UAAU,CAAC;EACpD,GACF,CAAC;CACH,GAAG;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,MAAM,sBAAsB,OAAO,WAAW,OAAO,gBAAgB;CACrE,gBAAgB;EACd,IAAI,CAAC,qBAAqB,OAAO,KAAA;EACjC,OAAO,IAAI,MAAM,UAAU,kBAAkB,mBAAmB;CAClE,GAAG,CAAC,KAAK,mBAAmB,CAAC;CAE7B,MAAM,uBAAuB,OAAO,WAAW,OAAO,aAAa;CACnE,gBAAgB;EACd,IAAI,CAAC,sBAAsB,OAAO,KAAA;EAClC,OAAO,IAAI,MAAM,UAAU,eAAe,oBAAoB;CAChE,GAAG,CAAC,KAAK,oBAAoB,CAAC;CAE9B,MAAM,sBAAsB,OAAO,WAAW,OAAO,YAAY;CACjE,gBAAgB;EACd,IAAI,CAAC,qBAAqB,OAAO,KAAA;EACjC,OAAO,IAAI,MAAM,UAAU,cAAc,mBAAmB;CAC9D,GAAG,CAAC,KAAK,mBAAmB,CAAC;CAE7B,OAAO;AACT"}
1
+ {"version":3,"file":"useAssistantForm.js","names":[],"sources":["../src/useAssistantForm.ts"],"sourcesContent":["\"use client\";\n\nimport { type ModelContext, tool } from \"@assistant-ui/core\";\nimport type {} from \"@assistant-ui/core/store\";\nimport { type ToolCallMessagePartComponent } from \"@assistant-ui/core/react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport {\n type Field,\n type FieldValues,\n type Path,\n type PathValue,\n type UseFormProps,\n type UseFormReturn,\n get,\n useForm,\n} from \"react-hook-form\";\nimport type { z } from \"zod\";\nimport { formTools } from \"./formTools\";\n\ntype PendingAssistantSubmit = {\n dispatching: boolean;\n event: unknown;\n handlerInvoked: boolean;\n outcome: boolean | undefined;\n resolve: (outcome: boolean) => void;\n reject: (error: unknown) => void;\n};\n\nexport type UseAssistantFormProps<\n TFieldValues extends FieldValues,\n TContext,\n TTransformedValues,\n> = UseFormProps<TFieldValues, TContext, TTransformedValues> & {\n assistant?:\n | {\n tools?:\n | {\n set_form_field?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<\n (typeof formTools.set_form_field)[\"parameters\"]\n >,\n unknown\n >\n | undefined;\n }\n | undefined;\n submit_form?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<(typeof formTools.submit_form)[\"parameters\"]>,\n unknown\n >\n | undefined;\n }\n | undefined;\n reset_form?:\n | {\n render?:\n | ToolCallMessagePartComponent<\n z.infer<(typeof formTools.reset_form)[\"parameters\"]>,\n unknown\n >\n | undefined;\n }\n | undefined;\n }\n | undefined;\n }\n | undefined;\n};\n\nexport const useAssistantForm = <\n TFieldValues extends FieldValues = FieldValues,\n TContext = any,\n TTransformedValues = TFieldValues,\n>(\n props?: UseAssistantFormProps<TFieldValues, TContext, TTransformedValues>,\n): UseFormReturn<TFieldValues, TContext, TTransformedValues> => {\n const form = useForm<TFieldValues, TContext, TTransformedValues>(props);\n const {\n control,\n getValues,\n setValue,\n reset,\n handleSubmit: baseHandleSubmit,\n formState: { isSubmitting },\n } = form;\n\n const pendingAssistantSubmitRef = useRef<PendingAssistantSubmit | null>(null);\n const settleAssistantSubmit = useCallback((outcome: boolean) => {\n const pending = pendingAssistantSubmitRef.current;\n if (!pending) return;\n\n pendingAssistantSubmitRef.current = null;\n pending.resolve(outcome);\n }, []);\n const rejectAssistantSubmit = useCallback((error: unknown) => {\n const pending = pendingAssistantSubmitRef.current;\n if (!pending) return;\n\n pendingAssistantSubmitRef.current = null;\n pending.reject(error);\n }, []);\n\n const handleSubmit = useCallback<\n UseFormReturn<TFieldValues, TContext, TTransformedValues>[\"handleSubmit\"]\n >(\n (onValid, onInvalid) => {\n const submit = baseHandleSubmit(\n (...args) => {\n const pending = pendingAssistantSubmitRef.current;\n const event = args[1]?.nativeEvent ?? args[1];\n if (pending && pending.event === event) pending.outcome = true;\n return onValid(...args);\n },\n (...args) => {\n const pending = pendingAssistantSubmitRef.current;\n const event = args[1]?.nativeEvent ?? args[1];\n if (pending && pending.event === event) pending.outcome = false;\n return onInvalid?.(...args);\n },\n );\n\n return (async (event) => {\n const pending = pendingAssistantSubmitRef.current;\n const nativeEvent = event?.nativeEvent ?? event;\n const assistantSubmit =\n pending &&\n (pending.event === nativeEvent ||\n (pending.dispatching &&\n pending.event === undefined &&\n nativeEvent !== undefined))\n ? pending\n : null;\n if (assistantSubmit) {\n assistantSubmit.event = nativeEvent;\n assistantSubmit.handlerInvoked = true;\n }\n\n try {\n const result = await submit(event);\n if (\n assistantSubmit &&\n pendingAssistantSubmitRef.current === assistantSubmit\n ) {\n settleAssistantSubmit(assistantSubmit.outcome ?? true);\n }\n return result;\n } catch (error) {\n if (pendingAssistantSubmitRef.current === assistantSubmit) {\n rejectAssistantSubmit(error);\n }\n throw error;\n }\n }) as typeof submit;\n },\n [baseHandleSubmit, rejectAssistantSubmit, settleAssistantSubmit],\n );\n const assistantForm = useMemo<\n UseFormReturn<TFieldValues, TContext, TTransformedValues>\n >(\n () => ({\n ...form,\n handleSubmit,\n get formState() {\n return form.formState;\n },\n }),\n [form, handleSubmit],\n );\n\n const aui = useAui();\n useEffect(() => {\n const value: ModelContext = {\n tools: {\n set_form_field: tool({\n ...formTools.set_form_field,\n parameters: formTools.set_form_field.parameters,\n execute: async (args) => {\n setValue(\n args.name as Path<TFieldValues>,\n args.value as PathValue<TFieldValues, Path<TFieldValues>>,\n );\n\n return { success: true };\n },\n }),\n submit_form: tool({\n ...formTools.submit_form,\n execute: async () => {\n if (isSubmitting || pendingAssistantSubmitRef.current) {\n return {\n success: false,\n message: \"The form is already submitting.\",\n };\n }\n const { _names, _fields } = control;\n let formElement: HTMLFormElement | null = null;\n for (const name of _names.mount) {\n const field: Field | undefined = get(_fields, name);\n if (field?._f) {\n const fieldReference = Array.isArray(field._f.refs)\n ? field._f.refs[0]\n : field._f.ref;\n\n if (fieldReference instanceof HTMLElement) {\n if (\"form\" in fieldReference) {\n formElement =\n fieldReference.form instanceof HTMLFormElement\n ? fieldReference.form\n : null;\n } else {\n formElement = fieldReference.closest(\"form\");\n }\n if (formElement) break;\n }\n }\n }\n\n if (formElement) {\n if (!formElement.noValidate && !formElement.reportValidity()) {\n return {\n success: false,\n message:\n \"The form contains invalid fields and was not submitted.\",\n };\n }\n\n let resolveSubmission: (outcome: boolean) => void = () => {};\n let rejectSubmission: (error: unknown) => void = () => {};\n const submissionResult = new Promise<boolean>(\n (resolve, reject) => {\n resolveSubmission = resolve;\n rejectSubmission = reject;\n },\n );\n const assistantSubmit: PendingAssistantSubmit = {\n dispatching: true,\n event: undefined,\n handlerInvoked: false,\n outcome: undefined,\n resolve: resolveSubmission,\n reject: rejectSubmission,\n };\n pendingAssistantSubmitRef.current = assistantSubmit;\n const onSubmit = (event: SubmitEvent) => {\n if (\n pendingAssistantSubmitRef.current === assistantSubmit &&\n assistantSubmit.event === undefined\n ) {\n assistantSubmit.event = event;\n }\n queueMicrotask(() => {\n if (\n pendingAssistantSubmitRef.current === assistantSubmit &&\n !assistantSubmit.handlerInvoked\n ) {\n settleAssistantSubmit(true);\n }\n });\n };\n\n formElement.addEventListener(\"submit\", onSubmit, { once: true });\n try {\n formElement.requestSubmit();\n } catch (error) {\n settleAssistantSubmit(false);\n throw error;\n } finally {\n assistantSubmit.dispatching = false;\n formElement.removeEventListener(\"submit\", onSubmit);\n }\n\n const dispatched = assistantSubmit.event !== undefined;\n if (!dispatched) {\n settleAssistantSubmit(false);\n }\n\n if (await submissionResult) return { success: true };\n return {\n success: false,\n message: dispatched\n ? \"The form contains invalid fields and was not submitted.\"\n : \"The form did not accept the submission.\",\n };\n }\n\n return {\n success: false,\n message:\n \"Unable retrieve the form element. This is a coding error.\",\n };\n },\n }),\n reset_form: tool({\n ...formTools.reset_form,\n execute: async () => {\n reset();\n return { success: true };\n },\n }),\n },\n };\n\n return aui.modelContext.register({\n getModelContext: () => ({\n ...value,\n system: `Form State:\\n${JSON.stringify(getValues())}`,\n }),\n });\n }, [\n control,\n setValue,\n getValues,\n aui,\n reset,\n isSubmitting,\n rejectAssistantSubmit,\n settleAssistantSubmit,\n ]);\n\n const renderFormFieldTool = props?.assistant?.tools?.set_form_field?.render;\n useEffect(() => {\n if (!renderFormFieldTool) return undefined;\n return aui.tools.setToolUI(\"set_form_field\", renderFormFieldTool);\n }, [aui, renderFormFieldTool]);\n\n const renderSubmitFormTool = props?.assistant?.tools?.submit_form?.render;\n useEffect(() => {\n if (!renderSubmitFormTool) return undefined;\n return aui.tools.setToolUI(\"submit_form\", renderSubmitFormTool);\n }, [aui, renderSubmitFormTool]);\n\n const renderResetFormTool = props?.assistant?.tools?.reset_form?.render;\n useEffect(() => {\n if (!renderResetFormTool) return undefined;\n return aui.tools.setToolUI(\"reset_form\", renderResetFormTool);\n }, [aui, renderResetFormTool]);\n\n return assistantForm;\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-hook-form",
3
- "version": "0.12.32",
3
+ "version": "0.12.33",
4
4
  "description": "React Hook Form integration for assistant-ui",
5
5
  "keywords": [
6
6
  "react-hook-form",
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@assistant-ui/core": "^0.3.18",
32
+ "@assistant-ui/core": "^0.3.19",
33
33
  "@assistant-ui/store": "^0.3.13",
34
34
  "zod": "^4.5.4"
35
35
  },
@@ -44,14 +44,14 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@assistant-ui/x-buildutils": "0.0.26",
47
+ "@assistant-ui/x-buildutils": "0.0.27",
48
48
  "@testing-library/react": "^16.3.3",
49
49
  "@types/react": "^19.2.18",
50
50
  "@types/react-dom": "^19.2.7",
51
51
  "jsdom": "^30.0.1",
52
52
  "react": "^19.2.8",
53
53
  "react-hook-form": "^7.87.0",
54
- "vitest": "^4.1.11"
54
+ "vitest": "^5.0.0"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public",
@@ -1,6 +1,7 @@
1
1
  /** @vitest-environment jsdom */
2
2
  import {
3
3
  act,
4
+ cleanup,
4
5
  fireEvent,
5
6
  render,
6
7
  renderHook,
@@ -9,7 +10,7 @@ import {
9
10
  import type { ModelContext } from "@assistant-ui/core";
10
11
  import type { FormEvent, ReactNode } from "react";
11
12
  import type { Resolver, ResolverResult } from "react-hook-form";
12
- import { beforeEach, describe, expect, it, vi } from "vitest";
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
14
 
14
15
  const mocks = vi.hoisted(() => {
15
16
  const register = vi.fn();
@@ -34,6 +35,8 @@ import { useAssistantForm } from "./useAssistantForm";
34
35
 
35
36
  let provider: { getModelContext: () => ModelContext };
36
37
 
38
+ afterEach(cleanup);
39
+
37
40
  beforeEach(() => {
38
41
  mocks.register.mockReset();
39
42
  mocks.setToolUI.mockReset();
@@ -99,6 +102,115 @@ describe("useAssistantForm", () => {
99
102
  });
100
103
  });
101
104
 
105
+ describe.each(["input", "textarea", "select", "radio"])(
106
+ "%s form ownership",
107
+ (control) => {
108
+ it.each([false, true])(
109
+ "submits the explicit owner with an ancestor form: %s",
110
+ async (nested) => {
111
+ const submitted = vi.fn((event: FormEvent<HTMLFormElement>) =>
112
+ event.preventDefault(),
113
+ );
114
+ const wrongSubmitted = vi.fn((event: FormEvent<HTMLFormElement>) =>
115
+ event.preventDefault(),
116
+ );
117
+ const Fields = () => {
118
+ const form = useAssistantForm<{ name: string }>();
119
+ if (control === "textarea")
120
+ return <textarea form="owner" {...form.register("name")} />;
121
+ if (control === "select")
122
+ return (
123
+ <select form="owner" {...form.register("name")}>
124
+ <option value="a">A</option>
125
+ </select>
126
+ );
127
+ if (control === "radio")
128
+ return (
129
+ <>
130
+ <input
131
+ type="radio"
132
+ value="a"
133
+ form="owner"
134
+ {...form.register("name")}
135
+ />
136
+ <input
137
+ type="radio"
138
+ value="b"
139
+ form="owner"
140
+ {...form.register("name")}
141
+ />
142
+ </>
143
+ );
144
+ return <input form="owner" {...form.register("name")} />;
145
+ };
146
+ render(
147
+ <>
148
+ {nested ? (
149
+ <form onSubmit={wrongSubmitted}>
150
+ <Fields />
151
+ </form>
152
+ ) : (
153
+ <Fields />
154
+ )}
155
+ <form id="owner" onSubmit={submitted} />
156
+ </>,
157
+ );
158
+
159
+ await expect(executeSubmitForm()).resolves.toEqual({ success: true });
160
+ expect(submitted).toHaveBeenCalledOnce();
161
+ expect(wrongSubmitted).not.toHaveBeenCalled();
162
+ },
163
+ );
164
+ },
165
+ );
166
+
167
+ it("does not fall back to an ancestor when the explicit owner does not exist", async () => {
168
+ const submitted = vi.fn((event: FormEvent<HTMLFormElement>) =>
169
+ event.preventDefault(),
170
+ );
171
+ const Fields = () => {
172
+ const form = useAssistantForm<{ name: string }>();
173
+ return <input form="missing-owner" {...form.register("name")} />;
174
+ };
175
+ render(
176
+ <form onSubmit={submitted}>
177
+ <Fields />
178
+ </form>,
179
+ );
180
+
181
+ await expect(executeSubmitForm()).resolves.toEqual({
182
+ success: false,
183
+ message: "Unable retrieve the form element. This is a coding error.",
184
+ });
185
+ expect(submitted).not.toHaveBeenCalled();
186
+ });
187
+
188
+ it("keeps the ancestor fallback for custom refs without a form owner property", async () => {
189
+ await expectRegisteredFieldsToSubmit(() => {
190
+ const form = useAssistantForm<{ name: string }>();
191
+ return <div ref={form.register("name").ref} />;
192
+ });
193
+ });
194
+
195
+ it("validates external controls before submitting their owner", async () => {
196
+ const submitted = vi.fn((event: FormEvent<HTMLFormElement>) =>
197
+ event.preventDefault(),
198
+ );
199
+ const Fields = () => {
200
+ const form = useAssistantForm<{ name: string }>();
201
+ return <input required form="owner" {...form.register("name")} />;
202
+ };
203
+ render(
204
+ <>
205
+ <Fields />
206
+ <form id="owner" onSubmit={submitted} />
207
+ </>,
208
+ );
209
+
210
+ await expectSubmitBlocked();
211
+ expect(submitted).not.toHaveBeenCalled();
212
+ });
213
+
102
214
  it("submits forms registered with nested inputs", async () => {
103
215
  await expectRegisteredFieldsToSubmit(() => {
104
216
  const form = useAssistantForm<{ profile: { name: string } }>();
@@ -209,7 +209,14 @@ export const useAssistantForm = <
209
209
  : field._f.ref;
210
210
 
211
211
  if (fieldReference instanceof HTMLElement) {
212
- formElement = fieldReference.closest("form");
212
+ if ("form" in fieldReference) {
213
+ formElement =
214
+ fieldReference.form instanceof HTMLFormElement
215
+ ? fieldReference.form
216
+ : null;
217
+ } else {
218
+ formElement = fieldReference.closest("form");
219
+ }
213
220
  if (formElement) break;
214
221
  }
215
222
  }