Design layer

How to build dynamic forms in React on the fly

How to build dynamic forms in React on the fly

When creating a site or web application that uses forms to send data, front-end developers often face the problem of correctly and rationally designing the code that is responsible for creating the form. Since there is usually more than one form on the site, you should create a unique component that can be used and customized for your own needs. Since we are talking about the React library, it is not difficult to make a unique component.

Lets create a pure HTML form for an example

When you are faced with the task of creating a form during work, the first option that comes to mind is to write HTML markup for each element of the form every time:

<form>
    <div>
        <label htmlFor="email">Email:</label>
        <input type="email" name="email" />
        <div>Help text</div>

        <label htmlFor="name">name:</label>
        <input type="text" name="name" />
        <div>Help text</div>
    </div>
</form>

It is not difficult to understand that this approach is very bad, if only because there can be any number of elements in the form, and writing such HTML markup for each of them is long, inconvenient, and not correct.

Let's convert our pure HTML into a React code

After that, next step is to simplify our code, since some aspects of the code are repeated, such as the type of the input element, the attributes of the elements, and the structure of the form-group element in general. Replace the functions that will render our layout with javascript. And the data that differ in the elements of the form will be transmitted by the arguments of the function.

So this approach eliminates the problem of code repetition by keeping such code in one place, and where you need to insert variables.

The very function that generates the layout we need looks like this:

export function generateTextField(name, handler, helpText, value) {
    return (
        <div>
            <label>{name}</label>
            <input 
                type="text"
                value={value}
                name={name.toLowerCase().replace(" ", "")}
                onChange={handler}
            />
            <div>{helpText}</div>
        </div>
    )
}

As you can see from the code example below, the function that generates the HTML markup in the code, as arguments to the function pass the name for the label tag, the handler for the input tag, and the error and value for the element if necessary.

This approach greatly reduces the amount of written code, and speeds up work on the site as a whole, because instead of 15 conditional strips of code, we have only one.

{generateTextField(nameOfElement, handlerOfElement, helpText, valueOfElement)}

Creating a form component using React that generates form on the fly

Our component will return a Form element from the reactstrap library, which allows us to add minimal styling to our form.

FormData, which will be processed depending on what manipulations the user will do with the form elements, will be stored in the local state of our CustomForm component and will be sent after the user confirms sending the data.

The function responsible for processing the data is also stored in the component. Depending on the type of elements of our form, it has its handler, for example, options are considered below if our form has inputs such as checkbox, file, and plain text.

As we can see from the following code example, the data in the object format is written to the local state, where the key is the name of the field, and the value is the data entered by the user.

if (typeData === "create") {
    setFormDate({ ...formData, account: "" });
}
if (e.target.type === "checkbox") {
    setFormDate({ ...formData, [e.target.name]: e.target.checked });
} else if (e.target.type === "file") {
    const newFormData = new FormData();
    newFormData.append([e.target.name], e.target.files[0]);
    setFormDate(newFormData);
} else {
    setFormDate({ ...formData, [e.target.name]: e.target.value });
}

Since errors are returned from the backend side, we first record them in redux, and already after unsuccessful processing of the form, they will be substituted individually for each field.

 <FormFeedback>
     {formErrors[item.name]?.map((error, i) => (
         <span key={i}>{error}</span>
     ))}
 </FormFeedback>

A logical question arises, "How to transfer names and types of fields for our form?". Here, too, everything is simple. In a separate file with the extension .js, create constants individually for each form. For example, below is an example of how to create fields for a form, where we can specify the field name, type, and title for the label tag. It should be understood that this functionality can be extended according to one's own needs, by transferring the necessary properties, such as class or required field, to each object.

export const example = [
    {
        type: "email",
        name: "username",
        label: "Email",
    },
    {
        type: "password",
        name: "password",
        label: "Password",
    },
];

As for how to render these fields in our CustomForm component, everything is also quite simple. Receiving the fields through the props of our component, we output them through the .map() method specifying the required type, field name, and other required properties:

{items.map((item, i) => (
    <FormGroup key={i} >
        <Label for={`${item.label}_${i}`} >
        {item.label}
        </Label>
        <Input
            id={`${item.label}_${i}`}
            type={item.type}
            name={item.name}
            onChange={handleInput}
            value={formData ? formData[item?.name] : ""}
        />
        <FormFeedback>
            {formErrors[item.name]?.map((error, i) => (
                <span key={i}>{error}</span>
            ))}
        </FormFeedback>
    </FormGroup>
))}

Now our form component looks like this:

function CustomForm({
    items,
    formTitle,
    onSubmitForm,
}) {
    const { t } = useTranslation();
    const [formData, setFormDate] = useState({});
    const dispatch = useDispatch();
    const formErrors = useSelector((state) => state.formErrors.errors);

    const handleInput = (e) => {
        setFormDate({ ...formData, [e.target.name]: e.target.value });
    };

    const onSubmit = (e) => {
        e.preventDefault();
        onSubmitForm(formData);
        dispatch(setFormErrors(""));
    };

    return (
    <Form onSubmit={onSubmit} >
        <h3>{t(formTitle)}</h3>
        {items.map((item, i) => (
            <FormGroup key={i} >
                <Label for={`${item.label}_${i}`} >
                {item.label}
                </Label>
                <Input
                    id={`${item.label}_${i}`}
                    type={item.type}
                    name={item.name}
                    onChange={handleInput}
                    value={formData ? formData[item?.name] : ""}
                />
                <FormFeedback>
                    {formErrors[item.name]?.map((error, i) => (
                        <span key={i}>{error}</span>
                    ))}
                </FormFeedback>
            </FormGroup>
        ))}
    </Form>
    );
}

This form creation approach helps reduce the amount of repetitive code to a minimum, variably change each element of the form, and customize and customize the form for your own needs.

line
design layer

Looking for an enthusiastic team?