Forms
Many times you will need to create forms to add/modify data on the backend. In HTML you have many elements that work as inputs for forms, for example select, textbox, checkbox etc. For React, UI libraries such as Material UI expose these inputs already stylized and with attributes that can help you to control the respective components.
Difficulty in defining forms lies both in controlling the components in the form and validating the form. We want the form to validate the data until it is sent to the backend as well as change to different user actions such as displaying errors or hiding fields. For component control we will use react-hook-form and for form validation Yup:
npm install react-hook-form yup @hookform/resolvers
Using useForm we can get the state and change actions on the state of a form by giving a certain structure for the form. With yup we can define a validation scheme for the state of the form which through a resolver object will prevent the form from being submitted if the validation scheme is not followed and will return error messages. In the example below you have the definition of a simple form where the validation scheme and form fields are defined in a controller hook function.
/**
* Use a function to return the default values of the form and the validation schema.
* You can add other values as the default, for example when populating the form with data to update an entity in the backend.
*/
const getDefaultValues = (initialData?: { email: string }) => {
const defaultValues = {
email: "",
password: ""
};
if (!isUndefined(initialData)) {
return {
...defaultValues,
...initialData,
};
}
return defaultValues;
};
/**
* Create a hook to get the validation schema.
*/
const useInitLoginForm = () => {
const { formatMessage } = useIntl();
const defaultValues = getDefaultValues();
const schema = yup.object().shape({ // Use yup to build the validation schema of the form.
email: yup.string() // This field should be a string.
.required(formatMessage( // Use formatMessage to get the translated error message.
{ id: "globals.validations.requiredField" },
{
fieldName: formatMessage({ // Format the message with other translated strings.
id: "globals.email",
}),
})) // The field is required and needs a error message when it is empty.
.email() // This requires the field to have a email format.
.default(defaultValues.email), // Add a default value for the field.
password: yup.string()
.required(formatMessage(
{ id: "globals.validations.requiredField" },
{
fieldName: formatMessage({
id: "globals.password",
}),
}))
.default(defaultValues.password),
});
const resolver = yupResolver(schema); // Get the resolver.
return { defaultValues, resolver }; // Return the default values and the resolver.
}
/**
* Create a controller hook for the form and return any data that is necessary for the form.
*/
export const useLoginFormController = (): LoginFormController => {
const { formatMessage } = useIntl();
const { defaultValues, resolver } = useInitLoginForm();
const { redirectToHome } = useAppRouter();
const { loginMutation: { mutation, key: mutationKey } } = useLoginApi();
const { mutateAsync: login, status } = useMutation({
mutationKey: [mutationKey],
mutationFn: mutation
})
const queryClient = useQueryClient();
const dispatch = useAppDispatch();
const submit = useCallback((data: LoginFormModel) => // Create a submit callback to send the form data to the backend.
login(data).then((result) => {
dispatch(setToken(result.response?.token ?? ''));
toast(formatMessage({ id: "notifications.messages.authenticationSuccess" }));
redirectToHome();
}), [login, queryClient, redirectToHome, dispatch]);
const {
register,
handleSubmit,
formState: { errors }
} = useForm<LoginFormModel>({ // Use the useForm hook to get callbacks and variables to work with the form.
defaultValues, // Initialize the form with the default values.
resolver // Add the validation resolver.
});
return {
actions: { // Return any callbacks needed to interact with the form.
handleSubmit, // Add the form submit handle.
submit, // Add the submit handle that needs to be passed to the submit handle.
register // Add the variable register to bind the form fields in the UI with the form variables.
},
computed: {
defaultValues,
isSubmitting: status === "pending" // Return if the form is still submitting or nit.
},
state: {
errors // Return what errors have occurred when validating the form input.
}
}
}
For the UI component, the form must be used inside a form type element where the submission function is specified as an attribute. Inside the form, the variables are linked to the form inputs, and in order to submit, there must be a submit button that will call the function specified with the data from the form, as you have as an example below.
export const LoginForm = () => {
const { formatMessage } = useIntl();
const { state, actions, computed } = useLoginFormController(); // Use the controller.
return <form onSubmit={actions.handleSubmit(actions.submit)}> {/* Wrap your form into a form tag and use the handle submit callback to validate the form and call the data submission. */}
<Stack spacing={4} style={{ width: "100%" }}>
<ContentCard title={formatMessage({ id: "globals.login" })}>
<Grid container item direction="row" xs={12} columnSpacing={4}>
<Grid container item direction="column" xs={12} md={12}>
<FormControl
fullWidth
error={!isUndefined(state.errors.email)}
> {/* Wrap the input into a form control and use the errors to show the input invalid if needed. */}
<FormLabel required>
<FormattedMessage id="globals.email" />
</FormLabel> {/* Add a form label to indicate what the input means. */}
<OutlinedInput
{...actions.register("email")} // Bind the form variable to the UI input.
placeholder={formatMessage(
{ id: "globals.placeholders.textInput" },
{
fieldName: formatMessage({
id: "globals.email",
}),
})}
autoComplete="username"
/> {/* Add a input like a textbox shown here. */}
<FormHelperText
hidden={isUndefined(state.errors.email)}
>
{state.errors.email?.message}
</FormHelperText> {/* Add a helper text that is shown then the input has a invalid value. */}
</FormControl>
</Grid>
<Grid container item direction="column" xs={12} md={12}>
<FormControl
fullWidth
error={!isUndefined(state.errors.password)}
>
<FormLabel required>
<FormattedMessage id="globals.password" />
</FormLabel>
<OutlinedInput
type="password"
{...actions.register("password")}
placeholder={formatMessage(
{ id: "globals.placeholders.textInput" },
{
fieldName: formatMessage({
id: "globals.password",
}),
})}
autoComplete="current-password"
/>
<FormHelperText
hidden={isUndefined(state.errors.password)}
>
{state.errors.password?.message}
</FormHelperText>
</FormControl>
</Grid>
</Grid>
</ContentCard>
<Grid container item direction="row" xs={12} className="padding-top-sm">
<Grid container item direction="column" xs={12} md={7}></Grid>
<Grid container item direction="column" xs={5}>
<Button type="submit" disabled={!isEmpty(state.errors) || computed.isSubmitting}> {/* Add a button with type submit to call the submission callback if the button is a descended of the form element. */}
{!computed.isSubmitting && <FormattedMessage id="globals.submit" />}
{computed.isSubmitting && <CircularProgress />}
</Button>
</Grid>
</Grid>
</Stack>
</form>
};