Sub form

formo provides a simple way to create subforms. A subForm is a nested form which requires its own validations. The typical use case is when you have a field which is a variable list of complex elements.

For example, let's say you have a form asking the user their name, surname and familyMembers. The familyMembers, in turn, have their own fields (name and surname) that need to be validated before being added to the user's field array.

tsx
import { useFormo, subFormValue, success, validators } from "@buildo/formo";
 
type FamilyMember = {
name: string;
surname: string;
};
 
export const MyForm = () => {
const { handleSubmit, fieldProps, subForm, formErrors } = useFormo(
{
initialValues: {
name: "Jhon",
surname: "Doe",
familyMembers: subFormValue<Array<FamilyMember>>([]),
},
fieldValidators: () => ({}),
subFormValidators: () => ({
familyMembers: {
name: validators.minLength(1, "Family member name is required"),
surname: validators.minLength(1, "Family member surname is required"),
},
}),
},
{
onSubmit: () => Promise.resolve(success(null)),
}
);
 
const emptyFamilyMember = {
name: "",
surname: "",
};
 
return (
<div>
<TextField label="name" {...fieldProps("name")} />
<TextField label="surname" {...fieldProps("surname")} />
 
<div>
<button
onClick={() => subForm("familyMembers").push(emptyFamilyMember)}
>
Add family members
</button>
 
{subForm("familyMembers").items.map((familyMember, index) => (
<div key={`${familyMember.namePrefix}-container`}>
<TextField
label={`${index}-name`}
{...familyMember.fieldProps("name")}
/>
<TextField
label={`${index}-surname`}
{...familyMember.fieldProps("surname")}
/>
<p>
{familyMember.fieldProps("name").issues}
<br />
{familyMember.fieldProps("surname").issues}
</p>
</div>
))}
</div>
 
<button onClick={handleSubmit}>Sign up</button>
</div>
);
};
tsx
import { useFormo, subFormValue, success, validators } from "@buildo/formo";
 
type FamilyMember = {
name: string;
surname: string;
};
 
export const MyForm = () => {
const { handleSubmit, fieldProps, subForm, formErrors } = useFormo(
{
initialValues: {
name: "Jhon",
surname: "Doe",
familyMembers: subFormValue<Array<FamilyMember>>([]),
},
fieldValidators: () => ({}),
subFormValidators: () => ({
familyMembers: {
name: validators.minLength(1, "Family member name is required"),
surname: validators.minLength(1, "Family member surname is required"),
},
}),
},
{
onSubmit: () => Promise.resolve(success(null)),
}
);
 
const emptyFamilyMember = {
name: "",
surname: "",
};
 
return (
<div>
<TextField label="name" {...fieldProps("name")} />
<TextField label="surname" {...fieldProps("surname")} />
 
<div>
<button
onClick={() => subForm("familyMembers").push(emptyFamilyMember)}
>
Add family members
</button>
 
{subForm("familyMembers").items.map((familyMember, index) => (
<div key={`${familyMember.namePrefix}-container`}>
<TextField
label={`${index}-name`}
{...familyMember.fieldProps("name")}
/>
<TextField
label={`${index}-surname`}
{...familyMember.fieldProps("surname")}
/>
<p>
{familyMember.fieldProps("name").issues}
<br />
{familyMember.fieldProps("surname").issues}
</p>
</div>
))}
</div>
 
<button onClick={handleSubmit}>Sign up</button>
</div>
);
};

Let's break down the code above.

Define a sub form#

The subFormValue function is used to initialize a subform: this instructs formo to treat the field as a sub form, rather than a regular top-level field. If the initial state is an empty array you will need to provide a type hint, since TypeScript won't be able to infer the type for you, for example:

ts
subFormValue([] as Array<FamilyMember>);
(alias) subFormValue<FamilyMember[]>(arrayFieldValue: FamilyMember[]): SubFormValue<FamilyMember[]> import subFormValue
ts
subFormValue([] as Array<FamilyMember>);
(alias) subFormValue<FamilyMember[]>(arrayFieldValue: FamilyMember[]): SubFormValue<FamilyMember[]> import subFormValue

otherwise, you can explicitly define the type of the assigned constant

ts
const familyMembersInitialState: Array<FamilyMember> = [];
subFormValue(familyMembersInitialState);
(alias) subFormValue<FamilyMember[]>(arrayFieldValue: FamilyMember[]): SubFormValue<FamilyMember[]> import subFormValue
ts
const familyMembersInitialState: Array<FamilyMember> = [];
subFormValue(familyMembersInitialState);
(alias) subFormValue<FamilyMember[]>(arrayFieldValue: FamilyMember[]): SubFormValue<FamilyMember[]> import subFormValue

subFormValidators work the same as fieldValidators, except they are applied to each of the members of the "sub form" fields.

ts
const { handleSubmit, fieldProps, subForm, formErrors } = useFormo(
{
initialValues: {
name: "Jhon",
surname: "Doe",
familyMembers: subFormValue<Array<FamilyMember>>([]),
},
fieldValidators: () => ({}),
subFormValidators: () => ({
familyMembers: {
name: validators.fromPredicate(
(i) => typeof i === "string" && i.length > 0,
"Family member name is required"
),
surname: validators.fromPredicate(
(i) => typeof i === "string" && i.length > 0,
"Family member surname is required"
),
},
}),
},
{
onSubmit: () => Promise.resolve(success(null)),
}
);
 
