Skip to main content

Introduction to Forms

This tutorial will introduce you to the basics of creating forms with React and Kyber's form components. Each demo builds on the previous one so it is recommended you go through them linearly.

In these examples <FormDataDisplay /> is a component internal to this documentation that is used to visually show the submitted form data.

Prerequisites

This tutorial assumes you are already comfortable with the following concepts in React:

Forms in React

Before diving into how to use Kyber's form components it is important to first cover the basics of how forms work in React.

How to get an input's value

When working with forms in React the first challenge is figuring out how to access the data a user entered into an input.

The easiest way to do this is to add an onChange prop to the input. Then in the handleChange method we can get the input's value from the onChange event's target property (which points to the HTML input element). You can also use a React ref to directly access the input element, but that approach is used less often.

const FormExample = () => {
const handleChange = (event) => {
// Get the value from the event target
console.log(event.target.value);
};

return (
<>
<p>Type some text in the input to see it logged in the console</p>

{/* Add event handler to input */}
<input type="text" onChange={handleChange} aria-label="Email" />
</>
);
};

<FormExample />;

Controlling the value of an input

Currently our input is "uncontrolled" because React is not controlling, or managing, the state of the input. By default the state is stored in the DOM. While this method works, it's a one way street. You can only get the input's value, you can't set it. For example you would not be able to clear out the input's value or modify it based on another input.

To control an input's value we create a loop where we get the value of the input, store the value in React's state, then feed the value back into the input's value prop.

With this approach we can also modify the value in React's state and the input's value will be automatically updated to match!

const FormExample = () => {
const [value, setValue] = React.useState('');

const handleChange = (event) => {
// Set the input's value in state
setValue(event.target.value);

console.log(event.target.value);
};

return (
<>
<p>Type some text in the input to see it logged in the console</p>

{/* Feed the value back to the input through the `value` prop */}
<input type="text" onChange={handleChange} value={value} aria-label="Email" />
</>
);
};

<FormExample />;

Adding a label

An empty input doesn't really tell the user what content they should enter so lets add a label.

Note: For proper accessibility we need to add an id prop to our input with a matching htmlFor prop on the label so the two elements are associated with each other.

const FormExample = () => {
const [value, setValue] = React.useState('');

const handleChange = (event) => {
setValue(event.target.value);

console.log(event.target.value);
};

return (
<>
<p>Type some text in the input to see it logged in the console</p>

<label htmlFor="email-input-1">Email</label>
<br />
<input id="email-input-1" type="text" onChange={handleChange} value={value} />
</>
);
};

<FormExample />;

Submitting a form

Now that we have an input we need a way for the user to submit the value. Let's wrap the input in a form tag and add a submit button inside.

We can now add an onSubmit prop to the form so when the user hits the Enter key or clicks the Submit button we can simply pass along the form data that is already stored in state.

Note: By default when you submit a form it refreshes the page. To prevent that we need to call event.preventDefault() in our form's onSubmit handler.

const FormExample = (props) => {
const [value, setValue] = React.useState('');

const handleChange = (event) => {
setValue(event.target.value);
};

const handleSubmit = (event) => {
// Prevent the form's default submit action which refreshes the page
event.preventDefault();

// Do what you want with the form data.
// For demo purposes we are calling onSubmit from props to
// display the form data in the FormDataDisplay component.
props.onSubmit(value);
};

return (
<>
<p>Type some text and hit the submit button</p>

<form onSubmit={handleSubmit}>
<label htmlFor="email-input-2">Email</label>
<br />
<input id="email-input-2" type="text" onChange={handleChange} value={value} />
<br />
<br />
<button>Submit</button>
</form>
</>
);
};

// The FormDataDisplay component is used to display the submitted form value for demo purposes
// It is not required for a real world example
<FormDataDisplay>
<FormExample />
</FormDataDisplay>;

Controlling multiple inputs

A form with a single input doesn't collect much data, so let's add a password field.

With two fields we now need to track two values in state. We could add another useState and handleChange, but that won't scale well if the form has a lot of inputs. A better solution is to store an object in state where the object keys represent the name of each input.

