How to create generic forms and fields components to reduce repetitive code with React and Typescript? | PART II

Cómo-crear-componentes-genéricos-Forms-y-Fields-reducir-código-repetitivo-Parte-II-Itequia-EN-CAT

How to create generic forms and fields components to reduce repetitive code with React and Typescript? | PART II

As we saw in the first part of the article, forms are a very important topic. Since they are extremely common in the applications we build.

Formularios-controlados-por-React-Itequia

In this article, we will continue learning how to create forms using components controlled by React. And we will see how to build generic components to help us minimize and optimize the code.

After creating the context in FormGeneric and the context provider, it is time to explain how to consume the context of FormGeneric.

Consuming context of FormGeneric

After the points seen in the previous article, now it’s time to focus on who consumes FormGeneric, FieldGeneric:

First step: We import

import { FC, useContext, ChangeEvent } from 'react';
import { FormContext } from './FormGeneric';

Second step: We consume the context

<FormContext.Consumer>
  {({ values, errors }) => (
	<div>
	...

Third step: We provide values and handles (handleChange, handelBlur) to the FieldGeneric component

{(type === 'Text') && (
<input
  type={type.toLowerCase()}
  id={name}
  value={
	values[name] === undefined ? '' : values[name]
  }
  onChange={handleChange}
  onBlur={handleBlur}
/>
)}
{type === 'TextArea' && (
...

Implementing FormGeneric and FieldGeneric in ArticleUsingGenericForm

In the image below, we show a generic form as well as any component, in this case, ArticleUsingFormGeneric, we use FormGeneric, our generic component that provides values/context, and FieldGeneric, our generic component that consumes values/context from FormGeneric.

Implementando-FormGenerico-FieldGenerico-ArticuloUsandoFormGenerico-Itequia

*The complete code of ArticleUsingFormGeneric.tsx is shown below

What sources have we used?

From this link on GitHub, you will be able to access the code used as a basis for developing this article and you will also have access to the book.

Likewise, it is important to exhibit that the valuesInitialization and GetValuesSetValueUseStateManually actions of FormGeneric are functions created after this source as a result of needs that we will indicate below:

How to edit an already-created record?

If we want to edit a record that has already been created, it means that we already have the necessary values.

In our applications it is very normal to implement CRUD of entities. For example, concerning the person entity, we will have a screen to create a person, read a person, update a person, delete a person, etc.

In the case that we want to edit an already existing person, we will have to start the useState values of our generic component FormGeneric, with the values we pass from ArticleUsingFormGeneric.tsx. Finally, be able to display the values in our generic component FieldGeneric.

As you can see in the image below:

Interactuar-directamente-control-values-desde-ArticuloUsandoFormGenerico-Itequia

How to interact directly with a control/values from ArticleUsingFormGeneric?

Apart from what we have discussed in the previous point, about the CRUD implementation, many times we need to be able to interact with controls on our screens before proceeding to send information, such as creating a new record, editing a new record, etc.

However, with the implementation that we have created at this point, it is not possible to act directly on the value of the input (control declared in our generic FieldGeneric component) from the ArticleUsingFormGeneric component.

For example, provide a default value for the number input by clicking on “Default value in number field”

Valor-por-defecto-campo-nombre-Formgenerico-Itequia

In this case, to overcome this limitation, we have GetValuesSetValueUseStateManually, a variable declared in FormGeneric.

This variable allows us to act directly with the values of FormGeneric from any component. No need to use context, which FieldGeneric does.

With GetValuesSetValueUseStateManually we have the values of the useState of FormGeneric from the ArticleUsingFormGeneric control. So we can on the one hand query the current values or introduce/update new ones.

On the other hand, setForceRedendering is nothing more than a useState created for React to render when it suits us.

As we can see in the image below:

Editar-registro-creado-Formgenerico-Itequia

What is the code for this example?

Next, we leave you the code of this example. However, this code is a very simple example used as an introduction to the explanation. We recommend starting from Chapter 15, and starting from this code, expanding it step by step with the needs of each project or development.

.\src\pages\Article\Article.tsx

import React, { useState, useEffect, ChangeEvent, FormEvent } from "react";
import { Page } from "../../components/Page/page";

interface Props {}

export const Article: React.FC<Props> = ({}) => {
  const [inputNameValue, setInputNameValue] = useState<string>("");
  const [inputSurnameValue, setInputSurnameValue] = useState<string>("");

  const requiredField = (value: any): string => {
    return value === undefined || value === null || value === ""
      ? "Required field"
      : "";
  };

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    //Field name required
    if (requiredField(inputNameValue) === "") {
      //Create record
      // await createRecord({
      //     name: inputNameValue,
      //     surname: inputSurnameValue
      // });
    } else {
      alert("The Name field is required");
    }
  };

  const handleInputNombreChange = (e: ChangeEvent<HTMLInputElement>) => {
    setInputNameValue(e.currentTarget.value);
  };

  const handleInputSurnameChange = (e: ChangeEvent<HTMLInputElement>) => {
    setInputSurnameValue(e.currentTarget.value);
  };

  return (
    // <Page>
    // </Page>
    <>
      <h1>
        Working with Forms in React and reducing boilerplate code with generic    components
      </h1>
      <form onSubmit={handleSubmit}>
        <p>
          <input
            id="name"
            type="text"
            placeholder="Name"
            value={inputNameValue}
            onChange={handleInputNameChange}
          />
        </p>
        <p>
          <input
            id="surname"
            type="text"
            placeholder="Surname"
            value={inputSurnameValue}
            onChange={handleInputSurnameChange}
          />
        </p>
        <p>
          <button type="submit">Save</button>
        </p>
      </form>
    </>
  );
};

.\src\pages\Article\ArticleUsingFormGeneric.tsx

import React, { useState, useEffect, ChangeEvent, FormEvent } from "react";
import { Page } from "../../components/Page/page";
import { FieldGeneric } from "./FieldGeneric";
import {
  FormGeneric,
  GetValuesSetValueUseStateManually,
  requiredField,
  Values,
} from "./FormGeneric";

interface Props {}

export const ArticleUsingFormGeneric: React.FC<Props> = ({}) => {
  const [inputNameValue, setInputNameValue] = useState<string>("");
  const [inputSurnameValue, setInputSurnameValue] = useState<string>("");
  const [forceRendering, setForceRendering] = useState(false);

  const handleSubmit = async (values: Values) => {
    let submit = true;
    //Create record
    // submit = await createRecord({
    //     name: values.name,
    //     surname: values.surname
    // });

    return { success: submit ? true : false };
  };

  const handleOnClickValueNameDefault = () => {
    const CurrentValues = GetValuesSetValueUseStateManually;

    GetValuesSetValueUseStateManually["name"] = "Mr. React";
    setForceRendering(!forceRendering);
  };

  return (
    // <Page>
    // </Page>
    <>
      <FormGeneric
        valuesInitialization={{
          name: "Mr. React",
          surname: "Pascual",
        }}
        validationRules={{
          name: [{ validator: requiredField }],
        }}
        onSubmit={handleSubmit}
        failureMessage="Error"
        successMessage="Ok"
      >
        <FieldGeneric name="name" label="Name" />
        <FieldGeneric name="surname" label="Surname" />

        <p>
          <button type="button" onClick={handleOnClickValueNameDefault}>
            Default value in name field
          </button>
        </p>
        <p>
          <button type="submit">Save</button>
        </p>
      </FormGeneric>
    </>
  );
};

.\src\pages\Article\FormGeneric.tsx

import { FC, useState, createContext, FormEvent } from "react";

export interface Values {
  [key: string]: any;
}

export interface Errors {
  [key: string]: string[];
}

export interface Touched {
  [key: string]: boolean;
}

interface FormContextProps {
  values: Values;
  setValue?: (fieldName: string, value: any) => void;
  errors: Errors;
  validate?: (fieldName: string) => void;
  touched: Touched;
  setTouched?: (fieldName: string) => void;
}

export let GetValuesSetValueUseStateManually: Values;

export const FormContext = createContext<FormContextProps>({
  values: {},
  errors: {},
  touched: {},
});

type Validator = (value: any, args?: any) => string;

export const requiredField: Validator = (value: any): string =>
  value === undefined || value === null || value === ""
    ? "Cant be empty"
    : "";

interface Validation {
  validator: Validator;
  arg?: any;
}

interface ValidationProp {
  [key: string]: Validation | Validation[];
}

export interface SubmitResult {
  success: boolean;
  errors?: Errors;
}

interface Props {
  validationRules?: ValidationProp;
  onSubmit: (values: Values) => Promise<SubmitResult>;
  successMessage?: string;
  failureMessage?: string;
  valuesInitialization?: Values;
}

export const FormGenerico: FC<Props> = ({
  children,
  validationRules,
  onSubmit,
  successMessage = "Ok",
  failureMessage = "Error",
  valuesInitialization = {},
}) => {
  const [values, setValues] = useState<Values>(valuesInitialization);
  const [errors, setErrors] = useState<Errors>({});
  const [touched, setTouched] = useState<Touched>({});
  const [submitting, setSubmitting] = useState(false);
  const [submitted, setSubmitted] = useState(false);
  const [submitError, setSubmitError] = useState(false);

  GetValuesSetValueUseStateManually = values;

  const validate = (fieldName: string): string[] => {
    if (!validationRules) {
      return [];
    }
    if (!validationRules[fieldName]) {
      return [];
    }
    const rules = Array.isArray(validationRules[fieldName])
      ? (validationRules[fieldName] as Validation[])
      : ([validationRules[fieldName]] as Validation[]);
    const fieldErrors: string[] = [];
    rules.forEach((rule) => {
      const error = rule.validator(values[fieldName], rule.arg);
      if (error) {
        fieldErrors.push(error);
      }
    });
    const newErrors = { ...errors, [fieldName]: fieldErrors };
    setErrors(newErrors);
    return fieldErrors;
  };

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (validateForm()) {
      setSubmitting(true);
      setSubmitError(false);
      const result = await onSubmit(values);
      setErrors(result.errors || {});
      setSubmitError(!result.success);
      setSubmitting(false);
      setSubmitted(true);
    }
  };

  const validateForm = () => {
    const newErrors: Errors = {};
    let haveError: boolean = false;
    if (validationRules) {
      Object.keys(validationRules).forEach((fieldName) => {
        newErrors[fieldName] = validate(fieldName);
        if (newErrors[fieldName].length > 0) {
          haveError = true;
        }
      });
    }
    setErrors(newErrors);
    return !haveError;
  };

  return (
    <FormContext.Provider
      value={{
        values,
        setValue: (fieldName: string, value: any) => {
          setValues({ ...values, [fieldName]: value });
        },
        errors,
        validate,
        touched,
        setTouched: (fieldName: string) => {
          setTouched({ ...touched, [fieldName]: true });
        },
      }}
    >
      <form noValidate={true} onSubmit={handleSubmit}>
        <fieldset disabled={submitting || (submitted && !submitError)}>
          {children}

          {submitted && submitError && <p>{failureMessage}</p>}
          {submitted && !submitError && <p>{successMessage}</p>}
        </fieldset>
      </form>
    </FormContext.Provider>
  );
};

.\src\pages\Article\FieldGeneric.tsx

import { FC, useContext, ChangeEvent } from "react";
import { FormContext } from "./FormGeneric";

interface Props {
  name: string;
  label?: string;
  type?: "Text" | "TextArea";
}

export const FieldGeneric: FC<Props> = ({ name, label, type = "Text" }) => {
  const { setValue, touched, setTouched, validate } = useContext(FormContext);
  const handleChange = (
    e: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement>
  ) => {
    if (setValue) {
      setValue(name, e.currentTarget.value);
    }
    if (touched[name]) {
      if (validate) {
        validate(name);
      }
    }
  };
  const handleBlur = () => {
    if (setTouched) {
      setTouched(name);
    }
    if (validate) {
      validate(name);
    }
  };
  return (
    <FormContext.Consumer>
      {({ values, errors }) => (
        <div>
          {label && <label>{label}</label>}
          {type === "Text" && (
            <input
              type={type.toLowerCase()}
              id={name}
              value={values[name] === undefined ? "" : values[name]}
              onChange={handleChange}
              onBlur={handleBlur}
            />
          )}
          {type === "TextArea" && (
            <textarea
              id={name}
              value={values[name] === undefined ? "" : values[name]}
              onChange={handleChange}
              onBlur={handleBlur}
            />
          )}
          {errors[name] &&
            errors[name].length > 0 &&
            errors[name].map((error) => <div key={error}>{error}</div>)}
        </div>
      )}
    </FormContext.Consumer>
  );
};

We hope that the development of this example will help you to learn how to create forms using components controlled by React and how to build generic components by minimizing and optimizing the code.

Hugo Pasctual – Software Developer at Itequia