Cuando desarrollamos aplicaciones, una tarea común, fundamental y crítica es la recepción de datos por parte del usuario. Es esta actividad la inicia el proceso básico de un sistema:

Entrada (Input del usuario) --> Procesamiento --> Salida (Presentación de resultados).

Veamos como realizamos esta tarea en ReactJS, partiendo de la forma básica, para ello vamos a crear un componente llamado Formulario el cual va a permitirle al usuario indicar sus datos y luego estos serán enviados a una API para ser procesados y guardados.

Recordemos que con ReactJS implementamos SPAs (Single Page Applications), por lo tanto, la gestión del formulario va a variar un poco con relación a un form tradicional de HTML.

Formulario como componente con useState

Para este componente formulario, nos vamos a apoyar en el hook useState, de forma tal que podamos capturar los valores introducidos por el usuario, toda vez que haga algún cambio en él alguna de las entradas disponibles. El código de ese componente será el siguiente:

import React, { useState } from 'react';

const countries = [
  "Argentina", "Bolivia", "Brasil", "Chile", "Colombia",
  "Ecuador", "Guyana", "Paraguay", "Perú", "Surinam",
  "Uruguay", "Venezuela"
];

const Formulario = () => {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    confirmPassword: '',
    name: '',
    country: '',
    phone: '',
    photo: null
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({ ...formData, [name]: value });
  };

  const handlePhotoChange = (e) => {
    setFormData({ ...formData, photo: e.target.files[0] });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form data submitted:', formData);
  };

  return (
    <div className="App">
      <form onSubmit={handleSubmit}>
        <h2>Datos de Acceso</h2>
        <div>
          <label>Email:</label>
          <input type="email" name="email" value={formData.email} onChange={handleChange} required />
        </div>
        <div>
          <label>Clave:</label>
          <input type="password" name="password" value={formData.password} onChange={handleChange} required />
        </div>
        <div>
          <label>Confirmar Clave:</label>
          <input type="password" name="confirmPassword" value={formData.confirmPassword} onChange={handleChange} required />
        </div>
        <h2>Datos Personales</h2>
        <div>
          <label>Nombre:</label>
          <input type="text" name="name" value={formData.name} onChange={handleChange} required />
        </div>
        <div>
          <label>País:</label>
          <select name="country" value={formData.country} onChange={handleChange} required>
            <option value="">Seleccione un país</option>
            {countries.map(country => (
              <option key={country} value={country}>{country}</option>
            ))}
          </select>
        </div>
        <div>
          <label>Teléfono:</label>
          <input type="tel" name="phone" value={formData.phone} onChange={handleChange} required />
        </div>
        <div>
          <label>Foto:</label>
          <input type="file" name="photo" accept="image/*" onChange={handlePhotoChange} required />
        </div>
        <button type="submit">Enviar</button>
      </form>
    </div>
  );
};

export default Formulario;

El componente tiene 3 funciones, las cuales explicamos:

  • handleChange, encargada de capturar el evento onChange de las cajas y listas del formulario, con ello actualizamos el estado definido al inicio que contendrá los valores indicados por el usuario.
  • handlePhotoChange, la cual nos ayudará a capturar la información de la foto solicitada en el formulario, siendo que el tipo de input file se maneja de forma diferente, por ello es importante separar esta funcionalidad.
  • handleSubmit, en esta función implementaremos lo necesario para enviar los datos a la capa de backend.

Enviando la foto en formato base64

Es común que el envío de archivos implique realizar algunos procesos adicionales, por ejemplo, tomar el contenido y codificarlo en base64 para transformarlo en un string y así poderlo enviar de forma más fácil a la capa de backend, para ello agregamos una función responsable de esa tarea en el código:

const toBase64 = (file) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = (error) => reject(error);
  });

Observa, que por ser una operación que implica tiempo no determinado, JavaScript la maneja como una promesa, es decir, cuando finalice la conversión, nos va a llamar y nos dará el resultado.

