import type { Meta, StoryObj } from "@storybook/react-vite"; import { delay, http, HttpResponse } from "msw"; import { useEffect, useState } from "react"; import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import { SubmissionType } from "../../interfaces"; import { InputText } from "../../molecules/forms/input-text/InputText"; import form from "../__fixtures__/form.fixture.json"; import formFirstname from "../__fixtures__/form-firstname.fixture.json"; import { useEditForm } from "../__fixtures__/useEditForm"; import { Form } from "./Form"; /** * The form component is the primary component of the system. It is what takes the form definition (json) and renders the * form into html. There are multiple ways to send the form to the Form component. The two main ways are to pass the `src` * prop with an url to the form definition, usually a form.io server. The other is to pass the `form` prop with the json * definition and optionally a `url` prop with the location of the form. * * ```tsx * import {Form} from "@tsed/react-formio/organisms/form/Form"; * ``` */ export default { title: "form/Form", component: Form, argTypes: { form: { description: "Instead of loading a form from the `src` url, you can preload the form definition and pass it in with the `form` prop. You should also set `url` if you are using any advanced components like file upload or oauth.", control: "object" }, src: { description: "The src of the form definition. This is commonly from a form.io server. When using src, the form will automatically submit the data to that url as well.", control: "text" }, url: { description: "The url of the form definition. The form will not be loaded from this url and the submission will not be saved here either. This is used for file upload, oauth and other components or actions that need to know where the server is. Use this in connection with `form`", control: "text" }, submission: { description: "Submission data to fill the form. You can either load a previous submission or create a submission with some pre-filled data. If you do not provide a submissions the form will initialize an empty submission using default values from the form.", control: "object" }, options: { description: "An options object that can pass options to the formio.js Form that is rendered. You can set options such as `readOnly`, `noAlerts` or `hide`. There are [many options to be found in the formio.js library](https://github.com/formio/formio.js/wiki/Form-Renderer#options).", control: "object" }, className: { control: "text" }, style: { control: "object" }, // onFormReady: { // description: // "A callback function that gets called when the form has rendered. It is useful for accessing the underlying @formio/js Webform instance.", // action: "onFormReady" // }, onPrevPage: { description: 'A callback function for Wizard forms that gets called when the "Previous" button is pressed.', action: "onPrevPage" }, onNextPage: { description: 'A callback function for Wizard forms that gets called when the "Next" button is pressed.', action: "onNextPage" }, onCancelSubmit: { description: "A callback function that gets called when the submission has been canceled.", action: "onCancelSubmit" }, onCancelComponent: { description: "A callback function that gets called when a component has been canceled.", action: "onCancelComponent" }, // onChange: { // description: "A callback function that gets called when a value in the submission has changed.", // action: "onChange" // }, onCustomEvent: { description: 'A callback function that is triggered from a button component configured with "Event" type.', action: "onCustomEvent" }, // onComponentChange: { // description: "A callback function that gets called when a specific component changes.", // action: "onComponentChange" // }, onSubmit: { description: "A callback function that gets called when the submission has started. If src is not a Form.io server URL, this will be the final submit event.", action: "onSubmit" }, onSubmitDone: { description: "A callback function that gets called when the submission has successfully been made to the server. This will only fire if src is set to a Form.io server URL.", action: "onSubmitDone" }, onSubmitError: { description: "A callback function that gets called when an error occurs during submission (e.g. a validation error).", action: "onSubmitError" }, onFormLoad: { description: "A callback function that gets called when the form is finished loading.", action: "onFormLoad" }, onError: { description: "A callback function that gets called when an error occurs during submission (e.g. a validation error).", action: "onError" }, onRender: { description: "A callback function that gets called when the form is finished rendering. param will depend on the form and display type.", action: "onRender" }, onAttach: { description: "Event", action: "onAttach" }, onBuild: { description: "Event", action: "onBuild" }, // onFocus: { // description: "Event", // action: "onFocus" // }, // onBlur: { // description: "Event", // action: "onBlur" // }, onInitialized: { description: "Event", action: "onInitialized" }, onLanguageChanged: { description: "Event", action: "onLanguageChanged" }, onBeforeSetSubmission: { description: "Event", action: "onBeforeSetSubmission" }, onSaveDraftBegin: { description: "Event", action: "onSaveDraftBegin" }, onSaveDraft: { description: "Event", action: "onSaveDraft" }, onRestoreDraft: { description: "Event", action: "onRestoreDraft" }, onSubmissionDeleted: { description: "Event", action: "onSubmissionDeleted" }, onRequestDone: { description: "Event", action: "onRequestDone" }, otherEvents: { description: 'A "catch-all" prop for subscribing to other events (for a complete list, see [our documentation](https://help.form.io/developers/form-development/form-renderer#form-events)).', control: "object" } }, tags: ["autodocs"] } satisfies Meta; type Story = StoryObj; /** * Form with `form` property. */ export const BasicUsageWithForm: Story = { args: { form: form as any, onFormReady: fn(), options: { template: "tailwind", iconset: "lu" } }, async play({ canvasElement, args }) { const canvas = within(canvasElement); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toBeInTheDocument(); }); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toHaveClass("formio-form-ready"); }); expect(args.onFormReady).toHaveBeenCalled(); expect(canvas.getByRole("textbox", { name: "Text Field" })).toBeInTheDocument(); } }; /** * Form with `src` property. */ export const BasicUsageWithSrc: Story = { args: { src: "https://example.form.io/example", options: { template: "tailwind", iconset: "lu" } }, async play({ canvasElement }) { const canvas = within(canvasElement); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toBeInTheDocument(); }); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toHaveClass("formio-form-ready"); }); expect(canvas.getByRole("textbox", { name: "First Name" })).toBeInTheDocument(); } }; /** * Form with `submission` property. */ export const WithSubmissionData: Story = { args: { form: formFirstname as never, options: { template: "tailwind", iconset: "lu" }, submission: { data: { firstName: "John", lastName: "Doe" } } }, async play({ canvasElement }) { const canvas = within(canvasElement); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toBeInTheDocument(); }); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toHaveClass("formio-form-ready"); }); const firstnameInput = canvas.getByRole("textbox", { name: "First name" }); const lastNameInput = canvas.getByRole("textbox", { name: "Last name" }); expect(firstnameInput).toHaveValue("John"); expect(lastNameInput).toHaveValue("Doe"); } }; /** * Form with `onSubmit` property. */ export const WithOnSubmit: Story = { render(args) { type OnSubmitData = { firstName?: string; [key: string]: unknown; }; // eslint-disable-next-line react-hooks/rules-of-hooks const [data, setData] = useState(() => args.submission!.data as OnSubmitData); // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { setData(args.submission!.data as OnSubmitData); }, [args.submission]); return ( <>
{ setTimeout(() => { setData(submission.data); }, 1000); }} />
Preview:
            {JSON.stringify(data, null, 2)}
          
setData({ ...data, firstName: value })} />
); }, args: { form: formFirstname as never, options: { template: "tailwind", iconset: "lu" }, submission: { data: { firstName: "John", lastName: "Doe" } } }, async play({ canvasElement }) { const canvas = within(canvasElement); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toBeInTheDocument(); }); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toHaveClass("formio-form-ready"); }); let firstnameInput = canvas.getByRole("textbox", { name: "First name" }); let lastNameInput = canvas.getByRole("textbox", { name: "Last name" }); expect(firstnameInput).toHaveValue("John"); expect(lastNameInput).toHaveValue("Doe"); await userEvent.clear(firstnameInput); await userEvent.type(firstnameInput, "Jane", { delay: 100 }); await waitFor(() => { expect(firstnameInput).toHaveValue("Jane"); }); await userEvent.clear(lastNameInput); await userEvent.type(lastNameInput, "Smith", { delay: 100 }); await waitFor(() => { expect(lastNameInput).toHaveValue("Smith"); }); let submitButton = canvas.getByRole("button", { name: "Submit" }); userEvent.click(submitButton); await delay(1200); firstnameInput = canvas.getByRole("textbox", { name: "First name" }); lastNameInput = canvas.getByRole("textbox", { name: "Last name" }); userEvent.clear(lastNameInput); userEvent.type(lastNameInput, "Endo", { delay: 100 }); await delay(1100); await waitFor(() => { expect(lastNameInput).toHaveValue("Endo"); }); submitButton = canvas.getByRole("button", { name: "Submit" }); userEvent.click(submitButton); await delay(1200); const rawFirstNameInput = canvas.getByRole("textbox", { name: "Raw First name" }); await waitFor(() => { expect(rawFirstNameInput).toHaveValue("Jane"); }); userEvent.clear(rawFirstNameInput); await delay(100); userEvent.type(rawFirstNameInput, "Romeo", { delay: 100 }); await waitFor(() => { expect(rawFirstNameInput).toHaveValue("Romeo"); }); firstnameInput = canvas.getByRole("textbox", { name: "First name" }); await waitFor(() => { expect(firstnameInput).toHaveValue("Romeo"); }); } }; /** * Form with custom validation hook */ export const CustomValidation: Story = { parameters: {}, args: { form: formFirstname as never, options: { hooks: { async customValidation(submission: SubmissionType, callback: (error: any) => void) { setTimeout(() => { callback({ message: "My custom message about this field", type: "custom", path: ["firstName"], level: "error" }); }, 200); } } } }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toBeInTheDocument(); }); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toHaveClass("formio-form-ready"); }); let firstnameInput = canvas.getByRole("textbox", { name: "First name" }); let lastNameInput = canvas.getByRole("textbox", { name: "Last name" }); await userEvent.clear(firstnameInput); await userEvent.type(firstnameInput, "Jane", { delay: 100 }); await waitFor(() => { expect(firstnameInput).toHaveValue("Jane"); }); await userEvent.clear(lastNameInput); await userEvent.type(lastNameInput, "Smith", { delay: 100 }); await waitFor(() => { expect(lastNameInput).toHaveValue("Smith"); }); let submitButton = canvas.getByRole("button", { name: "Submit" }); await userEvent.click(submitButton); await waitFor(async () => { expect(canvas.getByText("Please fix the following errors before submitting.")).toBeInTheDocument(); }); } }; /** * Fetch submission data from a server then use the custom `onAsyncSubmit` event to update the submission * data on a non form.io server. * * Formio support `form.action` property to send the form data to a custom server. * But here we want to handle the submission data manually and perform some custom action before sending the data to the server. */ export const FetchSubmissionWithCustomAction: Story = { args: { options: { template: "tailwind", iconset: "lu" } }, parameters: { chromatic: { disableSnapshot: false }, msw: { handlers: [ http.get("https://local.dev/form/Test", async () => { await delay(200); return HttpResponse.json(JSON.parse(JSON.stringify(formFirstname))); }), http.get("https://local.dev/form/Test/submissions/1", async () => { await delay(300); return HttpResponse.json({ firstName: "Jane", lastName: "Doe" }); }), http.put("https://local.dev/form/Test/submissions/1", async () => { await delay(800); return HttpResponse.json({ firstName: "Jane", lastName: "Doe" }); }) ] } }, render(args) { // eslint-disable-next-line react-hooks/rules-of-hooks const { loading, form, data, onSubmit } = useEditForm({ model: "Test", submissionId: "1" }); if (loading || !form) { return
Loading...
; } return (
Preview:
            {JSON.stringify(data, null, 2)}
          
); }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); await waitFor(() => { expect(canvas.getByTestId("loading")).toBeInTheDocument(); }); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toBeInTheDocument(); }); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toHaveClass("formio-form-ready"); }); await delay(200); let firstnameInput = canvas.getByRole("textbox", { name: "First name" }); let lastNameInput = canvas.getByRole("textbox", { name: "Last name" }); await userEvent.clear(firstnameInput); await waitFor(() => { expect(firstnameInput).toHaveValue(""); }); await userEvent.type(firstnameInput, "Jane", { delay: 100 }); await waitFor(() => { expect(firstnameInput).toHaveValue("Jane"); }); await userEvent.clear(lastNameInput); await userEvent.type(lastNameInput, "Smith", { delay: 100 }); await waitFor(() => { expect(lastNameInput).toHaveValue("Smith"); }); let submitButton = canvas.getByRole("button", { name: "Submit" }); await userEvent.click(submitButton); await delay(1000); await waitFor(() => { submitButton = canvas.getByRole("button", { name: "Submit" }); expect(submitButton.children).toHaveLength(1); }); } }; export const ErrorOnSubmitServer: Story = { args: { options: { template: "tailwind", iconset: "lu" } }, parameters: { chromatic: { disableSnapshot: false }, msw: { handlers: [ http.get("https://local.dev/form/Test2", async () => { await delay(200); return HttpResponse.json(JSON.parse(JSON.stringify(formFirstname))); }), http.get("https://local.dev/form/Test2/submissions/2", async () => { await delay(300); return HttpResponse.json({ firstName: "John", lastName: "Doe" }); }), http.put("https://local.dev/form/Test2/submissions/2", async () => { await delay(800); return HttpResponse.json( { message: "My custom message about this field", type: "custom", path: ["firstName"], level: "error" }, { status: 400 } ); }) ] } }, render(args) { // eslint-disable-next-line react-hooks/rules-of-hooks const { loading, form, data, onSubmit } = useEditForm({ model: "Test2", submissionId: "2" }); if (loading || !form) { return
Loading...
; } return (
Preview:
            {JSON.stringify(data, null, 2)}
          
); }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); await waitFor(() => { expect(canvas.getByTestId("loading")).toBeInTheDocument(); }); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toBeInTheDocument(); }); await waitFor(() => { expect(canvas.getByTestId("formio-container")).toHaveClass("formio-form-ready"); }); await delay(200); let firstnameInput = canvas.getByRole("textbox", { name: "First name" }); let lastNameInput = canvas.getByRole("textbox", { name: "Last name" }); await userEvent.clear(firstnameInput); await waitFor(() => { expect(firstnameInput).toHaveValue(""); }); await userEvent.type(firstnameInput, "Jane", { delay: 100 }); await waitFor(() => { expect(firstnameInput).toHaveValue("Jane"); }); await userEvent.clear(lastNameInput); await userEvent.type(lastNameInput, "Smith", { delay: 100 }); await waitFor(() => { expect(lastNameInput).toHaveValue("Smith"); }); let submitButton = canvas.getByRole("button", { name: "Submit" }); await userEvent.click(submitButton); await delay(1000); await waitFor(async () => { expect(canvas.getByText("Please fix the following errors before submitting.")).toBeInTheDocument(); }); } };