Form flowLoop

Form flow

Loop

Learn how the loop element is used in the flow.


Usage

The loop element is used to define a loop.

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.Variables<{
      i: number;
      questions: {
        question: string;
        options: { value: string; label: string }[];
        default: string;
        correct: string;
      }[];
      right: number;
      wrong: number;
    }>,
    s.Loop<
      [
        s.Variables<{
          question: {
            question: string;
            options: { value: string; label: string }[];
            default: string;
            correct: string;
          };
        }>,
        s.Form<{ answer: string }>,
        s.Variables<{ i: number; right: number; wrong: number }>,
      ]
    >,
    s.Return<{ right: number; wrong: number }>,
  ];
  inputs: Record<never, never>;
  params: Record<never, never>;
};

const flow: Flow<Schema> = [
  {
    variables: () => ({
      i: 0,
      questions: [
        {
          question: "What is the capital of Australia?",
          options: [
            { value: "sydney", label: "Sydney" },
            { value: "melbourne", label: "Melbourne" },
            { value: "canberra", label: "Canberra" },
          ],
          default: "sydney",
          correct: "canberra",
        },
        {
          question: "Who painted 'The Starry Night'?",
          options: [
            { value: "picasso", label: "Pablo Picasso" },
            { value: "vangogh", label: "Vincent van Gogh" },
            { value: "dali", label: "Salvador Dalí" },
          ],
          default: "picasso",
          correct: "vangogh",
        },
        {
          question: "Which planet is known as the 'Red Planet'?",
          options: [
            { value: "earth", label: "Earth" },
            { value: "mars", label: "Mars" },
            { value: "jupiter", label: "Jupiter" },
          ],
          default: "earth",
          correct: "mars",
        },
        {
          question: "What is water's symbol?",
          options: [
            { value: "h2o", label: "H₂O" },
            { value: "co2", label: "CO₂" },
            { value: "o2", label: "O₂" },
          ],
          default: "h2o",
          correct: "h2o",
        },
        {
          question: "Who wrote 'Romeo and Juliet'?",
          options: [
            { value: "shakespeare", label: "William Shakespeare" },
            { value: "dickens", label: "Charles Dickens" },
            { value: "twain", label: "Mark Twain" },
          ],
          default: "shakespeare",
          correct: "shakespeare",
        },
      ],
      right: 0,
      wrong: 0,
    }),
  },
  {
    loop: {
      while: ({ i, questions }) => i < questions.length,
      do: [
        {
          variables: ({ i, questions }) => ({
            question: questions[i],
          }),
        },
        {
          form: {
            fields: ({ question, i }) => ({
              answer: [question.default, [i]],
            }),
            render: ({ fields, values, onBack, onNext }) => (
              <Form
                key={`answer_${values.i}`}
                defaultValues={fields}
                resolver={zodResolver(
                  z.object({
                    answer: z.string(),
                  }),
                )}
                heading={values.question.question}
                content={[
                  {
                    type: "select",
                    name: "answer",
                    label: "Answer",
                    placeholder: "Select an option",
                    options: values.question.options,
                  },
                ]}
                buttons={{
                  back: values.i > 0 ? "Back" : null,
                  next:
                    values.i < values.questions.length - 1 ? "Next" : "Submit",
                }}
                onBack={onBack}
                onNext={onNext}
              />
            ),
          },
        },
        {
          variables: ({ i, question, answer, right, wrong }) => ({
            i: i + 1,
            right: question.correct === answer ? right + 1 : right,
            wrong: question.correct === answer ? wrong : wrong + 1,
          }),
        },
      ],
    },
  },
  {
    return: ({ right, wrong }) => ({
      right,
      wrong,
    }),
  },
];

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} />;
}

We need to use the s.Loop type with the corresponding types.

type Schema = {
  // ...
  struct: [
    // ...
    s.Loop<
      [
        s.Variables<{
          question: {
            question: string;
            options: { value: string; label: string }[];
            default: string;
            correct: string;
          };
        }>,
        s.Form<{ answer: string }>,
        s.Variables<{ i: number; right: number; wrong: number }>,
      ]
    >,
    // ...
  ];
  // ...
};

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

const flow: Flow<Schema> = [
  // ...
  {
    loop: {
      while: ({ i, questions }) => i < questions.length,
      do: [
        {
          variables: ({ i, questions }) => ({
            question: questions[i],
          }),
        },
        {
          form: {
            fields: ({ question, i }) => ({
              answer: [question.default, [i]],
            }),
            render: ({ fields, values, onBack, onNext }) => (
              <Form
                key={`answer_${values.i}`}
                defaultValues={fields}
                resolver={zodResolver(
                  z.object({
                    answer: z.string(),
                  }),
                )}
                heading={values.question.question}
                content={[
                  {
                    type: "select",
                    name: "answer",
                    label: "Answer",
                    placeholder: "Select an option",
                    options: values.question.options,
                  },
                ]}
                buttons={{
                  back: values.i > 0 ? "Back" : null,
                  next:
                    values.i < values.questions.length - 1 ? "Next" : "Submit",
                }}
                onBack={onBack}
                onNext={onNext}
              />
            ),
          },
        },
        {
          variables: ({ i, question, answer, right, wrong }) => ({
            i: i + 1,
            right: question.correct === answer ? right + 1 : right,
            wrong: question.correct === answer ? wrong : wrong + 1,
          }),
        },
      ],
    },
  },
  // ...
];

The while property is a function that takes the input values and returns a boolean value. While it is true, the elements in do are used.

Forms in a loop need a dynamic key to ensure a unique value. Moreover, a value must be passed in the array of the fields function to avoid persistence across iterations.