Formulare
De multe ori o să aveți nevoie de a crea formulare pentru a adăuga/modifica date pe backend. In HTML aveți multe elemente care funcționeaza ca input-uri pentru formulare, de exemplu select, textbox, checkbox etc. Pentru React bibliotecile de UI cum sunt Material UI expun aceste input-uri deja stilizate si cu atribute care va pot ajuta pentru a controla componentele respective.
Dificultate în definirea formularelor constă atât în controlul componentelor din formular cât și validarea formularului. Ne dorim ca formularul să valideze datele până să fie trimise către backend căt și să se schimbe la diferite acțiuni ale utilizatorului cum ar fi pentru afișarea erorilor sau pentru ascunderea unur câmpuri. Pentru controlul componentelor vom folosi react-hook-form iar pentru validarea formularului Yup:
npm install react-hook-form yup @hookform/resolvers
Folosind useForm putem obtine starea și acțiuni de modificare pe starea unui formular dându-se o anumită structură pentru formular. Cu yup putem definii o schema de validare pentru starea formularului care printr-un obiect resolver va împiedica formularul să fie submis dacă nu se respectă schema de validare și va returna mesaje de eroare. În exemplul de mai jos aveși definirea unui formular simplu unde se definește schema de validare și campurile formularului într-o funcție controller hook.
/**
* 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.
}
}
}
Pentru componenta de UI trebuie folosit formularul în interiorul unui element de tip form unde se specifică ca atribut funcția de submitere. În interiorul formularului se leagă variabilele la input-urile de form și ca să se submită trebuie sa existe un buton de tip submit care va apela funcția specificata cu datele din formular așa cum aveți ca exemplu mai jos.
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>
};