SmartOrder - Formularios & Validación
Esta skill te guía para crear y mantener formularios siguiendo los patrones establecidos en SmartOrder-Glassmorphism.
Patrón de Arquitectura de Formularios
1. Estructura de Directorios
Para formularios complejos, sigue esta estructura modular:
code
ComponenteFormulario/
├── index.tsx # Componente principal (orchestrator)
├── components/ # Sub-componentes por sección
│ ├── SeccionDatosBasicos.tsx
│ ├── SeccionDireccion.tsx
│ └── SeccionPercepciones.tsx
├── hooks/ # Hooks personalizados
│ ├── index.tsx # Re-exports
│ ├── useFormularioForm.tsx # Hook principal del form
│ ├── useSeccionEspecifica.tsx
│ └── useValidaciones.tsx
├── constants/ # Catálogos y constantes
│ └── catalogos.tsx
└── utils/ # Utilidades puras
└── validaciones.js # Funciones de validación puras
Ejemplos de referencia en el proyecto:
2. Componente Principal (index.tsx)
Responsabilidades:
- •Orquestar sub-componentes
- •Manejar submit del formulario
- •Gestionar estado global del form
- •Integrar con servicios
Template base:
typescript
import { useState } from 'react'
import { Box, VStack, Button, useToast } from '@chakra-ui/react'
import { useFormulario } from './hooks/useFormularioForm'
import SeccionDatosBasicos from './components/SeccionDatosBasicos'
import SeccionDireccion from './components/SeccionDireccion'
import { validarFormulario } from './utils/validaciones'
interface FormularioProps {
initialData?: any
onSubmit: (data: any) => Promise<void>
onCancel?: () => void
}
export function Formulario({ initialData, onSubmit, onCancel }: FormularioProps) {
const toast = useToast()
const {
formData,
updateField,
updateSection,
isValid,
errors,
reset
} = useFormulario(initialData)
const handleSubmit = async () => {
const validationErrors = validarFormulario(formData)
if (validationErrors.length > 0) {
toast({
title: 'Errores de validación',
description: validationErrors.join(', '),
status: 'error',
duration: 4000
})
return
}
try {
await onSubmit(formData)
toast({
title: 'Guardado exitoso',
status: 'success',
duration: 3000
})
} catch (error) {
toast({
title: 'Error al guardar',
description: error.message,
status: 'error',
duration: 5000
})
}
}
return (
<Box>
<VStack spacing={6} align="stretch">
<SeccionDatosBasicos
data={formData.datosBasicos}
onChange={(data) => updateSection('datosBasicos', data)}
errors={errors.datosBasicos}
/>
<SeccionDireccion
data={formData.direccion}
onChange={(data) => updateSection('direccion', data)}
errors={errors.direccion}
/>
<HStack justify="flex-end" spacing={4}>
{onCancel && (
<Button variant="ghost" onClick={onCancel}>
Cancelar
</Button>
)}
<Button
colorScheme="brand"
onClick={handleSubmit}
isDisabled={!isValid}
>
Guardar
</Button>
</HStack>
</VStack>
</Box>
)
}
3. Sub-componentes de Sección
Responsabilidades:
- •Renderizar campos de una sección específica
- •Recibir datos y onChange desde el padre
- •No manejar estado propio (controlled components)
- •Mostrar errores de validación
Template base:
typescript
import { Box, FormControl, FormLabel, Input, FormErrorMessage } from '@chakra-ui/react'
interface SeccionProps {
data: {
campo1: string
campo2: string
}
onChange: (data: any) => void
errors?: Record<string, string>
}
export function SeccionDatosBasicos({ data, onChange, errors = {} }: SeccionProps) {
const handleChange = (field: string, value: any) => {
onChange({ ...data, [field]: value })
}
return (
<Box>
<FormControl isInvalid={!!errors.campo1}>
<FormLabel>Campo 1</FormLabel>
<Input
value={data.campo1}
onChange={(e) => handleChange('campo1', e.target.value)}
placeholder="Ingrese campo 1"
/>
{errors.campo1 && <FormErrorMessage>{errors.campo1}</FormErrorMessage>}
</FormControl>
<FormControl isInvalid={!!errors.campo2} mt={4}>
<FormLabel>Campo 2</FormLabel>
<Input
value={data.campo2}
onChange={(e) => handleChange('campo2', e.target.value)}
placeholder="Ingrese campo 2"
/>
{errors.campo2 && <FormErrorMessage>{errors.campo2}</FormErrorMessage>}
</FormControl>
</Box>
)
}
export default SeccionDatosBasicos
4. Hook Principal del Formulario
Responsabilidades:
- •Gestionar estado completo del formulario
- •Proveer funciones para actualizar datos
- •Validación en tiempo real
- •Estado de carga/guardado
Template base:
typescript
import { useState, useCallback, useMemo } from 'react'
interface FormState {
datosBasicos: {
campo1: string
campo2: string
}
direccion: {
calle: string
ciudad: string
}
}
const initialState: FormState = {
datosBasicos: {
campo1: '',
campo2: ''
},
direccion: {
calle: '',
ciudad: ''
}
}
export function useFormulario(initialData?: Partial<FormState>) {
const [formData, setFormData] = useState<FormState>({
...initialState,
...initialData
})
const [errors, setErrors] = useState<Record<string, any>>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const updateField = useCallback((section: string, field: string, value: any) => {
setFormData(prev => ({
...prev,
[section]: {
...prev[section],
[field]: value
}
}))
}, [])
const updateSection = useCallback((section: string, data: any) => {
setFormData(prev => ({
...prev,
[section]: data
}))
}, [])
const reset = useCallback(() => {
setFormData(initialState)
setErrors({})
}, [])
// Validación simple
const isValid = useMemo(() => {
return formData.datosBasicos.campo1.length > 0 &&
formData.datosBasicos.campo2.length > 0
}, [formData])
return {
formData,
updateField,
updateSection,
errors,
setErrors,
isValid,
isSubmitting,
setIsSubmitting,
reset
}
}
5. Hooks Especializados
Para lógica específica de una sección (ej: cargar provincias, tipos de documento):
typescript
import { useState, useEffect } from 'react'
import cotizadorService from '@services/cotizadorService'
export function useProvincias() {
const [provincias, setProvincias] = useState([])
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
const cargarProvincias = async () => {
setIsLoading(true)
try {
const data = await cotizadorService.obtenerProvincias()
setProvincias(data)
} catch (error) {
console.error('Error cargando provincias:', error)
} finally {
setIsLoading(false)
}
}
cargarProvincias()
}, [])
const buscarPorCodigo = (codigo: string) => {
return provincias.find(p => p.codigo === codigo)
}
return { provincias, isLoading, buscarPorCodigo }
}
6. Validaciones (utils/validaciones.js)
Principios:
- •Funciones puras (sin side effects)
- •Retornan array de errores o booleano
- •Reutilizables entre componentes
- •Sin dependencias de React
javascript
/**
* Valida que un CUIT sea válido
*/
export function validarCuit(cuit) {
if (!cuit) return false
const cuitLimpio = cuit.replace(/[-\s]/g, '')
if (cuitLimpio.length !== 11) return false
// Algoritmo de validación de CUIT
const [checkDigit, ...rest] = cuitLimpio.split('').map(Number).reverse()
const total = rest.reduce(
(acc, cur, index) => acc + cur * (2 + (index % 6)),
0
)
const mod11 = 11 - (total % 11)
return checkDigit === (mod11 === 11 ? 0 : mod11)
}
/**
* Valida campos requeridos de un formulario
*/
export function validarCamposRequeridos(data, camposRequeridos) {
const errores = []
camposRequeridos.forEach(campo => {
if (!data[campo] || data[campo].toString().trim() === '') {
errores.push(`El campo ${campo} es requerido`)
}
})
return errores
}
/**
* Valida email
*/
export function validarEmail(email) {
if (!email) return false
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return regex.test(email)
}
7. Constantes y Catálogos
Para opciones de selects, tipos de documento, etc:
typescript
// constants/catalogos.tsx
export const TIPOS_DOCUMENTO = [
{ codigo: 'DNI', descripcion: 'DNI' },
{ codigo: 'CUIT', descripcion: 'CUIT' },
{ codigo: 'CUIL', descripcion: 'CUIL' },
{ codigo: 'LE', descripcion: 'Libreta de Enrolamiento' },
{ codigo: 'LC', descripcion: 'Libreta Cívica' },
{ codigo: 'PAS', descripcion: 'Pasaporte' }
]
export const CONDICIONES_IVA = [
{ codigo: 'RI', descripcion: 'Responsable Inscripto' },
{ codigo: 'MT', descripcion: 'Monotributista' },
{ codigo: 'EX', descripcion: 'Exento' },
{ codigo: 'CF', descripcion: 'Consumidor Final' }
]
Patrones de Chakra UI para Formularios
FormControl con validación
typescript
<FormControl isRequired isInvalid={!!errors.campo}>
<FormLabel>Nombre del Campo</FormLabel>
<Input
value={value}
onChange={handleChange}
placeholder="Placeholder"
/>
{errors.campo && (
<FormErrorMessage>{errors.campo}</FormErrorMessage>
)}
<FormHelperText>Texto de ayuda opcional</FormHelperText>
</FormControl>
Select con opciones
typescript
<FormControl>
<FormLabel>Provincia</FormLabel>
<Select
value={value}
onChange={(e) => handleChange(e.target.value)}
placeholder="Seleccione una provincia"
>
{provincias.map(prov => (
<option key={prov.codigo} value={prov.codigo}>
{prov.descripcion}
</option>
))}
</Select>
</FormControl>
Textarea
typescript
<FormControl>
<FormLabel>Observaciones</FormLabel>
<Textarea
value={value}
onChange={handleChange}
placeholder="Ingrese observaciones"
rows={4}
/>
</FormControl>
Checkbox
typescript
<Checkbox
isChecked={value}
onChange={(e) => handleChange(e.target.checked)}
>
Aceptar términos y condiciones
</Checkbox>
DatePicker (react-datepicker)
typescript
import DatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'
<FormControl>
<FormLabel>Fecha</FormLabel>
<DatePicker
selected={fecha}
onChange={(date) => setFecha(date)}
dateFormat="dd/MM/yyyy"
customInput={<Input />}
/>
</FormControl>
Integración con Servicios
Crear registro
typescript
const handleSubmit = async (data: FormData) => {
try {
const resultado = await cotizadorService.crearClienteOcasional(data)
toast({
title: 'Cliente creado exitosamente',
status: 'success'
})
onSuccess?.(resultado)
} catch (error) {
toast({
title: 'Error al crear cliente',
description: error.message,
status: 'error'
})
}
}
Actualizar registro
typescript
const handleUpdate = async (id: string, data: FormData) => {
try {
await cotizadorService.actualizarClienteOcasional(id, data)
toast({
title: 'Cliente actualizado',
status: 'success'
})
} catch (error) {
toast({
title: 'Error al actualizar',
description: error.message,
status: 'error'
})
}
}
Checklist para Crear un Nuevo Formulario
- • Crear estructura de carpetas (index, components, hooks, utils, constants)
- • Definir tipos/interfaces para el form data
- • Crear hook principal del formulario (useXxxForm)
- • Implementar funciones de validación puras en utils
- • Crear sub-componentes por sección
- • Crear hooks especializados si es necesario (ej: useProvincias)
- • Implementar componente principal (orchestrator)
- • Integrar con servicios (crear/actualizar)
- • Agregar toasts para feedback
- • Manejar estados de carga/error
- • Testear validaciones edge cases
Convenciones de Naming
- •Componente principal:
FormularioXxxo simplemente el nombre del feature - •Sub-componentes:
Seccion<Nombre>(ej:SeccionDatosBasicos) - •Hook principal:
use<Nombre>Form(ej:useClienteForm) - •Hooks especializados:
use<Recurso>(ej:useProvincias,useTiposDocumento) - •Validaciones:
validar<Campo>ovalidar<Entidad>(ej:validarCuit,validarFormulario)
Referencias de Código
Para más detalles, revisa estos archivos existentes: