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
keyto ensure a unique value. Moreover, a value must be passed in the array of thefieldsfunction to avoid persistence across iterations.