Building Forms
Howto build forms for SCM-Manager
Below we would like to explain how to write React Hook Form forms in an easy and fast way, why it makes sense to switch and what needs to be considered.
Legacy Process
Previously, we passed our self-written form component into the Configuration component's render function. In the form we defined a prop for each entry, plus an onChange handler that takes the value and writes it to a state. Additionally, we added validation logic when a field changes.
Especially in old areas, which were still built with class components, you should be very careful.
A lot of boilerplate code was needed, errors were frequent, and typings were generally flawed.
Standard Process
React Hook Form will bring the useForm
hook to validate your form with minimal re-render.
This contains a generic parameter which summarizes the possible input fields.
The useForm hook returns an object with several properties:
register
allows you to register an input or select element and apply validation rules to React Hook Form.formState
contains information about the form state. This can also specifyisValid
.handleSubmit
is called when you press the submit button and will receive the form data if form validation is successful.reset
reset either the entire form state or part of the form state.
import React, { FC, useEffect } from "react";
// import hook from react-hook-form library
import { useForm } from "react-hook-form";
const ReactHookForm: FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Name>();
const [stored, setStored] = useState<Person>();
const onSubmit = (person: Person) => {
setStored(person);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<InputField label="First Name" autofocus={true} {...register("firstName")} />
<InputField
label="Last Name"
{...register("lastName", { required: true })}
validationError={!!errors.lastName}
errorMessage={"Last name is required"}
/>
<Level className="pt-2" right={<SubmitButton>Submit</SubmitButton>} />
</form>
);
};
Building Configuration Forms
UseConfigLink
from @scm-manager/ui-api
gets links via prop from binder and loads initial config asynchronously,
also specifies as reading part whether readOnly (no update link was set) and as writing part an update method.
As well as formProps for isLoading, isUpdating etc for ConfigurationForm.
import React, { FC, useEffect } from "react";
import { useForm } from "react-hook-form";
const GlobalConfig: FC<Props> = ({ link }) => {
// formProps spread syntax returns prop for name, onBlur, onChange and ref and additionally attaches them to fields
const { initialConfiguration, update, isReadOnly, ...formProps } = useConfigLink<GlobalConfigurationDto>(link);
const { formState, handleSubmit, register, reset, control } = useForm<GlobalConfigurationDto>({
// mode onChange should be specified so that validation takes place immediately!
mode: "onChange",
});
// ...
};
ConfigurationForm only takes care of the display of the component. All the logic now lives in the hook.
Registering your own onChange
-handler is not necessary anymore.
onSubmit
handleSubmit
-function passes own submit function, which is called with filled form data type.
In the register
-function you can specify additional options for validation.
For example, required, min, max, pattern.
return (
<ConfigurationForm isValid={formState.isValid} isReadOnly={isReadOnly} onSubmit={handleSubmit(update)} {...formProps}>
<Title title={t("settings.title")} />
<Checkbox
label={t("fastForwardOnly.label")}
helpText={t("fastForwardOnly.helpText")}
disabled={isReadOnly}
{...register("fastForwardOnly", { shouldUnregister: true })}
/>
<InputField
label={t("branchesAndTagsPatterns.label")}
helpText={t("branchesAndTagsPatterns.helpText")}
disabled={isReadOnly}
{...register("branchesAndTagsPatterns")}
/>
<GpgVerificationControl control={control} isReadonly={isReadOnly} />
</ConfigurationForm>
);
Note when using formState
Be sure to use as proxy to get objects out (not formState.isValid!), because you won't notice the render cycle otherwise.
Set to initial values
In synchronous loading, a form can be set to an initial value using defaultValue
.
In the asynchronous case, values for each field can be set separately by using defaultValue={stored.fastForwardOnly}
or an entire form using reset
.
useEffect(() => {
if (initialConfiguration) {
reset(initialConfiguration);
}
}, [initialConfiguration]);
Note when Creating new Components
- If possible, pass all props.
- React Hook Form needs the following values for event to be recognized: name, onChange, onBlur, ref (reference to input element).
FormFieldTypes
is not a base, but helps for backwards compatibility with old function types. When writing a new component omit old onChange!- Since some components have other elements built around an input field, there is also the
forwardRef
. It creates a reference that can be passed to an inner element. - Nested forms are a bit more complex to build and might need a wrapper.
- Validation rules are all based on the HTML standard and also allow for custom validation methods.
- Fields marked as
disabled
in SCM-Manager won't be included on submission. If you want to prevent interaction but need to submit the value of a form element,readOnly
is the better choice.
Some implementations: