diff --git a/.changeset/yellow-lizards-rule.md b/.changeset/yellow-lizards-rule.md new file mode 100644 index 0000000..54dcfbf --- /dev/null +++ b/.changeset/yellow-lizards-rule.md @@ -0,0 +1,5 @@ +--- +"signal-form": minor +--- + +React to schema changes dynamically diff --git a/lib/create-form-context.tsx b/lib/create-form-context.tsx index 415806b..fa9cb00 100644 --- a/lib/create-form-context.tsx +++ b/lib/create-form-context.tsx @@ -1,5 +1,5 @@ import type { FormEventHandler } from "react"; -import type { Signal } from "@preact/signals-react"; +import type { ReadonlySignal, Signal } from "@preact/signals-react"; import { signal } from "@preact/signals-react"; import type { AnyObjectSchema, InferType } from "yup"; import type { FormContext } from "~/context"; @@ -12,7 +12,7 @@ type CreateFormContextOptions = { submittedData?: any; defaultData?: DeepPartial>; data?: Signal>; - schema?: S; + schema: ReadonlySignal; id: string; onSubmit?: FormEventHandler; }; @@ -33,8 +33,8 @@ export function createFormContext({ let didSubmit = signal(!!submittedData); function validate(): ValidationResult> { - if (schema) { - let validationResult = validateSync(schema, data.value); + if (schema.value) { + let validationResult = validateSync(schema.value, data.value); if (validationResult.ok) { errors.value = []; result.value = validationResult.data; diff --git a/lib/form.tsx b/lib/form.tsx index 180f013..8b6c77d 100644 --- a/lib/form.tsx +++ b/lib/form.tsx @@ -1,9 +1,9 @@ import type { FormHTMLAttributes, ForwardedRef, ReactNode } from "react"; -import { forwardRef, useId, useMemo } from "react"; -import type { Signal } from "@preact/signals-react"; +import { forwardRef, useEffect, useId, useMemo } from "react"; +import { useSignal, type Signal } from "@preact/signals-react"; import type { AnyObjectSchema, InferType } from "yup"; import { FieldsContext, FormContext } from "~/context"; -import type { ValidationError } from "~/utils/validate"; +import { type ValidationError } from "~/utils/validate"; import { createFormContext } from "./create-form-context"; import type { DeepPartial } from "./utils/deep-partial"; @@ -32,6 +32,7 @@ export const Form = forwardRef( ref: ForwardedRef ): JSX.Element => { let formId = useId(); + let schemaSignal = useSignal(schema); let formContext: FormContext = useMemo(() => { return createFormContext({ @@ -39,12 +40,17 @@ export const Form = forwardRef( submittedData, defaultData, data, - schema, + schema: schemaSignal, id: id || formId, onSubmit, }); }, []); + useEffect(() => { + schemaSignal.value = schema; + formContext.validate(); + }, [schema]); + return (
{ let actionData = useActionData(); let formId = useId(); + let schemaSignal = useSignal(schema); useEffect(() => { if (actionData?.errors) { @@ -51,12 +52,17 @@ export const Form = forwardRef( submittedData: actionData?.input, defaultData, data, - schema, + schema: schemaSignal, id: id || formId, onSubmit, }); }, []); + useEffect(() => { + schemaSignal.value = schema; + formContext.validate(); + }, [schema]); + return ( ; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render() { + let [required, setRequired] = useState(false); + + let Schema = schema.object().shape({ + title: required ? schema.string().required() : schema.string(), + }); + + return ( + +

+ +

+

+ +

+ + + ); + }, +}; + +export const Test: Story = { + ...Default, + play: async ({ canvasElement }) => { + let canvas = within(canvasElement); + + let requiredCheckbox = await canvas.findByLabelText("Required title"); + let titleInput = await canvas.findByLabelText("Title"); + + await userEvent.type(titleInput, "Hello", { delay: 10 }); + await userEvent.clear(titleInput); + + await expect(canvas.queryByText("title is a required field")).toBeNull(); + + await userEvent.click(requiredCheckbox); + + await canvas.findByText("title is a required field"); + await userEvent.type(titleInput, "Hello", { delay: 10 }); + + await expect(canvas.queryByText("title is a required field")).toBeNull(); + }, +};