Note: We have to add the name prop to each input for this to work.

const FormExample = (props) => {
// Now storing an object in state with keys that match the input names
const [values, setValues] = React.useState({
email: '',
password: '',
});

const handleChange = (event) => {
// Spread the previous values so we don't lose the other input's value
// Then update the value where the key is the name of the input
setValues({ ...values, [event.target.name]: event.target.value });
};

const handleSubmit = (event) => {
event.preventDefault();

props.onSubmit(values);
};

return (
<>
<p>Type some text and hit the submit button</p>

<form onSubmit={handleSubmit}>
<label htmlFor="email-input-3">Email</label>
<br />
<input
id="email-input-3"
name="email" // name prop is now required
type="text"
onChange={handleChange}
value={values.email}
/>
<br />
<br />
<label htmlFor="password-input-3">Password</label>
<br />
<input
id="password-input-3"
name="password" // name prop is now required
type="password"
onChange={handleChange}
value={values.password}
/>
<br />
<br />
<button>Submit</button>
</form>
</>
);
};

<FormDataDisplay>
<FormExample />
</FormDataDisplay>;

Forms in Kyber

Now that we have a basic form built in React we can start using Kyber components instead of the default HTML elements.

Use a Kyber <TextField> instead of an <input>

First we need to import a TextField component from the @emoney/kyber-components package and use it instead of the HTML input.

The TextField's onChange event also provides a second parameter that includes that value of the input so we don't have to pull it off the event.

Note: By default the TextField's type is text but we can set the prop type="password" to override it.

// Import the Kyber TextField component
import { TextField } from '@emoney/kyber-components';

const FormExample = (props) => {
const [values, setValues] = React.useState({
email: '',
password: '',
});

// New second `payload` parameter with field's value
const handleChange = (event, payload) => {
setValues({ ...values, [event.target.name]: payload.value });
};

const handleSubmit = (event) => {
event.preventDefault();

props.onSubmit(values);
};

return (
<>
<p>Type some text and hit the submit button</p>

<form onSubmit={handleSubmit}>
<label htmlFor="email-input-4">Email</label>
{/* TextField replaces input */}
<TextField id="email-input-4" name="email" onChange={handleChange} value={values.email} />
<br />
<label htmlFor="password-input-4">Password</label>
{/* TextField replaces input */}
<TextField
id="password-input-4"
name="password"
type="password"
onChange={handleChange}
value={values.password}
/>
<br />
<button>Submit</button>
</form>
</>
);
};

<FormDataDisplay>
<FormExample />
</FormDataDisplay>;

Use a Kyber <FormField>

Making sure the label's htmlFor value matches the input's id can be easy to forget so Kyber provides a FormField component that wraps an input and does this for you. It also provides other features like a prop for displaying error messages that we will use later.

You can remove the labels and wrap each TextField in a FormField component. Make sure to add the label prop to the FormField. Optionally, you can also remove the id from the TextField as the FormField component will add one for you.

import { FormField } from '@emoney/kyber-forms';
import { TextField } from '@emoney/kyber-components';

const FormExample = (props) => {
const [values, setValues] = React.useState({
email: '',
password: '',
});

const handleChange = (event, payload) => {
setValues({ ...values, [event.target.name]: payload.value });
};

const handleSubmit = (event) => {
event.preventDefault();

props.onSubmit(values);
};

return (
<>
<p>Type some text and hit the submit button</p>

<form onSubmit={handleSubmit}>
{/* FormField replaces label */}
<FormField id="email-field-1" label="Email">
<TextField name="email" onChange={handleChange} value={values.email} />
</FormField>
{/* FormField replaces label */}
<FormField id="password-field-1" label="Password">
<TextField
name="password"
type="password"
onChange={handleChange}
value={values.password}
/>
</FormField>
<button>Submit</button>
</form>
</>
);
};

<FormDataDisplay>
<FormExample />
</FormDataDisplay>;

Validation with Yup

While our form works, user can submit it without filling in any of the fields. We could write our own validation logic or use a third party library to help handle this. We recommend using the popular Yup validation library. It handles everything from simple to complex custom validation schemas.

