Hola, soy Miguel y aquí les traigo otro nuevo artículo.
Índice
Trabajar con formularios en React
Es muy difícil, especialmente cuando hay campos dinámicos involucrados. Existe una serie de bibliotecas que facilitan todo el proceso. Una de esas bibliotecas es React Hook Form.
Este enfoque presenta algunas ventajas, principalmente que los usuarios no están vinculados a ningún marco de interfaz de usuario en particular o componentes de formulario predefinidos.
En esta publicación, crearemos un formulario de receta simple, que permite ingresar los detalles básicos junto con una lista dinámica de ingredientes. El resultado final se verá así:
En cuanto a la interfaz de usuario, no parece demasiado elegante, ya que el enfoque principal es usar React Hook Form. Aparte de eso, usaremos Semantic UI React, una biblioteca de componentes de IU y Emotion / styled, para poder ajustar los estilos de esos componentes.
Como primer paso, instalemos todas las dependencias necesarias:
npm i @emotion/core @emotion/styled semantic-ui-react semantic-ui-css react-hook-form
Ahora podemos configurar nuestro componente de formulario en un nuevo archivo, llamado Form.js
.
import React from "react"; import styled from "@emotion/styled"; import { useForm } from "react-hook-form"; export const Recipe = () => { return ( <Container> <h1>New recipe</Title> </Container> ); }; const Container = styled.div` display: flex; flex-direction: column; `;
Además, recuerde agregar import "semantic-ui-css/semantic.min.css";
en el index.js
, por encima de la costumbre index.css
estilos.
Forma Base
Con toda esta configuración fuera del camino, finalmente podemos comenzar a trabajar en el formulario en sí. Comenzaremos con la sección esencial, que tendrá la información general sobre la receta.
Para ayudar con la agrupación de campos de formulario en secciones, agreguemos un componente personalizado, llamado FieldSet
, que es una pequeña abstracción sobre el HTML nativo fieldset
.
// FieldSet.js export const FieldSet = ({ label, children }) => { return ( <Container> {label && <Legend>{label}</Legend>} <Wrapper>{children}</Wrapper> </Container> ); }; const Container = styled.fieldset` margin: 16px 0; padding: 0; border: none; `; const Wrapper = styled.div` display: flex; justify-content: space-between; flex-direction: column; align-items: self-start; `; const Legend = styled.legend` font-size: 16px; font-weight: bold; margin-bottom: 20px; `;
Para el formulario en sí, usaremos el componente Form
de Semantic UI React, que también viene con algunos subcomponentes útiles, como Form.Field
.
Para este formulario de receta simple, solo tendremos algunos campos básicos, como el nombre de la receta, la descripción y la cantidad de porciones. Agreguémoslos al formulario.
import React from "react"; import styled from "@emotion/styled"; import { Button, Form } from "semantic-ui-react"; import { FieldSet } from "./FieldSet"; const fieldWidth = 8; export const Recipe = () => { return ( <Container> <h1>New recipe</h1> <Form size="large"> <FieldSet label="Basics"> <Form.Field width={fieldWidth}> <label htmlFor="name">Name</label> <input type="text" name="name" id="name" /> </Form.Field> <Form.Field width={fieldWidth}> <label htmlFor="description">Description</label> <textarea name="description" id="description" /> </Form.Field> <Form.Field width={fieldWidth}> <label htmlFor="amount">Servings</label> <input type="number" name="amount" id="amount" /> </Form.Field> </FieldSet> <Form.Field> <Button>Save</Button> </Form.Field> </Form> </Container> ); }; const Container = styled.div` display: flex; flex-direction: column; padding: 25px 50px; `;
Aquí agregamos los campos de recetas con sus etiquetas, lo que da como resultado un formulario simple a continuación.
Tenga en cuenta el uso de atributos name
en los elementos del formulario, ya que serán útiles en un momento.
También usamos una combinación de atributos htmlFor
e id
para mejorar la accesibilidad de los campos.
Ahora es el momento de usar React Hook Form para administrar el estado de nuestro formulario. Uno de los puntos de venta de la biblioteca es que facilita la administración del estado, sin la necesidad de agregar un montón de ganchos setState
.
Todo lo que tenemos que hacer es usar una combinación de atributos name
y ref
para registrar campos en el estado del formulario, para poder empezar a usar React Hook Form.
import React from "react"; import styled from "@emotion/styled"; import { Button, Form } from "semantic-ui-react"; import { FieldSet } from "./FieldSet"; import { useForm } from "react-hook-form"; const fieldWidth = 8; export const Recipe = () => { const { register, handleSubmit } = useForm(); const submitForm = formData => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <Form size="large" onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Form.Field width={fieldWidth}> <label htmlFor="name">Name</label> <input type="text" name="name" id="name" ref={register} /> </Form.Field> <Form.Field width={fieldWidth}> <label htmlFor="description">Description</label> <textarea name="description" id="description" ref={register} /> </Form.Field> <Form.Field width={fieldWidth}> <label htmlFor="amount">Servings</label> <input type="number" name="amount" id="amount" ref={register} /> </Form.Field> </FieldSet> <Form.Field> <Button>Save</Button> </Form.Field> </Form> </Container> ); };
Empezamos importando y llamando a hook useForm
, que devuelve varios ayudantes útiles. En este caso usamos register
para asignar un campo de formulario a través de su nombre a la propiedad correspondiente en el estado.
Es por eso que agregar nombres a los campos es importante aquí.
También necesitamos ajustar nuestra función de envío en handleSubmit
llamar de vuelta. Ahora, si ingresamos los detalles de una receta en los campos del formulario y presionamos Save
, deberíamos ver un objeto siguiente en la consola:
{ name: "Pancakes", description: "Super delicious pancake recipe", amount: "10" }
Validación de formularios y manejo de errores
El valor register
que obtenemos de useForm
es en realidad una función que acepta parámetros de validación como un objeto. Hay varias reglas de validación disponibles:
- necesario
- min
- max
- longitud mínima
- longitud máxima
- patrón
- validar
Para que el nombre de la receta sea un campo obligatorio, todo lo que tenemos que hacer es llamar al registro con un accesorio required
:
Adicionalmente, useForm
devuelve el objeto errors
, que asigna todos los errores planteados a los nombres de campo. Entonces, en caso de que falte la receta, nombre el errors
tendría un objeto name
con tipo required
.
También vale la pena señalar que en lugar de especificar una regla de validación con un valor booleano, también podemos pasarle una cadena, que se utilizará como mensaje de error:
Alternativamente, la propiedad message
se puede utilizar para esto. Posteriormente se puede acceder al mensaje de error a través de errors.name.message
.
También pasamos los errores de campo como valores booleanos a Form.Field
para alternar el estado de error.
Ahora podemos combinar la validación de formularios y los errores para mostrar mensajes útiles para los usuarios.
Si intentamos enviar el formulario con datos no válidos, obtenemos útiles mensajes de validación para los campos.
En React Hook Form también es posible aplicar reglas de validación personalizadas a los campos a través de la regla validate
.
Puede ser una función o un objeto de funciones con diferentes reglas de validación. Por ejemplo, podemos validar si el valor del campo es igual así:
ref={register({validate: value => value % 2 === 0})
Manejo de entradas de números
En la forma actual, estamos usando el campo de entrada de números para las porciones. Sin embargo, debido a cómo funcionan los elementos de entrada HTML, cuando se envía el formulario, este valor será una cadena en los datos del formulario.
En algunos casos, esto podría no ser lo que queremos, por ejemplo. si se espera que los datos sean un número en el backend.
De esa forma, cuando se envía el formulario, los datos tienen los tipos que necesitamos. Para conectar este componente al formulario, React Hook Form proporciona Controller
– una envoltura para trabajar con componentes externos controlados.
Primero, creemos dicho componente, llamado NumberInput
.
// NumberInput.js import React from "react"; export const NumberInput = ({ value, onChange, ...rest }) => { const handleChange = e => { onChange(Number(e.target.value)); }; return ( <input type="number" min={0} onChange={handleChange} value={value} {...rest} /> ); };
Después de eso, podemos reemplazar el campo amount
actual con este nuevo componente.
import { useForm, Controller } from "react-hook-form"; //... const { register, handleSubmit, errors, control } = useForm(); //... <Form.Field width={fieldWidth} error={!!errors.amount}> <label htmlFor="amount">Servings</label> <Controller control={control} name="amount" defaultValue={0} rules={{max: 10}} render={props => <NumberInput id="amount" {...props} />} /> {errors.amount && ( <ErrorMessage>Maximum number of servings is 10.</ErrorMessage> )} </Form.Field>
En vez de register
, usamos objeto control
que obtenemos de useForm
, para la validación usamos rules
. Todavía necesitamos agregar el atribuir name
a Controller
para registrarlo. Luego pasamos el componente de entrada a través del apoyo render
.
Ahora, los datos de las porciones de recetas se guardarán en el formulario como antes, mientras se usa un componente externo a React Hook Form.
Campos dinámicos
Ninguna receta está completa sin sus ingredientes. Sin embargo, no podemos agregar campos de ingredientes fijos a nuestro formulario, ya que su número varía según la fórmula.
Normalmente, necesitaríamos implementar nuestra propia lógica personalizada para manejar campos dinámicos, sin embargo, React Hook Form viene con un gancho personalizado para trabajar con entradas dinámicas: useFieldArray
.
Toma el objeto de control del formulario y el nombre del campo, devolviendo varias utilidades para trabajar con entradas dinámicas. Veámoslo en acción agregando los campos de ingredientes a nuestro formulario de receta.
import React from "react"; import styled from "@emotion/styled"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { Button, Form } from "semantic-ui-react"; import { FieldSet } from "./FieldSet"; import { NumberInput } from "./NumberInput"; const fieldWidth = 8; export const Recipe = () => { const { register, handleSubmit, errors, control } = useForm(); const { fields, append, remove } = useFieldArray({ name: "ingredients", control }); const submitForm = formData => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <Form size="large" onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Form.Field width={fieldWidth} error={!!errors.name}> <label htmlFor="name">Name</label> <input type="text" name="name" id="name" ref={register({ required: "Recipe name is required." })} /> {errors.name && <ErrorMessage>{errors.name.message}</ErrorMessage>} </Form.Field> <Form.Field width={fieldWidth} error={!!errors.description}> <label htmlFor="description">Description</label> <textarea name="description" id="description" ref={register({ maxLength: 100 })} /> {errors.description && ( <ErrorMessage> Description cannot be longer than 100 characters. </ErrorMessage> )} </Form.Field> <Form.Field width={fieldWidth} error={!!errors.amount}> <label htmlFor="amount">Servings</label> <Controller control={control} name="amount" defaultValue={0} rules={{max: 10}} render={props => <NumberInput id="amount" {...props} />} /> {errors.amount && ( <ErrorMessage>Maximum number of servings is 10.</ErrorMessage> )} </Form.Field> </FieldSet> <FieldSet label="Ingredients"> {fields.map((field, index) => { return ( <Row key={field.id}> <Form.Field width={8}> <label htmlFor={`ingredients[${index}].name`}>Name</label> <input type="text" ref={register()} name={`ingredients[${index}].name`} id={`ingredients[${index}].name`} /> </Form.Field> <Form.Field width={6}> <label htmlFor={`ingredients[${index}].amount`}>Amount</label> <input type="text" ref={register()} defaultValue={field.amount} name={`ingredients[${index}].amount`} id={`ingredients[${index}].amount`} /> </Form.Field> <Button type="button" onClick={() => remove(index)}> − </Button> </Row> ); })} <Button type="button" onClick={() => append({ name: "", amount: "" })} > Add ingredient </Button> </FieldSet> <Form.Field> <Button>Save</Button> </Form.Field> </Form> </Container> ); }; const Container = styled.div` display: flex; flex-direction: column; padding: 25px 50px; `; const ErrorMessage = styled.span` font-size: 12px; color: red; `; const Row = styled.div` display: flex; align-items: center; & > * { margin-right: 20px !important; } .ui.button { margin: 10px 0 0 8px; } `; ErrorMessage.defaultProps = { role: "alert" };
El primer paso es importar useFieldArray
y llámalo con el control
obtenemos del gancho de formulario, así como para pasarle el nombre del campo. useFieldArray
devuelve varias utilidades para administrar campos dinámicos, de las cuales usaremos append,
remove
y la matriz de los propios campos de React Hook Form.
La lista completa de funciones de utilidad está disponible en la biblioteca sitio de documentación. Dado que no tenemos valores predeterminados para los ingredientes, el campo está inicialmente vacío.
Podemos comenzar a poblarlo usando la función append
y proporcionándole valores predeterminados para campos vacíos.
Tenga en cuenta que la representación de los campos se realiza mediante su índice en una matriz, por lo que es importante tener los nombres de los campos en formato fieldArrayName[fieldIndex][fieldName]
.
delete
. Ahora, después de agregar algunos campos de ingredientes y completar sus valores, cuando enviemos el formulario, todos esos valores se guardarán en el ingredients
campo en el formulario.Eso es básicamente todo lo que se necesita para crear un formulario completamente funcional y fácilmente manejable con React Hook Form. La biblioteca tiene muchas más funciones, que no se tratan en esta publicación, así que asegúrese de consultar la documentación para más ejemplos.
Gracias por leer este post.
Añadir comentario