Skip to main content

Form

A conditionally stateful Form wrapper. This component maintains state and validation of any child FormField components. It utilizes Formik and can receive props related to configuring Formik.

Validation Sync and Async Form and Field level validation is available through the validate prop.

import { Alert } from '@emoney/kyber';

<Alert>
Check out the <a href="#/Tutorials/Intro%20To%20Forms">Intro To Forms Tutorial</a> to get familiar
with how forms work in React with Kyber.
</Alert>;

Basic Example

Wrap the Kyber form field component you would like to use in a FormField and place it inside the Form component. You will need to provide an id and label prop to the FormField. Set the name prop on the field component, this will be used as the key in the returned data.

Result
Loading...
Live Editor
<Form
	onSubmit={(values, actions) => {
		actions.setSubmitting(false);
		console.log(values);
	}}
>
	<FormField>
		<TextField label="Label" name="textfield" />
	</FormField>
</Form>

Validation

The Form and related FormField components support multiple types of validation.

The preferred order of validation is:

  1. Form level schema validation with yup.
  2. Form level synchronous validation.
  3. Form level asynchronous validation.
  4. Field level synchronous validation (See FormField).
  5. Field level synchronous validation (See FormField).

Validation Library

Yup is a powerful validation library that can be used with the Form component. There are a lot of options and ways that Yup validation schemas can be constructed. Review the Yup Documentation for full details.

Form Level Schema Validation

Result
Loading...
Live Editor
const schema = object().shape({
	textfield: string().required(),
});

render(
	<Form
		onSubmit={(values, actions) => {
			actions.setSubmitting(false);
			console.log(values);
		}}
		validationSchema={schema}
	>
		<FormField>
			<TextField label="Label" name="textfield" />
		</FormField>
	</Form>,
);

Sync Form Level Validation

Result
Loading...
Live Editor
<Form
	id="sync-form-level"
	onSubmit={(values, actions) => {
		actions.setSubmitting(false);
		console.log(values);
	}}
	validate={(values, actions) => {
		if (values.textfield === 'a') {
			return {
				textfield: 'Cannot be a',
			};
		}
	}}
>
	<FormField id="sync-form-level-textfield" label="Label" description="Try setting to 'a'">
		<TextField name="textfield" />
	</FormField>
</Form>

Async Form Level Validation

Result
Loading...
Live Editor
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

render(
	<Form
		onSubmit={(values, actions) => {
			actions.setSubmitting(false);
			console.log(values);
		}}
		validate={(values, actions) =>
			sleep(2000).then(() => {
				if (values.textfield === 'a') {
					return {
						textfield: 'Cannot be a',
					};
				}
			})
		}
	>
		<FormField>
			<TextField name="textfield" label="Label" description="Try setting to 'a'" />
		</FormField>
	</Form>,
);

Other Validation Uses

Toggling Field Validation

Sometimes it may be necessary to alter form behavior based on local or external state. A common case is that validation changes based on state changes.

You can do this by using Yup's when method to conditionaly add validation based on the value of another field.

Result
Loading...
Live Editor
function Example() {
	const [isEnabled, setIsEnabled] = React.useState(true);

	const initialValue = {
		switch: isEnabled && 'on',
	};

	const schema = object().shape({
		disabled: string().when('switch', { is: 'on', then: string().required() }),
		conditional: string().when('switch', { is: 'on', then: string().required() }),
	});

	return (
		<Form
			initialValues={initialValue}
			validationSchema={schema}
			onSubmit={(values, actions) => {
				console.log(values);
				actions.setSubmitting(false);
			}}
		>
			<FormField>
				<Switch
					label={`Field ${isEnabled ? 'Enabled' : 'Disabled'}`}
					name="switch"
					onChange={(payload) => setIsEnabled(payload.checked)}
				/>
			</FormField>

			<FormField>
				<TextField label="Disabled Field" name="disabled" disabled={!isEnabled} />
			</FormField>

			{isEnabled && (
				<FormField>
					<TextField label="Conditionally Rendered Field" name="conditional" />
				</FormField>
			)}
		</Form>
	);
}

Cross Field Validation

Result
Loading...
Live Editor
function Example() {
	const schema = object().shape({
		age: number().required(),
		retirement_age: number().required().min(ref('age'), '${path} can not be less than Age'),
	});

	return (
		<Form
			validationSchema={schema}
			onSubmit={(values, actions) => {
				console.log(values);
				actions.setSubmitting(false);
			}}
		>
			<FormField>
				<NumberField label="Age" name="age" />
			</FormField>

			<FormField>
				<NumberField label="Retirement Age" description="Set less than Age" name="retirement_age" />
			</FormField>
		</Form>
	);
}

You can also replace the Form's footer by using the footerRenderer prop on the Form. Alternatively you can set this prop to null to disable the footer entirely.

