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. 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.
Després dels punts vistos a l’article anterior, ara toca centrem en qui consumeix de FormGeneric, FieldGeneric:
import { FC, useContext, ChangeEvent } from 'react';
import { FormContext } from './FormGeneric';
<FormContext.Consumer>
{({ values, errors }) => (
<div>
...
{(type === 'Text') && (
<input
type={type.toLowerCase()}
id={name}
value={
values[name] === undefined ? '' : values[name]
}
onChange={handleChange}
onBlur={handleBlur}
/>
)}
{type === 'TextArea' && (
...
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.
* Més avall es mostra el codi complet ArticleUsingFormGeneric.tsx
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:
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:
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”
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:
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.
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>
</>
);
};
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>
</>
);
};
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>
);
};
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.