It takes a lot to hook up the validation, but don't worry because we will show you an easier way of doing this in the next step. The overall gist is when a user focuses on an input we save it in state as "touched" or interacted with. Every time an input's value changes or the user blurs an input we run the validateForm method to check the input's value against the validation schema and save any errors into state. When the user submits the form we validate all the inputs and only submit the form if there are no errors in state. See comments in the demo for more details.

import { object, shape, string } from 'yup';
import { FormField } from '@emoney/kyber-forms';
import { TextField } from '@emoney/kyber-components';

// Create a validation schema using Yup
const validationSchema = object().shape({
email: string().required(),
password: string().required(),
});

const FormExample = (props) => {
const [values, setValues] = React.useState({
email: '',
password: '',
});
const [errors, setErrors] = React.useState({});
const [touched, setTouched] = React.useState({});
const isMounted = React.useRef(false);

// Every time the values changes validate inputs that have been touched
React.useEffect(() => {
if (isMounted.current) {
validateForm();
} else {
isMounted.current = true;
}
}, [values]);

const handleChange = (event, payload) => {
setValues({ ...values, [event.target.name]: payload.value });
};

// New handle focus method to track which fields have been touched
const handleFocus = (event) => {
setTouched({ ...touched, [event.target.name]: true });
};

// New handle blur method to check validation when leaving an input
const handleBlur = (event) => {
validateForm();
};

const handleSubmit = (event) => {
event.preventDefault();

// Set all fields as touched so we show all errors
const newTouched = {};

Object.keys(values).forEach((fieldName) => {
newTouched[fieldName] = true;
});

setTouched(newTouched);

// Validate the form and submit it if valid
validateForm(true);
};

const validateForm = (submitIfValid) => {
const newErrors = {};

try {
// Pass the field state object to the validation schema's validateSync method
// Setting abortEarly to false gives us errors for all the inputs, not just the first one.
validationSchema.validateSync(values, { abortEarly: false });
} catch (err) {
// Turn the error into an object where the key is the field name and the value is the error message
// E.g. { email: 'email is required' }
err.inner.forEach((item) => {
newErrors[item.path] = item.message;
});
}

setErrors(newErrors);

// Submit the form if it is valid
if (submitIfValid && Object.keys(newErrors).length === 0) {
props.onSubmit(values);
}
};

return (
<>
<p>Hit the submit button to see the errors. Then type some text and submit again.</p>

<form onSubmit={handleSubmit}>
{/* Pass the error to the errorText prop */}
<FormField id="email-field-2" label="Email" errorText={touched.email && errors.email}>
<TextField
name="email"
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
value={values.email}
/>
</FormField>
{/* Pass the error to the errorText prop */}
<FormField
id="password-field-2"
label="Password"
errorText={touched.password && errors.password}
>
<TextField
name="password"
type="password"
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
value={values.password}
/>
</FormField>
<button>Submit</button>
</form>
</>
);
};

<FormDataDisplay>
<FormExample />
</FormDataDisplay>;

Third party state management

In the previous example we had to manage state for the field values, touched field, and errors. That's a lot to do for every form. Fortunately there are third party libraries that can handle all of this for us. We recommend using the Formik library due to it's popularity and how well it works with Yup validation.

We can remove all of our manual state management and instead use the useFormik hook which handles all that logic and returns the state and handler methods for us to use.

import { useFormik } from 'formik';
import { object, shape, string } from 'yup';
import { FormField } from '@emoney/kyber-forms';
import { TextField } from '@emoney/kyber-components';

const validationSchema = object().shape({
email: string().required(),
password: string().required(),
});