Además de hacer esa transformación, es importante que enviemos los datos de nombre original de archivo, su tamaño y su tipo de contenido, ya que en la capa de backend esos datos serán necesarios, para ello mejoramos la función handlePhotoChange, dejándola de esta forma:

  const handlePhotoChange = (e) => {
    const file = e.target.files[0];
    if (file) {
      setFormData({
        ...formData,
        photo: file,
        photoName: file.name,
        photoType: file.type,
        photoSize: file.size
      });
    }
  };

Ahora con este proceso podemos mejorar nuestra función handleSubmit incorporando la conversión de la foto y el envío de los datos a la capa de backend.

const handleSubmit = async (e) => {
    e.preventDefault();

    // Convertir la foto a base64
    let base64Photo = '';
    if (formData.photo) {
      base64Photo = await toBase64(formData.photo);
    }

    // Crear el objeto con los datos del formulario
    const dataToSend = {
      email: formData.email,
      password: formData.password,
      confirmPassword: formData.confirmPassword,
      name: formData.name,
      country: formData.country,
      phone: formData.phone,
      photo: {
        base64: base64Photo,
        name: formData.photoName,
        type: formData.photoType,
        size: formData.photoSize
      }
    };

    // Enviar la solicitud POST a la API
    try {
      const response = await fetch('https://leonardojose.dev/api/usuarios', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(dataToSend)
      });

      if (!response.ok) {
        throw new Error('Error en la solicitud');
      }

      const result = await response.json();
      console.log('Form data submitted:', result);
    } catch (error) {
      console.error('Error:', error);
    }
  };

Recuerda, React trabaja como SPA, por lo tanto, el envío de datos es asíncrono, es por ello que lo primero que realizamos en la función de submit es evitar que el navegador nos haga un envío de datos o POST con la línea: e.preventDefault();

Trabajando con TypeScript

El código anteriormente implementado es totalmente váildo cuando trabajamos en JavaScript, solo que actualmente, ha tenido un gran auge, el uso del SuperJavascript, o mejor conocido como TypeScript.

Vamos a convertir nuestro componente a lenguaje typescript, para ello vamos a definir los tipos de datos correctamente, vamos a cambiar la extensión del componente para Formulario.tsx y con ello vamos a aprovechar las bondades que TypeScript, nos da, la principal para mí, poder determinar errores en tiempo de compilación.

El código del componente ahora quedará:

import React, { useState, ChangeEvent, FormEvent } from 'react';

const countries = [
  "Argentina", "Bolivia", "Brasil", "Chile", "Colombia",
  "Ecuador", "Guyana", "Paraguay", "Perú", "Surinam",
  "Uruguay", "Venezuela"
];

interface FormData {
  email: string;
  password: string;
  confirmPassword: string;
  name: string;
  country: string;
  phone: string;
  photo: File | null;
  photoName: string;
  photoType: string;
  photoSize: number | string;
}