ts
const { handleSubmit, fieldProps, subForm, formErrors } = useFormo(
{
initialValues: {
name: "Jhon",
surname: "Doe",
familyMembers: subFormValue<Array<FamilyMember>>([]),
},
fieldValidators: () => ({}),
subFormValidators: () => ({
familyMembers: {
name: validators.fromPredicate(
(i) => typeof i === "string" && i.length > 0,
"Family member name is required"
),
surname: validators.fromPredicate(
(i) => typeof i === "string" && i.length > 0,
"Family member surname is required"
),
},
}),
},
{
onSubmit: () => Promise.resolve(success(null)),
}
);
 

Accessing sub forms#

Note how subForm and fieldProps statically enforce the correct field names: for example, you can't accidentally call subForm("surname")

ts
subForm("name");
Argument of type '"name"' is not assignable to parameter of type '"familyMembers"'.2345Argument of type '"name"' is not assignable to parameter of type '"familyMembers"'.
subForm("surname");
Argument of type '"surname"' is not assignable to parameter of type '"familyMembers"'.2345Argument of type '"surname"' is not assignable to parameter of type '"familyMembers"'.
 
subForm("familyMembers");
ts
subForm("name");
Argument of type '"name"' is not assignable to parameter of type '"familyMembers"'.2345Argument of type '"name"' is not assignable to parameter of type '"familyMembers"'.
subForm("surname");
Argument of type '"surname"' is not assignable to parameter of type '"familyMembers"'.2345Argument of type '"surname"' is not assignable to parameter of type '"familyMembers"'.
 
subForm("familyMembers");

You can add new sub forms to a field using push or insertAt:

tsx
subForm("familyMembers").insertAt;
(property) insertAt: <SK extends "name" | "surname", V extends ArrayRecord<{ name: string; surname: string; familyMembers: SubFormValue<FamilyMember[]>; }, SK>>(index: number, value: V["familyMembers"][number]) => void
subForm("familyMembers").push;
(property) push: <SK extends "name" | "surname", V extends ArrayRecord<{ name: string; surname: string; familyMembers: SubFormValue<FamilyMember[]>; }, SK>>(value: V["familyMembers"][number]) => void
tsx
subForm("familyMembers").insertAt;
(property) insertAt: <SK extends "name" | "surname", V extends ArrayRecord<{ name: string; surname: string; familyMembers: SubFormValue<FamilyMember[]>; }, SK>>(index: number, value: V["familyMembers"][number]) => void
subForm("familyMembers").push;
(property) push: <SK extends "name" | "surname", V extends ArrayRecord<{ name: string; surname: string; familyMembers: SubFormValue<FamilyMember[]>; }, SK>>(value: V["familyMembers"][number]) => void

For example, to add a new family member with an initial state, you can use the push method inside a button onClick

tsx
const emptyFamilyMember = {
name: "",
surname: "",
};
 
<button onClick={() => subForm("familyMembers").push(emptyFamilyMember)}>
Add family member
</button>;
tsx
const emptyFamilyMember = {
name: "",
surname: "",
};
 
<button onClick={() => subForm("familyMembers").push(emptyFamilyMember)}>
Add family member
</button>;

To access all the elements of a sub form field, use the items property

ts
subForm("familyMembers").items;
(property) items: { fieldProps: <SK extends "name" | "surname", V extends ArrayRecord<{ name: string; surname: string; familyMembers: SubFormValue<FamilyMember[]>; }, SK>>(name: SK) => ComputedFieldProps<...>; onChangeValues: <SK extends "name" | "surname", V extends ArrayRecord<...>>(v: V["familyMembers"][number]) => unknown; remove: () => void; namePrefix: string; }[]
ts
subForm("familyMembers").items;
(property) items: { fieldProps: <SK extends "name" | "surname", V extends ArrayRecord<{ name: string; surname: string; familyMembers: SubFormValue<FamilyMember[]>; }, SK>>(name: SK) => ComputedFieldProps<...>; onChangeValues: <SK extends "name" | "surname", V extends ArrayRecord<...>>(v: V["familyMembers"][number]) => unknown; remove: () => void; namePrefix: string; }[]

Each of the items provides a fieldProps function analogous to the top-level fieldProps, that can be used to render a form element:

tsx
subForm("familyMembers").items.map((familyMember, index) => (
<div>
<TextField
key={`${familyMember.namePrefix}-name`}
label={`${index}-name`}
{...familyMember.fieldProps("name")}
/>
<TextField
key={`${familyMember.namePrefix}-surname`}
label={`${index}-surname`}
{...familyMember.fieldProps("surname")}
/>
<p>
{familyMember.fieldProps("name").issues}
<br></br>
{familyMember.fieldProps("surname").issues}
</p>
</div>
));
tsx
subForm("familyMembers").items.map((familyMember, index) => (
<div>
<TextField
key={`${familyMember.namePrefix}-name`}
label={`${index}-name`}
{...familyMember.fieldProps("name")}
/>
<TextField
key={`${familyMember.namePrefix}-surname`}
label={`${index}-surname`}
{...familyMember.fieldProps("surname")}
/>
<p>
{familyMember.fieldProps("name").issues}
<br></br>
{familyMember.fieldProps("surname").issues}
</p>
</div>
));