¿Cómo crear componentes genéricos Forms y Fields para reducir código repetitivo? | Parte II

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

¿Cómo crear componentes genéricos Forms y Fields para reducir código repetitivo? | Parte II

Como vimos en la primera parte del artículo, los formularios son un tema muy importante. Ya que son extremadamente comunes en las aplicaciones que construimos.

Formularios-controlados-por-React-Itequia

En este artículo, continuaremos aprendiendo cómo crear formularios usando componentes controlados por React. Y veremos cómo construir componentes genéricos para ayudarnos a minimizar y optimizar el código.

Tras crear el context en FormGenerico y el proveedor de contexto, llega el momento de explicar cómo consumir el context de un FormGenerico.

Consumiendo context de FormGenerico

Tras los puntos vistos en el artículo anterior, ahora toca centramos en quién consume de FormGenerico, FieldGenerico:

Primer paso: Importamos

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

Segundo paso: Consumimos el contexto

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

Tercer paso: Proporcionamos valores y manejadores (handleChange, handelBlur) al componente FieldGenerico

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

Implementando FormGenerico y FieldGenerico en ArticuloUsandoFormGenerico

En la imagen a continuación, mostramos de una forma genérica como de un componente cualquiera, en este caso, ArticuloUsandoFormGenerico, usamos FormGenerico, nuestro componente genérico que provee valores/contexto, y FieldGenerico, nuestro componente genérico que consume valores/contexto de FormGenerico.

Implementando-FormGenerico-FieldGenerico-ArticuloUsandoFormGenerico-Itequia

* Más abajo se muestra el código completo de ArticuloUsandoFormGenerico.tsx

¿Qué fuentes hemos utilizado?

Desde este link a GitHub podréis acceder al código usado como base para desarrollar este artículo y también tendréis acceso al libro.

Asimismo, es importante indicar que las acciones valuesInitialization y GetValuesSetValueUseStateManually de FormGenerico son funciones creadas posteriores a esta fuente como consecuencia de necesidades que más abajo iremos indicando:

¿Cómo editar un registro ya creado?

Si queremos editar un registro ya creado, quiere decir que ya disponemos de los valores necesarios.

En nuestras aplicaciones es muy normal implementar CRUD de entidades. Por ejemplo, en relación a la entidad persona, tendremos una pantalla para crear persona, leer persona, actualizar persona, borrar persona, etc.

En el caso de que deseemos editar una persona ya existente, vamos a tener que iniciar el useState values de nuestro componente genérico FormGenerico, con los valores que pasamos desde ArticuloUsandoFormGenerico.tsx. Para finalmente poder mostrar los valores en nuestro componente genérico FieldGenerico.

Como podéis ver en la imagen a continuación:

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

¿Cómo interactuar directamente con un control/values desde ArticuloUsandoFormGenerico?

A parte de lo que hemos comentado en el punto anterior, sobre la implementación CRUD, muchas veces tenemos la necesidad de poder interactuar con controles de nuestras pantallas antes de proceder a enviar información, como crear un nuevo registro, editar un nuevo registro, etc.

No obstante, con la implementación que hemos creado a este punto, no es posible actuar directamente sobre el valor de un input (control declarado en nuestro componente genérico FieldGenerico) desde el componente ArticuloUsandoFormGenerico.

Por ejemplo, proporcionar un valor por defecto al input nombre al hacer click en “Valor por defecto en campo nombre”

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

En este caso, para superar dicha limitación, disponemos de GetValuesSetValueUseStateManually, variable declarada en FormGenerico.

Esta variable nos permite actuar directamente con los valores de FormGenerico desde cualquier componente. Sin necesidad de usar su contexto, cosa que sí que hace FieldGenerico.

Con GetValuesSetValueUseStateManually disponemos de los values del useState de FormGenerico desde el control ArticuloUsandoFormGenerico. Así que podemos por un lado hacer una consulta de los values actuales o introducir/actualizar nuevos.

Por otro lado, setForceRedendering no es más que un useState creado para que React renderice cuando nos convenga.

Como observamos en la imagen a continuación:

Editar-registro-creado-Formgenerico-Itequia

¿Cuál es el código de este ejemplo?

A continuación, os dejamos el código de este ejemplo. No obstante, este código es un ejemplo muy simple utilizado como introducción para la explicación. Aconsejamos partir del Capítulo 15, y a partir de este código ir ampliándolo paso a paso con las necesidades de cada proyecto o desarrollo.

.\src\pages\Articulo\Articulo.tsx

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

interface Props {}

export const Articulo: React.FC<Props> = ({}) => {
  const [inputNameValue, setInputNameValue] = useState<string>("");
  const [inputApellidoValue, setInputApellidoValue] = useState<string>("");

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

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    //Campo nombre requerido
    if (requiredField(inputNameValue) === "") {
      //Creamos registro
      // await creamosRegistro({
      //     name: inputNameValue,
      //     apellido: inputApellidoValue
      // });
    } else {
      alert("El campo Nombre es requerido");
    }
  };

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

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

  return (
    // <Page>
    // </Page>
    <>
      <h1>
        Trabajando con Forms en React y reducción de código repetitivo con
        componentes genéricos
      </h1>
      <form onSubmit={handleSubmit}>
        <p>
          <input
            id="nombre"
            type="text"
            placeholder="Nombre"
            value={inputNameValue}
            onChange={handleInputNombreChange}
          />
        </p>
        <p>
          <input
            id="apellido"
            type="text"
            placeholder="Apellido"
            value={inputApellidoValue}
            onChange={handleInputApellidoChange}
          />
        </p>
        <p>
          <button type="submit">Guardar</button>
        </p>
      </form>
    </>
  );
};

.\src\pages\Articulo\ArticuloUsandoFormGenerico.tsx

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

interface Props {}

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

  const handleSubmit = async (values: Values) => {
    let submit = true;
    //Creamos registro
    // submit = await creamosRegistro({
    //     name: values.nombre,
    //     apellido: values.apellido
    // });

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

  const handleOnClickValorNombrePorDefecto = () => {
    const valoresActuales = GetValuesSetValueUseStateManually;

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

  return (
    // <Page>
    // </Page>
    <>
      <FormGenerico
        valuesInitialization={{
          nombre: "Mr. React",
          apellido: "Pascual",
        }}
        validationRules={{
          nombre: [{ validator: requiredField }],
        }}
        onSubmit={handleSubmit}
        failureMessage="Error"
        successMessage="Ok"
      >
        <FieldGenerico name="nombre" label="Nombre" />
        <FieldGenerico name="apellido" label="Apellido" />

        <p>
          <button type="button" onClick={handleOnClickValorNombrePorDefecto}>
            Valor por defecto en campo nombre
          </button>
        </p>
        <p>
          <button type="submit">Guardar</button>
        </p>
      </FormGenerico>
    </>
  );
};

.\src\pages\Articulo\FormGenerico.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 === ""
    ? "No puede estar vacío"
    : "";

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\Articulo\FieldGenerico.tsx

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

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

export const FieldGenerico: 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>
  );
};

Esperamos que el desarrollo de este ejemplo os sirva de ayuda para aprender cómo crear formularios usando componentes controlados por React y cómo construir componentes genéricos minimizando y optimizando el código.

Hugo Pasctual – Software Developer at Itequia