Form flowJump

Form flow

Jump

Learn how the jump element is used in the flow.


Usage

The jump element is used to enable jumping to a specific form step.

To understand how it is used let's look at this example.

import { useCallback, useState } from "react";

import {
  Formity,
  type s,
  type Flow,
  type OnReturn,
  type ReturnOutput,
} from "@formity/react";

import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

import { Form } from "./components/form";
import { Output } from "./components/output";

type Schema = {
  render: React.ReactNode;
  struct: [
    s.Form<{ name: string; surname: string; experience: string }>,
    s.Form<{ question1: string }>,
    s.Form<{ question2: string }>,
    s.Form<{ question3: string }>,
    s.Jump<s.Form<{ noticePeriod: string }>>,
    s.Return<{
      name: string;
      surname: string;
      experience: string;
      question1: string;
      question2: string;
      question3: string;
      noticePeriod: string;
    }>,
  ];
  inputs: Record<never, never>;
  params: Record<never, never>;
};

const flow: Flow<Schema> = [
  {
    form: {
      fields: () => ({
        name: ["", []],
        surname: ["", []],
        experience: ["", []],
      }),
      render: ({ fields, onBack, onNext, onJump }) => (
        <Form
          key="personal"
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              name: z.string().nonempty("Required"),
              surname: z.string().nonempty("Required"),
              experience: z.string().nonempty("Required"),
            }),
          )}
          heading="Tell us about yourself"
          content={[
            {
              type: "columns",
              columns: [
                {
                  type: "input",
                  name: "name",
                  label: "Name",
                  placeholder: "Your name",
                },
                {
                  type: "input",
                  name: "surname",
                  label: "Surname",
                  placeholder: "Your surname",
                },
              ],
            },
            {
              type: "select",
              name: "experience",
              label: "Years of experience",
              placeholder: "Select an option",
              options: [
                { value: "0-1", label: "Less than 1 year" },
                { value: "1-3", label: "1 – 3 years" },
                { value: "3-5", label: "3 – 5 years" },
                { value: "5-10", label: "5 – 10 years" },
                { value: "10+", label: "More than 10 years" },
              ],
            },
          ]}
          buttons={{
            skip: null,
            back: null,
            next: "Next",
          }}
          onBack={onBack}
          onNext={onNext}
          onJump={onJump}
          jumpId={null}
        />
      ),
    },
  },
  {
    form: {
      fields: () => ({
        question1: ["", []],
      }),
      render: ({ fields, onBack, onNext, onJump }) => (
        <Form
          key="question1"
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              question1: z.string().nonempty("Required"),
            }),
          )}
          heading="What is your preferred styling approach?"
          content={[
            {
              type: "select",
              name: "question1",
              label: "Styling approach",
              placeholder: "Select an option",
              options: [
                { value: "tailwind", label: "Tailwind CSS" },
                { value: "css-modules", label: "CSS Modules" },
                { value: "styled-components", label: "Styled Components" },
                { value: "plain-css", label: "Plain CSS" },
              ],
            },
          ]}
          buttons={{
            skip: "Skip to notice period",
            back: "Back",
            next: "Next",
          }}
          onBack={onBack}
          onNext={onNext}
          onJump={onJump}
          jumpId="noticePeriod"
        />
      ),
    },
  },
  {
    form: {
      fields: () => ({
        question2: ["", []],
      }),
      render: ({ fields, onBack, onNext, onJump }) => (
        <Form
          key="question2"
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              question2: z.string().nonempty("Required"),
            }),
          )}
          heading="Which framework do you use most?"
          content={[
            {
              type: "select",
              name: "question2",
              label: "Framework",
              placeholder: "Select an option",
              options: [
                { value: "react", label: "React" },
                { value: "vue", label: "Vue" },
                { value: "angular", label: "Angular" },
                { value: "svelte", label: "Svelte" },
              ],
            },
          ]}
          buttons={{
            skip: "Skip to notice period",
            back: "Back",
            next: "Next",
          }}
          onBack={onBack}
          onNext={onNext}
          onJump={onJump}
          jumpId="noticePeriod"
        />
      ),
    },
  },
  {
    form: {
      fields: () => ({
        question3: ["", []],
      }),
      render: ({ fields, onBack, onNext, onJump }) => (
        <Form
          key="question3"
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              question3: z.string().nonempty("Required"),
            }),
          )}
          heading="Which testing tool do you use most?"
          content={[
            {
              type: "select",
              name: "question3",
              label: "Testing tool",
              placeholder: "Select an option",
              options: [
                { value: "vitest", label: "Vitest" },
                { value: "jest", label: "Jest" },
                { value: "cypress", label: "Cypress" },
                { value: "playwright", label: "Playwright" },
              ],
            },
          ]}
          buttons={{
            skip: "Skip to notice period",
            back: "Back",
            next: "Next",
          }}
          onBack={onBack}
          onNext={onNext}
          onJump={onJump}
          jumpId="noticePeriod"
        />
      ),
    },
  },
  {
    jump: {
      id: "noticePeriod",
      at: {
        form: {
          fields: () => ({
            noticePeriod: ["", []],
          }),
          render: ({ fields, onBack, onNext, onJump }) => (
            <Form
              key="noticePeriod"
              defaultValues={fields}
              resolver={zodResolver(
                z.object({
                  noticePeriod: z.string().nonempty("Required"),
                }),
              )}
              heading="What is your notice period?"
              content={[
                {
                  type: "select",
                  name: "noticePeriod",
                  label: "Notice period",
                  placeholder: "Select an option",
                  options: [
                    { value: "immediate", label: "Immediate" },
                    { value: "2-weeks", label: "2 weeks" },
                    { value: "1-month", label: "1 month" },
                    { value: "2-months", label: "2 months" },
                    { value: "3-months", label: "3 months" },
                  ],
                },
              ]}
              buttons={{
                skip: null,
                back: "Back",
                next: "Submit",
              }}
              onBack={onBack}
              onNext={onNext}
              onJump={onJump}
              jumpId={null}
            />
          ),
        },
      },
    },
  },
  {
    return: ({
      name,
      surname,
      experience,
      question1,
      question2,
      question3,
      noticePeriod,
    }) => ({
      name,
      surname,
      experience,
      question1,
      question2,
      question3,
      noticePeriod,
    }),
  },
];