const FormExample = (props) => {
// Pass in the initial values, onSubmit function, and validation schema to the `useFormik` hook
// We get back state and handler methods to add to our components
const { values, touched, errors, handleChange, handleBlur, handleSubmit } = useFormik({
initialValues: {
email: '',
password: '',
},
onSubmit: (values) => {
props.onSubmit(values);
},
validationSchema,
});

return (
<>
<p>Hit the submit button to see the errors. Then type some text and submit again.</p>

<form onSubmit={handleSubmit}>
{/* `touched` and `errors` state provided by Formik */}
<FormField id="email-field-3" label="Email" errorText={touched.email && errors.email}>
{/* Handler methods and values provided by Formik */}
<TextField
name="email"
onChange={handleChange}
onBlur={handleBlur}
value={values.email}
/>
</FormField>
{/* `touched` and `errors` state provided by Formik */}
<FormField
id="password-field-3"
label="Password"
errorText={touched.password && errors.password}
>
{/* Handler methods and values provided by Formik */}
<TextField
name="password"
type="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
/>
</FormField>
<button>Submit</button>
</form>
</>
);
};

<FormDataDisplay>
<FormExample />
</FormDataDisplay>;

Using a Kyber <Form>

The Kyber <Form> component configures Formik and connects the state and handler methods for you, so all you need to do is provide the Yup validation schema. The component also disables the fields when submitting and shows a spinner on the submit button. You can reset the isSubmitting state with the actions parameter provided to the handleSubmit method.

The Form component assumes you always need a button to submit the form so it adds one automatically. If you want to change the text of the submit button you can use the Forms's submitButtonContents prop. Refer to the Form component page for a complete list of props.

For this example we need to replace the HTML <form> element with the Kyber <Form> component and we can remove all the previous Formik config and just use the props on the <Form> component.

import { object, shape, string } from 'yup';
import { Form, FormField } from '@emoney/kyber-forms';
import { TextField } from '@emoney/kyber-components';

const validationSchema = object().shape({
email: string().required(),
password: string().required(),
});

const FormExample = (props) => {
const handleSubmit = (values, actions) => {
// The actions argument gives you access to Formik actions which can be used to reset the submitting state
setTimeout(() => {
actions.setSubmitting(false);
}, 2000);

props.onSubmit(values);
};

return (
<>
<p>Hit the submit button to see the errors. Then type some text and submit again.</p>

<Form
id="demo-form"
validationSchema={validationSchema}
onSubmit={handleSubmit}
submitButtonContents="Submit"
>
<FormField label="Email">
<TextField id="email-field-4" name="email" />
</FormField>
<FormField label="Password">
<TextField id="password-field-4" name="password" type="password" />
</FormField>
</Form>
</>
);
};

<FormDataDisplay>
<FormExample />
</FormDataDisplay>;

Using a Kyber <DataForm>

If you are just using standard Kyber form fields you can use the DataForm component to build the form for you. Instead of importing and using each form component (E.g. TextField, NumberField, etc...) you can describe each field as an object that is passed into the DataForm's fields prop. The DataForm will then handle rendering and configuring each field for you.

For each field object you need to provide a Kyber form component name for the as key to tell the DataForm which component you would like to use. For example { as: 'TextField' } will render the TextField component. You also need to provide values for the name and fieldLabel keys. The name will be the key used in the returned form data. The fieldLabel will be the label displayed above the field. All other key/value pairs are treated as props that will be spread onto the component. See each component's documentation for which props are available.

import { object, string } from 'yup';
import { DataForm } from '@emoney/kyber-forms';

const validationSchema = object().shape({
email: string().required(),
password: string().required(),
});

// Array of objects defining the components to render and their props
const fields = [
{ as: 'TextField', name: 'email', fieldLabel: 'Email' },
{ as: 'TextField', name: 'password', fieldLabel: 'Password', type: 'password' },
];

const FormExample = (props) => {
const handleSubmit = (values, actions) => {
setTimeout(() => {
actions.setSubmitting(false);
}, 2000);

props.onSubmit(values);
};

return (
<>
<p>Hit the submit button to see the errors. Then type some text and submit again.</p>

<DataForm
id="data-form-demo"
fields={fields}
validationSchema={validationSchema}
onSubmit={handleSubmit}
submitButtonContents="Submit"
/>
</>
);
};

<FormDataDisplay>
<FormExample />
</FormDataDisplay>;

Next Steps

Now that you have the basics under control you can check out the Form and DataForm documentation for more examples or you can explore the different kinds of form inputs like Checkboxes, Sliders, DatePickers, and more!