const Formulario: React.FC = () => {
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: '',
    confirmPassword: '',
    name: '',
    country: '',
    phone: '',
    photo: null,
    photoName: '',
    photoType: '',
    photoSize: ''
  });

  const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
    const { name, value } = e.target;
    setFormData({ ...formData, [name]: value });
  };

  const handlePhotoChange = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files ? e.target.files[0] : null;
    if (file) {
      setFormData({
        ...formData,
        photo: file,
        photoName: file.name,
        photoType: file.type,
        photoSize: file.size
      });
    }
  };

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();

    // Convertir la foto a base64
    let base64Photo = '';
    if (formData.photo) {
      base64Photo = await toBase64(formData.photo);
    }

    // Crear el objeto con los datos del formulario
    const dataToSend = {
      email: formData.email,
      password: formData.password,
      confirmPassword: formData.confirmPassword,
      name: formData.name,
      country: formData.country,
      phone: formData.phone,
      photo: {
        base64: base64Photo,
        name: formData.photoName,
        type: formData.photoType,
        size: formData.photoSize
      }
    };

    // Enviar la solicitud POST a la API
    try {
      const response = await fetch('https://leonardojose.dev/api/usuarios', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(dataToSend)
      });

      if (!response.ok) {
        throw new Error('Error en la solicitud');
      }

      const result = await response.json();
      console.log('Form data submitted:', result);
    } catch (error) {
      console.error('Error:', error);
    }
  };

  const toBase64 = (file: File) => new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result as string);
    reader.onerror = (error) => reject(error);
  });

  return (
    <div className="App">
      <form onSubmit={handleSubmit}>
        <h2>Datos de Acceso</h2>
        <div>
          <label>Email:</label>
          <input type="email" name="email" value={formData.email} onChange={handleChange} required />
        </div>
        <div>
          <label>Clave:</label>
          <input type="password" name="password" value={formData.password} onChange={handleChange} required />
        </div>
        <div>
          <label>Confirmar Clave:</label>
          <input type="password" name="confirmPassword" value={formData.confirmPassword} onChange={handleChange} required />
        </div>
        <h2>Datos Personales</h2>
        <div>
          <label>Nombre:</label>
          <input type="text" name="name" value={formData.name} onChange={handleChange} required />
        </div>
        <div>
          <label>País:</label>
          <select name="country" value={formData.country} onChange={handleChange} required>
            <option value="">Seleccione un país</option>
            {countries.map(country => (
              <option key={country} value={country}>{country}</option>
            ))}
          </select>
        </div>
        <div>
          <label>Teléfono:</label>
          <input type="tel" name="phone" value={formData.phone} onChange={handleChange} required />
        </div>
        <div>
          <label>Foto:</label>
          <input type="file" name="photo" accept="image/*" onChange={handlePhotoChange} required />
        </div>
        <button type="submit">Enviar</button>
      </form>
    </div>
  );
};

export default Formulario;

Observa que aparecen los tipos de datos de cada propiedad, de casda evento e incluso del componente React, algo que es fundamental para la mantenibilidad y legibilidad del código.

Luego nuestro código podrá ser incoporado en proyectos que usen TypeScript como base, lo cual está sucediendo con más frecuencia cada día.

Mejorando la visual del formulario

Hasta ahora nuestro código apenas tiene HTML y no usa nada de las bondades de CSS, por lo tanto, será funcional, pero no será agradable a la vista. Para ello vamos a incorporarle estilos del framework para CSS tailwind, específicamente del plugin daisyui.

Para instalar Tailwind en una aplicación react con vite, puedes seguir estas instrucciones:

How to Setup React and Tailwind CSS with Vite in a Project
Tailwind CSS is a popular CSS framework, and React is one of the most popularJavaScript libraries. And Tailwind CSS and React are a great combo to use if you’re building afrontend project. In this article, you will learn how to setup your coding environment with Vite,install React and Tailwind …
Parameters-vs-Arguments--1-

Luego de instalado Tailwind, debemos instalar daisyui, esto lo logramos con las siguientes instrucciones:

npm install daisyui

Incluso es posible usar un paquete que ya genera los componentes para react, llamado react-daisyui (no lo usamos aquí, pero es interesante conocerlo), el cual podemos instalar con el siguiente comando:

npm install react-daisyui

Ahora corresponde realizar el ajuste en nuestro componente, de forma que podamos aprovechar la visual de daisyui, quedando nuestro componente como sigue:

import React, { useState, ChangeEvent, FormEvent } from 'react';
import 'daisyui/dist/full.css';

const countries = [
  "Argentina", "Bolivia", "Brasil", "Chile", "Colombia",
  "Ecuador", "Guyana", "Paraguay", "Perú", "Surinam",
  "Uruguay", "Venezuela"
];

interface FormData {
  email: string;
  password: string;
  confirmPassword: string;
  name: string;
  country: string;
  phone: string;
  photo: File 
  photoName: string;
  photoType: string;
  photoSize: number | string;
}