export default function App() {
  const [output, setOutput] = useState<ReturnOutput<Schema> | null>(null);

  const onReturn = useCallback<OnReturn<Schema>>((output) => {
    setOutput(output);
  }, []);

  if (output) {
    return <Output output={output} onStart={() => setOutput(null)} />;
  }

  return <Formity<Schema> flow={flow} onReturn={onReturn} />;
}

If you try this code, you will notice some errors that will soon be addressed, but for now you need to understand that we need to use the s.Jump type with the s.Form type.

type Schema = {
  // ...
  struct: [
    // ...
    s.Jump<s.Form<{ noticePeriod: string }>>,
    // ...
  ];
  // ...
};

Then, in the flow we need to create an object with the following structure.

const flow: Flow<Schema> = [
  // ...
  {
    jump: {
      id: "noticePeriod",
      at: {
        form: {
          fields: () => ({
            noticePeriod: ["", []],
          }),
          render: ({ fields, onBack, onNext, onJump }) => (
            <Form
              key="noticePeriod"
              defaultValues={fields}
              resolver={zodResolver(
                z.object({
                  noticePeriod: z.string().nonempty("Required"),
                }),
              )}
              heading="What is your notice period?"
              content={[
                {
                  type: "select",
                  name: "noticePeriod",
                  label: "Notice period",
                  placeholder: "Select an option",
                  options: [
                    { value: "immediate", label: "Immediate" },
                    { value: "2-weeks", label: "2 weeks" },
                    { value: "1-month", label: "1 month" },
                    { value: "2-months", label: "2 months" },
                    { value: "3-months", label: "3 months" },
                  ],
                },
              ]}
              buttons={{
                skip: null,
                back: "Back",
                next: "Submit",
              }}
              onBack={onBack}
              onNext={onNext}
              onJump={onJump}
              jumpId={null}
            />
          ),
        },
      },
    },
  },
  // ...
];

The id property is the id of the jump element and the at property contains the form.

If you look at the return, you'll see a type error when accessing values from previous forms. This is because you can jump here from anywhere, so those forms may not be completed.

To solve this, we can set default values in the inputs property of Formity. This ensures these values can be accessed from anywhere in the flow.

import { useCallback, useState } from "react";

import {
  Formity,
  type s,
  type Flow,
  type OnReturn,
  type ReturnOutput,
} from "@formity/react";

import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

import { Form } from "./components/form";
import { Output } from "./components/output";