Note: If you add type="submit" to one of your buttons it will be used as the submit button.

Result
Loading...
Live Editor
// The function receives the Form components props as the first parameter in case you need them
const renderCustomFooter = (props) => {
	return (
		<Button id="demo-button" type="submit">
			My Custom Button
		</Button>
	);
};

function Example() {
	const handleSubmit = (values, actions) => {
		actions.setSubmitting(false);
		console.log(values);
	};

	return (
		<Form onSubmit={handleSubmit} footerRenderer={renderCustomFooter}>
			<FormField>
				<TextField label="Label" name="textfield" />
			</FormField>
		</Form>
	);
}

render(<Example />);

Further Customization

With children As A Render Prop

See Formik for more details about the props that are forwarded to children.

Result
Loading...
Live Editor
<Form
	onSubmit={(values, actions) => {
		actions.setSubmitting(false);
		console.log({ values });
	}}
>
	{({ submitCount, dirty }) => (
		<>
			<p>Is dirty? {dirty.toString()}</p>

			<p>Number of times submitted: {submitCount}</p>

			<FormField>
				<TextField label="Label" name="textfield" />
			</FormField>
		</>
	)}
</Form>

Using Formik Context

It's possible to directly use the Formik context within a subcomponent of a Form. Either as the Formik published hook or through direct access with of the FormikContext object and useContext hook.

Result
Loading...
Live Editor
function SubComponent() {
	const { dirty, submitCount } = useFormikContext();

	return (
		<>
			<p>Is dirty? {dirty.toString()}</p>

			<p>Number of times submitted: {submitCount}</p>
		</>
	);
}

render(
	<Form
		onSubmit={(values, actions) => {
			actions.setSubmitting(false);
			console.log({ values });
		}}
	>
		<SubComponent />

		<FormField>
			<TextField label="Label" name="textfield" />
		</FormField>
	</Form>,
);

Using the Form Ref

The Form's ref prop will return a reference to the Formik instance. This gives you access to Formik's props and methods which let you control the form's state manually. For example this can be used to reset the form and clear out the field values.

Resetting the Form

Result
Loading...
Live Editor
function Example() {
	const formRef = useRef();

	const handleSubmit = (values, actions) => {
		actions.setSubmitting(false);
		console.log(values);
	};

	const handleSecondaryButtonClick = (event) => {
		console.log(formRef.current);
		console.log('secondary button clicked');
		formRef.current.resetForm();
	};

	return (
		<Form
			onSubmit={handleSubmit}
			onSecondaryButtonClick={handleSecondaryButtonClick}
			secondaryButtonContents="Reset form"
			submitButtonContents="Submit button text"
			ref={formRef}
		>
			<FormField>
				<TextField label="Label" name="textfield" />
			</FormField>
		</Form>
	);
}

Nested Values

Result
Loading...
Live Editor
const options = [
	{
		name: 'Georgia',
		value: 'GA',
	},
	{
		name: 'Iowa',
		value: 'IA',
	},
];

render(
	<Form
		onSubmit={(values, actions) => {
			actions.setSubmitting(false);
			console.log(values);
		}}
		initialValues={{
			test: { checkbox: false, group: ['a', 'b', 'd'], select: 'GA', multiple: ['GA', 'IA'] },
		}}
		initialErrors={{ test: { stuff: 'Has an initial error' } }}
		initialTouched={{ test: { stuff: true } }}
	>
		<FormField>
			<TextField label="Label" name="test.stuff" />
		</FormField>

		<FormField>
			<Checkbox label="Checkbox" name="test.checkbox" />
		</FormField>

		<FormField>
			<CheckboxGroup label="Group" name="test.group">
				<CheckboxGroup.Item value="a" label="A" />
				<CheckboxGroup.Item value="b" label="B" />
				<CheckboxGroup.Item value="c" label="C" />
				<CheckboxGroup.Item value="d" label="D" />
			</CheckboxGroup>
		</FormField>

		<FormField>
			<Select label="Select" name="test.select">
				{options.map((option) => (
					<Item id={`nested-select-${option.value}`} key={option.value} {...option}>
						{option.name}
					</Item>
				))}
			</Select>
		</FormField>

		<FormField>
			<MultiSelect label="MultiSelect" name="test.multiple">
				{options.map((option) => (
					<Item id={`nested-multiple-${option.value}`} key={option.value} {...option}>
						{option.name}
					</Item>
				))}
			</MultiSelect>
		</FormField>
	</Form>,
);

Analytics

The Form component is trackable through Kyber Analytics. This is the default analytics config.

export default {
value: 'Form',
actions: {
onSubmit: { type: 'FORM_SUBMIT', payload: 'Submit' },
},
};

Props