const Formulario: React.FC = () => {
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: '',
    confirmPassword: '',
    name: '',
    country: '',
    phone: '',
    photo: null,
    photoName: '',
    photoType: '',
    photoSize: ''
  });

  const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
    const { name, value } = e.target;
    setFormData({ ...formData, [name]: value });
  };

  const handlePhotoChange = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files ? e.target.files[0] : null;
    if (file) {
      setFormData({
        ...formData,
        photo: file,
        photoName: file.name,
        photoType: file.type,
        photoSize: file.size
      });
    }
  };

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();

    // Convertir la foto a base64
    let base64Photo = '';
    if (formData.photo) {
      base64Photo = await toBase64(formData.photo);
    }

    // Crear el objeto con los datos del formulario
    const dataToSend = {
      email: formData.email,
      password: formData.password,
      confirmPassword: formData.confirmPassword,
      name: formData.name,
      country: formData.country,
      phone: formData.phone,
      photo: {
        base64: base64Photo,
        name: formData.photoName,
        type: formData.photoType,
        size: formData.photoSize
      }
    };

    // Enviar la solicitud POST a la API
    try {
      const response = await fetch('https://leonardojose.dev/api/usuarios', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(dataToSend)
      });

      if (!response.ok) {
        throw new Error('Error en la solicitud');
      }

      const result = await response.json();
      console.log('Form data submitted:', result);
    } catch (error) {
      console.error('Error:', error);
    }
  };

  const toBase64 = (file: File) => new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result as string);
    reader.onerror = (error) => reject(error);
  });

  return (
    <div className="App container mx-auto p-4">
      <form onSubmit={handleSubmit} className="bg-white p-6 rounded shadow-md w-full max-w-lg mx-auto">
        <h2 className="text-2xl font-bold mb-4">Datos de Acceso</h2>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Email:</label>
          <input
            type="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            required
            className="input input-bordered w-full"
          />
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Clave:</label>
          <input
            type="password"
            name="password"
            value={formData.password}
            onChange={handleChange}
            required
            className="input input-bordered w-full"
          />
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Confirmar Clave:</label>
          <input
            type="password"
            name="confirmPassword"
            value={formData.confirmPassword}
            onChange={handleChange}
            required
            className="input input-bordered w-full"
          />
        </div>
        <h2 className="text-2xl font-bold mb-4">Datos Personales</h2>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Nombre:</label>
          <input
            type="text"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
            className="input input-bordered w-full"
          />
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">País:</label>
          <select
            name="country"
            value={formData.country}
            onChange={handleChange}
            required
            className="select select-bordered w-full"
          >
            <option value="">Seleccione un país</option>
            {countries.map(country => (
              <option key={country} value={country}>{country}</option>
            ))}
          </select>
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Teléfono:</label>
          <input
            type="tel"
            name="phone"
            value={formData.phone}
            onChange={handleChange}
            required
            className="input input-bordered w-full"
          />
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Foto:</label>
          <input
            type="file"
            name="photo"
            accept="image/*"
            onChange={handlePhotoChange}
            required
            className="file-input file-input-bordered w-full"
          />
        </div>
        <button type="submit" className="btn btn-primary w-full">Enviar</button>
      </form>
    </div>
  );
};

export default Formulario;

Observa que el código anterior ahora:

  1. Importa DaisyUI CSS: import 'daisyui/dist/full.css'; asegurando que DaisyUI está incluido.
  2. Uso de Clases de DaisyUI: Se aplicaron clases de DaisyUI como input input-bordered, select select-bordered, y btn btn-primary para estilizar los componentes del formulario.
  3. Estructura del Formulario: La estructura del formulario incluye contenedores como div y form con clases de TailwindCSS y DaisyUI para un diseño limpio y responsive.

Con ello logramos una apariencia mucho más linda y agradable.

Puedes observar en el siguiente video, lo que hemos realizado y lo que explicaremos más adelante, con lo cual vas a reforzar los conocimientos.

Incorporando a react-hook-form

En el código anterior, la gestión de los valores la hicimos, usando a useState, no tenemos gestión de errores y tampoco tenemos validaciones. Podemos realizar esto de forma manual, sí, pero si nos aprovechamos de un paquete que ya realiza esto, y que además es altamente usado por la comunidad, es decir, está probado y es código que los desarrolladores ya conocemos, ¿mejor aún cierto?

