@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.
package/dist/useAssistantForm.js
CHANGED
|
@@ -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.
|
|
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":"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"}
|
|
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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 } }>();
|
package/src/useAssistantForm.ts
CHANGED
|
@@ -209,7 +209,14 @@ export const useAssistantForm = <
|
|
|
209
209
|
: field._f.ref;
|
|
210
210
|
|
|
211
211
|
if (fieldReference instanceof HTMLElement) {
|
|
212
|
-
|
|
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
|
}
|