Complex forms
The form library enables some fairly complex behaviour without a lot of effort from the implementer, however some of the functionality can be a little hard to figure out how to actually implement correctly.
Dynamic fields
A fairly common pattern to see within forms is fields being created dynamically based on the values of other fields. There are some caveats to this pattern though.
While values will not appear in the form state until the field is rendered for the first time (unless they are in the initialValues sent to the form), un-mounting the field won't make it's values disappear. This means that in order to get the desired outcome, some manual handling of form values might be required.
Another thing to be mindful of is that sometimes react will try to be smart and re-use existing elements if it thinks they are the same. If there is any weirdness around the elements not swapping out like they should, give each field a key prop as that can force react to see these elements as different things.
import React from 'react';
import { ReactForms, Form, Field } from '@rexlabs/form';
import { RadioGroupInput } from '@rexlabs/radio-input';
export const DynamicFields = () => {
return (
<ReactForms>
{({ values, setFieldValue }) => {
return (
<Form>
<Field
name='allegiance'
label='Allegiance'
inputProps={{
options: [
{ value: 'jedi', label: 'Jedi' },
{ value: 'sith', label: 'Sith' }
]
}}
onChange={() => {
// Reset the value of the `home` field
// when this field is changed so that
// we don't get into a state where
// we have a jedi that lives on the death star
setFieldValue('home', null);
}}
Input={RadioGroupInput}
/>
{values.allegiance === 'jedi' && (
<Field
key='jedi_home'
name='home'
label='Home base'
inputProps={{
options: [
{ value: 'ahch-to', label: 'Ahch-To' },
{ value: 'coruscant', label: 'Coruscant' }
]
}}
Input={RadioGroupInput}
/>
)}
{values.allegiance === 'sith' && (
<Field
key='sith_home'
name='home'
label='Home base'
inputProps={{
options: [
{ value: 'ds_1', label: 'Death Star 1' },
{ value: 'ds_2', label: 'Death Star 2 (Secret)' }
]
}}
Input={RadioGroupInput}
/>
)}
</Form>
);
}}
</ReactForms>
);
};
Constrained Field Array
Sometimes the form will need to react to changes in the value of one of it's fields. A naive way to do this would be to add an onChange handler to the field that is changing, however the value can often change outside of the lifecycle of the input or you might want to react to changing any value in a nested field / field array, so this approach isn't very scalable. Often times the best way to do this is to utilise the useEffect hook to react to all changes in the value of the field, in this case to make sure that there is always at least one "friend" field visible.
Because the form uses the render prop pattern, we have to break the actual form that's being rendered out into it's own component to use hooks safely.
import React, { useEffect } from 'react';
import { ReactForms, Form, FieldArray, Field } from '@rexlabs/form';
import { TextInput } from '@rexlabs/text-input';
function ConstrainedForm({ values: { friends }, setValues }) {
// This effect runs whenever the friends value changes
// and forces the form to keep at least one friend field visible
useEffect(() => {
if (friends && friends.length <= 0) {
setValues((prevValues) => ({
...prevValues,
friends: ['']
}));
}
}, [friends, setValues]);
return (
<Form>
<FieldArray name='friends'>
{({ fields, push }) => {
return (
<div>
{fields.map(({ field, actions: { remove } }, index) => (
<div key={index}>
<Field {...field} label='Friend' Input={TextInput} />
<button
onClick={() => {
remove();
}}
>
Remove
</button>
</div>
))}
<button
onClick={() => {
push(null);
}}
>
Add Field
</button>
</div>
);
}}
</FieldArray>
</Form>
);
}
export const ConstrainFieldArray = () => {
return (
<ReactForms>
{({ values, setValues }) => {
return <ConstrainedForm values={values} setValues={setValues} />;
}}
</ReactForms>
);
};