Para instalar a react-hook-form, solo debemos hacer lo siguiente:

npm install react-hook-form

Luego incorporamos los hooks del paquete de esta forma:

const { register, handleSubmit, watch, formState: { errors } } = useForm<FormData>();
const photoFile = watch('photo');
  • register, nos permite indicarle al hook qué campos del formulario vamos a utilizar.
  • handleSubmit, es la función propia de react-hook-form que va a manejar los valores de los campos (esto antes lo haciamos nosotros manualmente), y luego podemos indicar un callback para llamar nuestro backend.
  • watch, nos permite colocar una referencia para saber en todo momento el valor de un campo, en nuestro caso observamos la foto porque debemos convertirla a base64.
  • formState, nos indica en que estado está el formulario, por enviar, enviando o enviado y nos permite obtener los errores que se produjeron al momento de validar los campos (sí! Validaciones 😃)

Y que tal agregar algunas validaciones, como por ejemplo que la contraseña sea a la confirmación, y que la contraseña además tenga 8 caracteres, un número y un carácter especial. Queremos adicionalmente que la foto venga en formato png o jpg. Todo esto es posible haciendo uso de react-hook-form.

Veamos algunos de los ajustes que debemos realizar para que nuestro formulario sea fácilmente administrable con react-hook-form:

<input
            type="email"
            {...register('email', { required: "Email es requerido" })}
            className="input input-bordered w-full"
          />
          {errors.email && <span className="text-red-500 text-sm">{errors.email.message}</span>}

Observa que agregamos el uso de register y colocamos una validación, que sea obligatorio el valor, luego si hay error lo mostramos en pantalla.

Ahora veamos un caso más complejo, la contraseña:

<input
            type="password"
            {...register('password', {
              required: "Clave es requerida",
              minLength: { value: 8, message: "La clave debe tener al menos 8 caracteres" },
              pattern: {
                value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
                message: "La clave debe contener al menos un número y un caracter especial"
              }
            })}
            className="input input-bordered w-full"
          />
          {errors.password && <span className="text-red-500 text-sm">{errors.password.message}</span>}

Se puede identificar que cuando usamos el register, podemos indicar si es obligatorio (required), el mensaje de error (message) y el tamaño del valor mínimo aceptable (minLength) y el patrón (pattern) del valor para obligar colocar un número y un carácter especial.

Con solo eso ya hemos hecho validaciones complejas de ese campo.

Veamos como queda el componente luego de todos los cambios realizados:

import React from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import 'daisyui/dist/full.css';

const countries = [
  "Argentina", "Bolivia", "Brasil", "Chile", "Colombia",
  "Ecuador", "Guyana", "Paraguay", "Perú", "Surinam",
  "Uruguay", "Venezuela"
];

interface FormData {
  email: string;
  password: string;
  confirmPassword: string;
  name: string;
  country: string;
  phone: string;
  photo: FileList;
}

