Form schemaLoop

Form schema

Loop

Learn how the loop element is used in the schema.


Usage

The loop element is used to define a loop.

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

import type { Schema, Form, Return, Variables, Loop } from "@formity/react";

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

import {
  FormStep,
  FormStepContent,
  FormStepHeading,
  FormStepInputs,
  FormStepRow,
} from "./components/form-step";

import { Select } from "./components/input/select";
import { NextButton } from "./components/buttons/next-button";
import { BackButton } from "./components/buttons/back-button";

import { MultiStep } from "./multi-step";

export type Values = [
  Variables<{
    i: number;
    questions: {
      question: string;
      options: { value: string; label: string }[];
      default: string;
      correct: string;
    }[];
    right: number;
    wrong: number;
  }>,
  Loop<
    [
      Variables<{
        question: {
          question: string;
          options: { value: string; label: string }[];
          default: string;
          correct: string;
        };
      }>,
      Form<{ answer: string }>,
      Variables<{ i: number; right: number; wrong: number }>,
    ]
  >,
  Return<{ right: number; wrong: number }>,
];

export const schema: Schema<Values> = [
  {
    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: {
            values: ({ question, i }) => ({
              answer: [question.default, [i]],
            }),
            render: ({ inputs, values, onNext, onBack }) => (
              <MultiStep onNext={onNext} onBack={onBack}>
                <FormStep
                  key={`answer_${inputs.i}`}
                  defaultValues={values}
                  resolver={zodResolver(
                    z.object({
                      answer: z.string(),
                    }),
                  )}
                >
                  <FormStepContent>
                    <FormStepHeading>
                      {inputs.question.question}
                    </FormStepHeading>
                    <FormStepInputs>
                      <Select
                        name="answer"
                        label="Answer"
                        options={inputs.question.options}
                      />
                    </FormStepInputs>
                    <FormStepRow>
                      {inputs.i > 0 ? (
                        <BackButton onClick={onBack}>Back</BackButton>
                      ) : null}
                      {inputs.i < inputs.questions.length - 1 ? (
                        <NextButton>Next</NextButton>
                      ) : (
                        <NextButton>Submit</NextButton>
                      )}
                    </FormStepRow>
                  </FormStepContent>
                </FormStep>
              </MultiStep>
            ),
          },
        },
        {
          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,
    }),
  },
];

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

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

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

export const schema: Schema<Values> = [
  // ...
  {
    loop: {
      while: ({ i, questions }) => i < questions.length,
      do: [
        {
          variables: ({ i, questions }) => ({
            question: questions[i],
          }),
        },
        {
          form: {
            values: ({ question, i }) => ({
              answer: [question.default, [i]],
            }),
            render: ({ inputs, values, onNext, onBack }) => (
              <MultiStep onNext={onNext} onBack={onBack}>
                <FormStep
                  key={`answer_${inputs.i}`}
                  defaultValues={values}
                  resolver={zodResolver(
                    z.object({
                      answer: z.string(),
                    }),
                  )}
                >
                  <FormStepContent>
                    <FormStepHeading>
                      {inputs.question.question}
                    </FormStepHeading>
                    <FormStepInputs>
                      <Select
                        name="answer"
                        label="Answer"
                        options={inputs.question.options}
                      />
                    </FormStepInputs>
                    <FormStepRow>
                      {inputs.i > 0 ? (
                        <BackButton onClick={onBack}>Back</BackButton>
                      ) : null}
                      {inputs.i < inputs.questions.length - 1 ? (
                        <NextButton>Next</NextButton>
                      ) : (
                        <NextButton>Submit</NextButton>
                      )}
                    </FormStepRow>
                  </FormStepContent>
                </FormStep>
              </MultiStep>
            ),
          },
        },
        {
          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 values function to avoid persistence across iterations.