Saltar al contenido principal
Logo de JSCamp

Ejercicios: Formularios y Eventos

Practica implementando filtros, eventos onFocus/onBlur y gestión de formularios en tiempo real

midudev

Más clases y directos en YouTube

Suscribirse

Ejercicios: Formularios y Eventos

En esta clase vamos a practicar todo lo que hemos aprendido sobre gestión de formularios, filtros y eventos en React. Estos ejercicios te ayudarán a consolidar los conceptos de la clase anterior y a mejorar tu aplicación DevJobs.

Objetivo de los ejercicios

Los ejercicios de esta clase se centran en mejorar la experiencia de usuario del formulario de búsqueda:

  1. Implementar el resto de filtros - Añadir filtros adicionales que aún no hemos implementado
  2. Usar onFocus y onBlur - Mejorar la UX con feedback visual al interactuar con los inputs
  3. Pasar eventos a tiempo real o usar submit - Decidir qué filtros se aplican instantáneamente y cuáles al hacer submit

Ejercicio 1: Implementar el resto de filtros

En la clase anterior implementamos filtros básicos, pero aún nos faltan algunos. Vamos a completar todos los filtros disponibles en el formulario.

Filtros a implementar

Si tu formulario tiene estos campos pero no están funcionando todavía, es hora de implementarlos:

1. Filtro por tecnología

// src/App.jsxconst jobsFilteredByFilters = jobsData.filter((job) => {  return (    (filters.technology === '' || job.data.technology === filters.technology) &&    (filters.location === '' || job.data.modalidad === filters.location) &&    (filters.experienceLevel === '' || job.data.nivel === filters.experienceLevel)  )})

Si aún no lo tienes implementado:

  • Añade un select con las tecnologías disponibles (React, Node, Python, etc.)
  • Asegúrate de que el campo tenga un name con useId()
  • Captura el valor en handleSubmit con FormData
  • Añade la tecnología al estado filters

2. Filtro por salario

// src/components/SearchForm.jsxconst idSalary = useId()return (  <form className="search-form" onSubmit={handleSubmit}>    {/* ... otros campos ... */}    <div className="form-group">      <label htmlFor={idSalary}>Salario mínimo</label>      <input type="number" name={idSalary} id={idSalary} placeholder="30000" min="0" step="1000" />    </div>    <button type="submit">Buscar</button>  </form>)

En App.jsx, añade el filtro de salario:

const jobsFilteredByFilters = jobsData.filter((job) => {  // Convertir el salario del job a número (asumiendo que job.data.salary es un string)  const jobSalary = parseInt(job.data.salary) || 0  const minSalary = filters.salary ? parseInt(filters.salary) : 0  return (    (filters.technology === '' || job.data.technology === filters.technology) &&    (filters.location === '' || job.data.modalidad === filters.location) &&    (filters.experienceLevel === '' || job.data.nivel === filters.experienceLevel) &&    jobSalary >= minSalary  )})

3. Filtro por tipo de contrato

// src/components/SearchForm.jsxconst idContractType = useId()<div className="form-group">  <label htmlFor={idContractType}>Tipo de contrato</label>  <select name={idContractType} id={idContractType}>    <option value="">Todos</option>    <option value="full-time">Full Time</option>    <option value="part-time">Part Time</option>    <option value="freelance">Freelance</option>    <option value="internship">Prácticas</option>  </select></div>

Añade el filtro en App.jsx:

const jobsFilteredByFilters = jobsData.filter((job) => {  return (    // ... filtros anteriores ...    filters.contractType === '' || job.data.contractType === filters.contractType  )})

Checklist del Ejercicio 1

  • Todos los selects del formulario tienen su filtro correspondiente
  • Los filtros se capturan correctamente con FormData
  • El estado filters en App.jsx incluye todos los campos
  • La función jobsFilteredByFilters aplica todos los filtros correctamente
  • Los resultados se filtran correctamente cuando cambias cada select

Ejercicio 2: Usar onFocus y onBlur

Los eventos onFocus y onBlur mejoran la experiencia de usuario dando feedback visual cuando el usuario interactúa con los campos del formulario.

¿Qué son onFocus y onBlur?

<input  onFocus={() => console.log('El input está activo')}  onBlur={() => console.log('El input perdió el foco')}/>
  • onFocus - Se ejecuta cuando el usuario hace clic en el input (o usa Tab para llegar a él)
  • onBlur - Se ejecuta cuando el usuario sale del input (hace clic fuera o presiona Tab)

Implementación con estado local

Vamos a añadir un borde de color cuando un campo está activo:

// src/components/SearchForm.jsximport { useId, useState } from 'react'export function SearchForm({ onSearch, onChangeText }) {  const idText = useId()  const idTechnology = useId()  const idLocation = useId()  const idExperienceLevel = useId()  // Estado para saber qué campo está activo  const [focusedField, setFocusedField] = useState(null)  const handleSubmit = (event) => {    event.preventDefault()    const formData = new FormData(event.target)    const filters = {      search: formData.get(idText),      technology: formData.get(idTechnology),      location: formData.get(idLocation),      experienceLevel: formData.get(idExperienceLevel),    }    onSearch(filters)  }  const handleChangeText = (event) => {    onChangeText(event.target.value)  }  return (    <form className="search-form" onSubmit={handleSubmit}>      <div className="form-group">        <label htmlFor={idText}>Búsqueda</label>        <input          type="text"          name={idText}          id={idText}          placeholder="Buscar trabajos..."          onChange={handleChangeText}          onFocus={() => setFocusedField('search')}          onBlur={() => setFocusedField(null)}          style={{            borderColor: focusedField === 'search' ? '#4f46e5' : '#d1d5db',            outline: focusedField === 'search' ? '2px solid #4f46e5' : 'none',          }}        />        {focusedField === 'search' && (          <small className="input-hint">Busca por título de trabajo, empresa o tecnología</small>        )}      </div>      <div className="form-group">        <label htmlFor={idTechnology}>Tecnología</label>        <select          name={idTechnology}          id={idTechnology}          onFocus={() => setFocusedField('technology')}          onBlur={() => setFocusedField(null)}          style={{            borderColor: focusedField === 'technology' ? '#4f46e5' : '#d1d5db',          }}        >          <option value="">Todas</option>          <option value="react">React</option>          <option value="node">Node.js</option>          <option value="python">Python</option>        </select>      </div>      {/* ... resto de los campos con onFocus y onBlur ... */}      <button type="submit">Buscar</button>    </form>  )}

¿Cómo funciona?

Flujo del evento:

Usuario hace clic en el inputonFocus se ejecutasetFocusedField('search')El componente se re-renderizaEl input muestra el borde de colorUsuario hace clic fuera del inputonBlur se ejecutasetFocusedField(null)El componente se re-renderizaEl input vuelve a su estado normal

Mejorando con CSS en lugar de inline styles

En lugar de usar style inline, puedes usar clases CSS:

// src/components/SearchForm.jsx<input  type="text"  name={idText}  id={idText}  placeholder="Buscar trabajos..."  onChange={handleChangeText}  onFocus={() => setFocusedField('search')}  onBlur={() => setFocusedField(null)}  className={focusedField === 'search' ? 'input-focused' : ''}/>

Y en tu CSS:

/* src/styles/SearchForm.css */.form-group input,.form-group select {  border: 2px solid #d1d5db;  transition: border-color 0.2s ease;}.input-focused {  border-color: #4f46e5 !important;  outline: 2px solid #4f46e5;}.input-hint {  display: block;  margin-top: 0.25rem;  font-size: 0.875rem;  color: #6b7280;}

Checklist del Ejercicio 2

  • Los inputs tienen eventos onFocus y onBlur
  • Hay estado local para saber qué campo está activo
  • Los campos activos muestran feedback visual (borde de color, outline, etc.)
  • Opcionalmente: Muestra hints o mensajes de ayuda cuando un campo está activo
  • El feedback visual se elimina cuando el usuario sale del campo

Ejercicio 3: Pasar eventos a tiempo real o usar submit

En este ejercicio vas a decidir qué filtros se aplican en tiempo real y cuáles solo al hacer submit del formulario.

Estrategias de filtrado

Opción A: Todo en tiempo real (onChange)

// src/components/SearchForm.jsxexport function SearchForm({ onSearch }) {  const idText = useId()  const idTechnology = useId()  const idLocation = useId()  const handleChange = (event) => {    const formData = new FormData(event.target.form)    const filters = {      search: formData.get(idText),      technology: formData.get(idTechnology),      location: formData.get(idLocation),    }    onSearch(filters)  }  return (    <form className="search-form">      <input type="text" name={idText} id={idText} onChange={handleChange} />      <select name={idTechnology} id={idTechnology} onChange={handleChange}>        <option value="">Todas</option>        <option value="react">React</option>      </select>      <select name={idLocation} id={idLocation} onChange={handleChange}>        <option value="">Todas</option>        <option value="remoto">Remoto</option>      </select>      {/* No necesitas botón de submit */}    </form>  )}

Ventajas:

  • ✅ Resultados instantáneos
  • ✅ UX más fluida
  • ✅ No necesitas botón de submit

Desventajas:

  • ❌ Muchas re-renderizaciones
  • ❌ Puede ser lento con muchos datos
  • ❌ El usuario no puede “preparar” varios filtros antes de buscar

Opción B: Solo submit (onSubmit)

// src/components/SearchForm.jsxexport function SearchForm({ onSearch }) {  const idText = useId()  const idTechnology = useId()  const idLocation = useId()  const handleSubmit = (event) => {    event.preventDefault()    const formData = new FormData(event.target)    const filters = {      search: formData.get(idText),      technology: formData.get(idTechnology),      location: formData.get(idLocation),    }    onSearch(filters)  }  return (    <form className="search-form" onSubmit={handleSubmit}>      <input type="text" name={idText} id={idText} />      <select name={idTechnology} id={idTechnology}>        <option value="">Todas</option>        <option value="react">React</option>      </select>      <select name={idLocation} id={idLocation}>        <option value="">Todas</option>        <option value="remoto">Remoto</option>      </select>      <button type="submit">Buscar</button>    </form>  )}

Ventajas:

  • ✅ Mejor rendimiento
  • ✅ El usuario puede preparar varios filtros
  • ✅ Control sobre cuándo se ejecuta la búsqueda

Desventajas:

  • ❌ Menos inmediato
  • ❌ Requiere que el usuario haga clic en el botón

Opción C: Híbrido (Recomendado) ⭐

Búsqueda de texto en tiempo real + Selects con submit:

// src/components/SearchForm.jsxexport function SearchForm({ onSearch, onChangeText }) {  const idText = useId()  const idTechnology = useId()  const idLocation = useId()  const idExperienceLevel = useId()  const handleSubmit = (event) => {    event.preventDefault()    const formData = new FormData(event.target)    const filters = {      search: formData.get(idText),      technology: formData.get(idTechnology),      location: formData.get(idLocation),      experienceLevel: formData.get(idExperienceLevel),    }    onSearch(filters)  }  // Búsqueda de texto en tiempo real  const handleChangeText = (event) => {    onChangeText(event.target.value)  }  return (    <form className="search-form" onSubmit={handleSubmit}>      {/* Input de texto: tiempo real */}      <div className="form-group">        <label htmlFor={idText}>Búsqueda</label>        <input          type="text"          name={idText}          id={idText}          placeholder="Buscar trabajos..."          onChange={handleChangeText}        />      </div>      {/* Selects: solo con submit */}      <div className="form-group">        <label htmlFor={idTechnology}>Tecnología</label>        <select name={idTechnology} id={idTechnology}>          <option value="">Todas</option>          <option value="react">React</option>          <option value="node">Node.js</option>        </select>      </div>      <div className="form-group">        <label htmlFor={idLocation}>Ubicación</label>        <select name={idLocation} id={idLocation}>          <option value="">Todas</option>          <option value="remoto">Remoto</option>          <option value="cdmx">Ciudad de México</option>        </select>      </div>      <div className="form-group">        <label htmlFor={idExperienceLevel}>Nivel de experiencia</label>        <select name={idExperienceLevel} id={idExperienceLevel}>          <option value="">Todos</option>          <option value="junior">Junior</option>          <option value="mid">Mid-level</option>          <option value="senior">Senior</option>        </select>      </div>      <button type="submit">Aplicar filtros</button>    </form>  )}

¿Por qué es mejor el enfoque híbrido?

  • Input de texto en tiempo real - Para búsquedas rápidas, el usuario ve resultados mientras escribe
  • Selects con submit - Para filtros múltiples, el usuario puede ajustar varios antes de buscar
  • Mejor rendimiento - No re-renderizas todo cada vez que cambias un select
  • Mejor UX - Combina inmediatez con control

Implementación del enfoque híbrido en App.jsx

// src/App.jsxfunction App() {  const [filters, setFilters] = useState({    technology: '',    location: '',    experienceLevel: '',  })  const [textToFilter, setTextToFilter] = useState('')  const [currentPage, setCurrentPage] = useState(1)  // Maneja el submit del formulario (selects)  const handleSearch = (newFilters) => {    setFilters({      technology: newFilters.technology,      location: newFilters.location,      experienceLevel: newFilters.experienceLevel,    })    // También actualizamos el texto por si acaso    setTextToFilter(newFilters.search || '')    // Reiniciamos a la página 1 cuando cambian los filtros    setCurrentPage(1)  }  // Maneja el cambio de texto en tiempo real  const handleChangeText = (text) => {    setTextToFilter(text)    // Reiniciamos a la página 1 cuando cambia el texto    setCurrentPage(1)  }  // ... resto del código de filtrado y paginación  return (    <>      <Header />      <main>        <SearchForm onSearch={handleSearch} onChangeText={handleChangeText} />        <JobListings jobs={pagedResults} />        <Pagination          currentPage={currentPage}          totalPages={totalPages}          onPageChange={handlePageChange}        />      </main>      <Footer />    </>  )}

Extra: Resetear la página al cambiar filtros

Cuando el usuario cambia los filtros, es buena práctica volver a la página 1:

const handleSearch = (newFilters) => {  setFilters({    technology: newFilters.technology,    location: newFilters.location,    experienceLevel: newFilters.experienceLevel,  })  setTextToFilter(newFilters.search || '')  setCurrentPage(1) // ← Volver a la primera página}const handleChangeText = (text) => {  setTextToFilter(text)  setCurrentPage(1) // ← Volver a la primera página}

¿Por qué?

Imagina que estás en la página 5 mostrando trabajos de React. Si cambias el filtro a Python, puede que Python solo tenga 2 páginas de resultados. Sin el reset, seguirías en la página 5 que no existe para Python.

Checklist del Ejercicio 3

  • Has decidido qué filtros van en tiempo real y cuáles con submit
  • El input de búsqueda de texto funciona en tiempo real (onChange)
  • Los selects se aplican al hacer submit del formulario
  • Cuando cambias filtros, la página se resetea a 1
  • El botón de submit dice algo descriptivo como “Aplicar filtros” o “Buscar”
  • La UX es fluida y los filtros funcionan como esperas

Desafío extra: Botón de limpiar filtros

Añade un botón para limpiar todos los filtros y volver al estado inicial:

// src/components/SearchForm.jsxexport function SearchForm({ onSearch, onChangeText, onReset }) {  // ... useId's y handlers  const handleReset = () => {    // Resetear el formulario    document.querySelector('.search-form').reset()    // Notificar al padre    onReset()  }  return (    <form className="search-form" onSubmit={handleSubmit}>      {/* ... campos del formulario ... */}      <div className="form-actions">        <button type="submit" className="btn-primary">          Aplicar filtros        </button>        <button type="button" className="btn-secondary" onClick={handleReset}>          Limpiar filtros        </button>      </div>    </form>  )}

En App.jsx:

const handleReset = () => {  setFilters({    technology: '',    location: '',    experienceLevel: '',  })  setTextToFilter('')  setCurrentPage(1)}return <SearchForm onSearch={handleSearch} onChangeText={handleChangeText} onReset={handleReset} />

Desafío extra 2: Contador de resultados

Muestra cuántos trabajos se encontraron con los filtros actuales:

// src/App.jsxfunction App() {  // ... estados y handlers  const jobsFilteredByFilters = jobsData.filter((job) => {    /* ... */  })  const jobsWithTextFilter =    textToFilter === ''      ? jobsFilteredByFilters      : jobsFilteredByFilters.filter((job) => {          /* ... */        })  const totalResults = jobsWithTextFilter.length  return (    <>      <Header />      <main>        <SearchForm onSearch={handleSearch} onChangeText={handleChangeText} />        {/* Contador de resultados */}        <div className="results-summary">          <p>            Se encontraron <strong>{totalResults}</strong> trabajos            {textToFilter && ` para "${textToFilter}"`}          </p>        </div>        <JobListings jobs={pagedResults} />        <Pagination          currentPage={currentPage}          totalPages={totalPages}          onPageChange={handlePageChange}        />      </main>      <Footer />    </>  )}

Lo que has practicado en estos ejercicios

  • 🎯 Implementar filtros completos - Todos los campos del formulario filtran correctamente
  • 👁️ Eventos onFocus y onBlur - Feedback visual al interactuar con inputs
  • Estrategias de filtrado - Tiempo real vs submit, y enfoque híbrido
  • 🔄 Reset de página - Volver a página 1 al cambiar filtros
  • 🧹 Limpiar filtros - Botón para resetear todos los filtros
  • 📊 Contador de resultados - Mostrar cuántos trabajos se encontraron
  • 🎨 UX mejorada - Hints, feedback visual y mejor usabilidad

Comparación de enfoques

Característica Todo onChange Todo onSubmit Híbrido ⭐
Resultados instantáneos ✅ (solo texto)
Rendimiento
Control del usuario
Filtros múltiples
Mejor para búsqueda
Mejor para filtros

Recomendación: Usa el enfoque híbrido para la mejor experiencia de usuario.

Recap de conceptos clave

FormData

const formData = new FormData(event.target)const value = formData.get('field-name')

onFocus y onBlur

<input onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} />

onChange para tiempo real

<input onChange={(e) => handleChange(e.target.value)} />

onSubmit para formularios

<form onSubmit={(e) => {  e.preventDefault()  handleSubmit()}}>

Reset de formulario

// Con DOM APIdocument.querySelector('form').reset()// Con refformRef.current.reset()

Siguientes pasos

Ahora que dominas la gestión de formularios en React, en las próximas clases aprenderemos:

  • 🎣 Custom Hooks - Crear hooks reutilizables para lógica de formularios
  • 📝 Validación de formularios - Validar datos antes de enviarlos
  • 🚀 Optimización de rendimiento - useMemo y useCallback para formularios

💡 Recuerda: La experiencia de usuario es clave. Un formulario con búsqueda en tiempo real para texto y submit para filtros múltiples ofrece la mejor combinación de rapidez y control.