type Schema = {
  render: React.ReactNode;
  struct: [
    s.Form<{ name: string; surname: string; experience: string }>,
    s.Form<{ question1: string }>,
    s.Form<{ question2: string }>,
    s.Form<{ question3: string }>,
    s.Jump<s.Form<{ noticePeriod: string }>>,
    s.Return<{
      name: string;
      surname: string;
      experience: string;
      question1: string;
      question2: string;
      question3: string;
      noticePeriod: string;
    }>,
  ];
  inputs: {
    name: string;
    surname: string;
    experience: string;
    question1: string;
    question2: string;
    question3: string;
  };
  params: Record<never, never>;
};

const flow: Flow<Schema> = [
  {
    form: {
      fields: () => ({
        name: ["", []],
        surname: ["", []],
        experience: ["", []],
      }),
      render: ({ fields, onBack, onNext, onJump }) => (
        <Form
          key="personal"
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              name: z.string().nonempty("Required"),
              surname: z.string().nonempty("Required"),
              experience: z.string().nonempty("Required"),
            }),
          )}
          heading="Tell us about yourself"
          content={[
            {
              type: "columns",
              columns: [
                {
                  type: "input",
                  name: "name",
                  label: "Name",
                  placeholder: "Your name",
                },
                {
                  type: "input",
                  name: "surname",
                  label: "Surname",
                  placeholder: "Your surname",
                },
              ],
            },
            {
              type: "select",
              name: "experience",
              label: "Years of experience",
              placeholder: "Select an option",
              options: [
                { value: "0-1", label: "Less than 1 year" },
                { value: "1-3", label: "1 – 3 years" },
                { value: "3-5", label: "3 – 5 years" },
                { value: "5-10", label: "5 – 10 years" },
                { value: "10+", label: "More than 10 years" },
              ],
            },
          ]}
          buttons={{
            skip: null,
            back: null,
            next: "Next",
          }}
          onBack={onBack}
          onNext={onNext}
          onJump={onJump}
          jumpId={null}
        />
      ),
    },
  },
  {
    form: {
      fields: () => ({
        question1: ["", []],
      }),
      render: ({ fields, onBack, onNext, onJump }) => (
        <Form
          key="question1"
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              question1: z.string().nonempty("Required"),
            }),
          )}
          heading="What is your preferred styling approach?"
          content={[
            {
              type: "select",
              name: "question1",
              label: "Styling approach",
              placeholder: "Select an option",
              options: [
                { value: "tailwind", label: "Tailwind CSS" },
                { value: "css-modules", label: "CSS Modules" },
                { value: "styled-components", label: "Styled Components" },
                { value: "plain-css", label: "Plain CSS" },
              ],
            },
          ]}
          buttons={{
            skip: "Skip to notice period",
            back: "Back",
            next: "Next",
          }}
          onBack={onBack}
          onNext={onNext}
          onJump={onJump}
          jumpId="noticePeriod"
        />
      ),
    },
  },
  {
    form: {
      fields: () => ({
        question2: ["", []],
      }),
      render: ({ fields, onBack, onNext, onJump }) => (
        <Form
          key="question2"
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              question2: z.string().nonempty("Required"),
            }),
          )}
          heading="Which framework do you use most?"
          content={[
            {
              type: "select",
              name: "question2",
              label: "Framework",
              placeholder: "Select an option",
              options: [
                { value: "react", label: "React" },
                { value: "vue", label: "Vue" },
                { value: "angular", label: "Angular" },
                { value: "svelte", label: "Svelte" },
              ],
            },
          ]}
          buttons={{
            skip: "Skip to notice period",
            back: "Back",
            next: "Next",
          }}
          onBack={onBack}
          onNext={onNext}
          onJump={onJump}
          jumpId="noticePeriod"
        />
      ),
    },
  },
  {
    form: {
      fields: () => ({
        question3: ["", []],
      }),
      render: ({ fields, onBack, onNext, onJump }) => (
        <Form
          key="question3"
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              question3: z.string().nonempty("Required"),
            }),
          )}
          heading="Which testing tool do you use most?"
          content={[
            {
              type: "select",
              name: "question3",
              label: "Testing tool",
              placeholder: "Select an option",
              options: [
                { value: "vitest", label: "Vitest" },
                { value: "jest", label: "Jest" },
                { value: "cypress", label: "Cypress" },
                { value: "playwright", label: "Playwright" },
              ],
            },
          ]}
          buttons={{
            skip: "Skip to notice period",
            back: "Back",
            next: "Next",
          }}
          onBack={onBack}
          onNext={onNext}
          onJump={onJump}
          jumpId="noticePeriod"
        />
      ),
    },
  },
  {
    jump: {
      id: "noticePeriod",
      at: {
        form: {
          fields: () => ({
            noticePeriod: ["", []],
          }),
          render: ({ fields, onBack, onNext, onJump }) => (
            <Form
              key="noticePeriod"
              defaultValues={fields}
              resolver={zodResolver(
                z.object({
                  noticePeriod: z.string().nonempty("Required"),
                }),
              )}
              heading="What is your notice period?"
              content={[
                {
                  type: "select",
                  name: "noticePeriod",
                  label: "Notice period",
                  placeholder: "Select an option",
                  options: [
                    { value: "immediate", label: "Immediate" },
                    { value: "2-weeks", label: "2 weeks" },
                    { value: "1-month", label: "1 month" },
                    { value: "2-months", label: "2 months" },
                    { value: "3-months", label: "3 months" },
                  ],
                },
              ]}
              buttons={{
                skip: null,
                back: "Back",
                next: "Submit",
              }}
              onBack={onBack}
              onNext={onNext}
              onJump={onJump}
              jumpId={null}
            />
          ),
        },
      },
    },
  },
  {
    return: ({
      name,
      surname,
      experience,
      question1,
      question2,
      question3,
      noticePeriod,
    }) => ({
      name,
      surname,
      experience,
      question1,
      question2,
      question3,
      noticePeriod,
    }),
  },
];

export default function App() {
  const [output, setOutput] = useState<ReturnOutput<Schema> | null>(null);

  const onReturn = useCallback<OnReturn<Schema>>((output) => {
    setOutput(output);
  }, []);

  if (output) {
    return <Output output={output} onStart={() => setOutput(null)} />;
  }

  return (
    <Formity<Schema>
      flow={flow}
      inputs={{
        name: "",
        surname: "",
        experience: "",
        question1: "",
        question2: "",
        question3: "",
      }}
      onReturn={onReturn}
    />
  );
}

Apart from that, we also need to update the Form component to use the new props.

import type { DefaultValues, Resolver } from "react-hook-form";
import type { OnBack, OnNext, OnJump } from "@formity/react";

import { useForm, FormProvider } from "react-hook-form";

import { ItemView, type Item } from "./item";

interface FormProps<T extends Record<string, unknown>> {
  defaultValues: DefaultValues<T>;
  resolver: Resolver<T>;
  heading: string;
  content: Item[];
  buttons: {
    skip: string | null;
    back: string | null;
    next: string;
  };
  onBack: OnBack<T>;
  onNext: OnNext<T>;
  onJump: OnJump<T>;
  jumpId: string;
}

export function Form<T extends Record<string, unknown>>({
  defaultValues,
  resolver,
  heading,
  content,
  buttons,
  onBack,
  onNext,
  onJump,
  jumpId,
}: FormProps<T>) {
  const form = useForm({ defaultValues, resolver });
  return (
    <form
      onSubmit={form.handleSubmit(onNext)}
      className="color-scheme-dark flex h-screen w-full items-center justify-center px-4 py-8"
      autoComplete="off"
    >
      <FormProvider {...form}>
        {buttons.skip && (
          <button
            type="button"
            onClick={() => onJump(jumpId, form.getValues())}
            className="fixed right-6 top-6 flex items-center gap-1 rounded-full border border-neutral-800 px-4 py-1.5 text-sm font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
          >
            {buttons.skip} <span aria-hidden="true"></span>
          </button>
        )}
        <div className="w-full max-w-md">
          <h2 className="mb-6 text-center text-4xl font-semibold text-white">
            {heading}
          </h2>
          <div className="mb-6 flex flex-col gap-4">
            {content.map((field, index) => (
              <ItemView key={index} {...field} />
            ))}
          </div>
          <div className="flex gap-4">
            {buttons.back && (
              <button
                type="button"
                onClick={() => onBack(form.getValues())}
                className="bg-neutral-90 w-full rounded-xl border border-neutral-800 px-6 py-2 text-base font-medium text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
              >
                {buttons.back}
              </button>
            )}
            <button
              type="submit"
              className="w-full rounded-xl border border-transparent bg-blue-500 px-6 py-2 text-base font-medium text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white active:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
            >
              {buttons.next}
            </button>
          </div>
        </div>
      </FormProvider>
    </form>
  );
}