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";ÂtypeFamilyMember = {name : string;surname : string;};Âexport constMyForm = () => {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)),});ÂconstemptyFamilyMember = {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";ÂtypeFamilyMember = {name : string;surname : string;};Âexport constMyForm = () => {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)),});ÂconstemptyFamilyMember = {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 formThe 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 ([] asArray <FamilyMember >);
ts
subFormValue ([] asArray <FamilyMember >);
otherwise, you can explicitly define the type of the assigned constant
ts
constfamilyMembersInitialState :Array <FamilyMember > = [];subFormValue (familyMembersInitialState );
ts
constfamilyMembersInitialState :Array <FamilyMember > = [];subFormValue (familyMembersInitialState );
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 ) => typeofi === "string" &&i .length > 0,"Family member name is required"),surname :validators .fromPredicate ((i ) => typeofi === "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 ) => typeofi === "string" &&i .length > 0,"Family member name is required"),surname :validators .fromPredicate ((i ) => typeofi === "string" &&i .length > 0,"Family member surname is required"),},}),},{onSubmit : () =>Promise .resolve (success (null)),});Â
#
Accessing sub formsNote how subForm
and fieldProps
statically enforce the correct field names: for example, you can't accidentally call subForm("surname")
ts
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 ("name" );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 ("surname" );ÂsubForm ("familyMembers");
ts
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 ("name" );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 ("surname" );ÂsubForm ("familyMembers");
You can add new sub forms to a field using push
or insertAt
:
tsx
subForm ("familyMembers").insertAt ;subForm ("familyMembers").push ;
tsx
subForm ("familyMembers").insertAt ;subForm ("familyMembers").push ;
For example, to add a new family member with an initial state, you can use the push
method inside a button onClick
tsx
constemptyFamilyMember = {name : "",surname : "",};Â<button onClick ={() =>subForm ("familyMembers").push (emptyFamilyMember )}>Add family member</button >;
tsx
constemptyFamilyMember = {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 ;
ts
subForm ("familyMembers").items ;
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 >));