Com crear components genèrics forms i fields per reduir codi repetitiu amb react i Typescript? | PART II

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

Com crear components genèrics forms i fields per reduir codi repetitiu amb react i Typescript? | PART II

Com vam veure a la primera part de l’article, els formularis són un tema molt important. Ja que són extremadament comuns a les aplicacions que construïm.

Formularios-controlados-por-React-Itequia

En aquest article, continuarem aprenent com crear formularis usant components controlats per React. I veurem com construir components genèrics per ajudar-nos a minimitzar i optimitzar el codi.

Després de crear el context a FormGeneric i el proveïdor de context, arriba el moment d’explicar com consumir el context d’un FormGeneric.

Consumint context de FormGeneric

Després dels punts vistos a l’article anterior, ara toca centrem en qui consumeix de FormGeneric, FieldGeneric:

Primer pas: Importem

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

Segon pas: Consumim el context

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

Tercer pas: Proporcionem valors i manejadors (handleChange, handelBlur) al component FieldGeneric

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

Implementant FormGeneric i FieldGeneric a ArticleUsingFormGeneric

A la imatge que segueix, mostrem d’una forma genèrica com d’un component qualsevol, en aquest cas, ArticleUsingFormGeneric, fem servir FormGeneric, el nostre component genèric que proveeix valors/context, i FieldGeneric, el nostre component genèric que consumeix valors/context de FormGeneric.

Implementando-FormGenerico-FieldGenerico-ArticuloUsandoFormGenerico-Itequia

* Més avall es mostra el codi complet ArticleUsingFormGeneric.tsx

Quines fonts hem fet servir?

Des de aquest link a GitHub podreu accedir al codi utilitzat com a base per desenvolupar aquest article i també tindreu accés al llibre.

Així mateix, és important indicar que les accions valuesInitialization i GetValuesSetValueUseStateManually de FormGeneric són funcions creades posteriors a aquesta font com a conseqüència de necessitats que més avall anirem indicant:

Com editar un registre ja creat?

Si volem editar un registre ja creat, vol dir que ja disposem dels valors necessaris.

A les nostres aplicacions és molt normal implementar CRUD d’entitats. Per exemple, en relació amb l’entitat persona, tindrem una pantalla per crear persona, llegir persona, actualitzar persona, esborrar persona, etc.

En el cas que vulguem editar una persona ja existent, haurem d’iniciar l’useState values del nostre component genèric FormGeneric, amb els valors que passem des d’ArticleUsingFormGeneric.tsx. Per finalment poder mostrar els valors al nostre component genèric FieldGeneric.

Com podeu veure a la següent imatge:

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

Com interactuar directament amb un control/values des d’ArticleUsingFormGeneric?

A part del que hem comentat al punt anterior, sobre la implementació CRUD, moltes vegades tenim la necessitat de poder interactuar amb controls de les nostres pantalles abans de procedir a enviar informació, com crear un nou registre, editar un nou registre, etc.

No obstant això, amb la implementació que hem creat en aquest punt, no és possible actuar directament sobre el valor d’un input (control declarat al nostre component genèric FieldGeneric) des del component ArticleUsingFormGeneric.

Per exemple, proporcionar un valor per defecte a l’input nom en fer clic a “Valor per defecte en camp nom”

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

En aquest cas, per superar aquesta limitació, disposem de GetValuesSetValueUseStateManually, variable declarada a FormGeneric.

Aquesta variable ens permet actuar directament amb els valors de FormGeneric des de qualsevol component. Sense necessitat de fer servir el seu context, cosa que sí que fa FieldGeneric.

Amb GetValuesSetValueUseStateManually disposem dels values de l’useState de FormGeneric des del control ArticleUsingFormGeneric. Així que podem per una banda fer una consulta dels valors actuals o introduir/actualitzar nous.

D’altra banda, setForceRedendering no és res més que un useState creat perquè React renderitzi quan ens convingui.

Com observem a la següent imatge:

Editar-registro-creado-Formgenerico-Itequia

Quin és el codi daquest exemple?

A continuació us deixem el codi d’aquest exemple. Aquest codi, però, és un exemple molt simple utilitzat com a introducció per a l’explicació. Aconsellem partir del Capítol 15, ia partir d’aquest codi anar-lo ampliant pas a pas amb les necessitats de cada projecte o desenvolupament.

.\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>
  );
};

Esperem que el desenvolupament d’aquest exemple us serveixi d’ajuda per aprendre com crear formularis usant components controlats per React i com construir components genèrics minimitzant i optimitzant el codi.

Hugo Pasctual – Software Developer at Itequia