const Formulario: React.FC = () => {
  const { register, handleSubmit, watch, formState: { errors } } = useForm<FormData>();
  const password = watch('password');
  const photoFile = watch('photo');

  const onSubmit: SubmitHandler<FormData> = async (data) => {
    // Convertir la foto a base64
    let base64Photo = '';
    const file = data.photo[0];
    if (file) {
      base64Photo = await toBase64(file);
    }

    // Crear el objeto con los datos del formulario
    const dataToSend = {
      email: data.email,
      password: data.password,
      confirmPassword: data.confirmPassword,
      name: data.name,
      country: data.country,
      phone: data.phone,
      photo: {
        base64: base64Photo,
        name: file.name,
        type: file.type,
        size: file.size
      }
    };

    // Enviar la solicitud POST a la API
    try {
      const response = await fetch('https://leonardojose.dev/api/usuarios', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(dataToSend)
      });

      if (!response.ok) {
        throw new Error('Error en la solicitud');
      }

      const result = await response.json();
      console.log('Form data submitted:', result);
    } catch (error) {
      console.error('Error:', error);
    }
  };

  const toBase64 = (file: File) => new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result as string);
    reader.onerror = (error) => reject(error);
  });

  return (
    <div className="App container mx-auto p-4">
      <form onSubmit={handleSubmit(onSubmit)} className="bg-white p-6 rounded shadow-md w-full max-w-lg mx-auto">
        <h2 className="text-2xl font-bold mb-4">Datos de Acceso</h2>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Email:</label>
          <input
            type="email"
            {...register('email', { required: "Email es requerido" })}
            className="input input-bordered w-full"
          />
          {errors.email && <span className="text-red-500 text-sm">{errors.email.message}</span>}
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Clave:</label>
          <input
            type="password"
            {...register('password', {
              required: "Clave es requerida",
              minLength: { value: 8, message: "La clave debe tener al menos 8 caracteres" },
              pattern: {
                value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/,
                message: "La clave debe contener al menos un número y un caracter especial"
              }
            })}
            className="input input-bordered w-full"
          />
          {errors.password && <span className="text-red-500 text-sm">{errors.password.message}</span>}
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Confirmar Clave:</label>
          <input
            type="password"
            {...register('confirmPassword', {
              required: "Confirmar clave es requerido",
              validate: value => value === password || "Las claves no coinciden"
            })}
            className="input input-bordered w-full"
          />
          {errors.confirmPassword && <span className="text-red-500 text-sm">{errors.confirmPassword.message}</span>}
        </div>
        <h2 className="text-2xl font-bold mb-4">Datos Personales</h2>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Nombre:</label>
          <input
            type="text"
            {...register('name', { required: "Nombre es requerido" })}
            className="input input-bordered w-full"
          />
          {errors.name && <span className="text-red-500 text-sm">{errors.name.message}</span>}
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">País:</label>
          <select
            {...register('country', { required: "País es requerido" })}
            className="select select-bordered w-full"
          >
            <option value="">Seleccione un país</option>
            {countries.map(country => (
              <option key={country} value={country}>{country}</option>
            ))}
          </select>
          {errors.country && <span className="text-red-500 text-sm">{errors.country.message}</span>}
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Teléfono:</label>
          <input
            type="tel"
            {...register('phone', {
              required: "Teléfono es requerido",
              pattern: {
                value: /^[0-9()-]+$/,
                message: "Solo se permiten números, paréntesis y guiones"
              }
            })}
            className="input input-bordered w-full"
          />
          {errors.phone && <span className="text-red-500 text-sm">{errors.phone.message}</span>}
        </div>
        <div className="mb-4">
          <label className="block text-sm font-medium mb-1">Foto:</label>
          <input
            type="file"
            {...register('photo', {
              required: "Foto es requerida",
              validate: {
                acceptedFormats: files => files[0] && ['image/jpeg', 'image/png'].includes(files[0]?.type) || "Solo se permiten fotos en formato PNG o JPG"
              }
            })}
            accept="image/*"
            className="file-input file-input-bordered w-full"
          />
          {errors.photo && <span className="text-red-500 text-sm">{errors.photo.message}</span>}
        </div>
        <button type="submit" className="btn btn-primary w-full">Enviar</button>
      </form>
    </div>
  );
};

export default Formulario;

En el código anterior hemos realizado lo siguiente:

  1. Validación del Email: required: "Email es requerido".
  2. Validación de la Clave:
  • Mínimo 8 caracteres.
  • Al menos un número y un carácter especial.

3. Validación de Confirmar Clave: Verificamos que coincida con la clave.

4. Validación del País: Obligatorio.

5. Validación del Teléfono: Obligatorio y solo números

6. Validación de la Foto:

  • Obligatorio.
  • Formatos permitidos: PNG o JPG.

En el video indicado anteriormente vas a poder visualizar en detalle cada paso realizado y podrás ver como evolucionamos en el uso de formulario en la aplicación que tomamos de ejemplo.

Para cualquier duda o comentario, me puedes enviar un mensaje en mis redes: