Appearance
Domain Field Wrappers — Patrón {Entidad}Field
Última actualización: 2026-03-02 Estado: Estándar arquitectónico Aplicabilidad: Campos de autocomplete ligados a entidades de dominio del sistema
Propósito
El patrón domain field wrapper encapsula la configuración repetitiva de ControlledAutoComplete cuando un campo de formulario siempre apunta a la misma entidad de negocio con el mismo endpoint, el mismo dataMapping y el mismo label canónico.
Problema que resuelve
Sin este patrón, cada formulario que necesita seleccionar una localidad replica inline:
- El endpoint (
"localidad?include=provincia") - El
dataMappingcon la función de formateo de label - El label por defecto (
"Localidad") - El tipo genérico interno (
Localidad)
Resultado: inconsistencias de label, diferencias en el formato de las opciones entre formularios, y código duplicado difícil de mantener.
Solución
Un componente wrapper específico del dominio (LocalidadField, ClienteField, etc.) que fija internamente todos esos detalles y expone solo las props variables por formulario: name, control, validaciones opcionales y estado inicial de edición.
Diferencia con core/components/inputs/
Los componentes en ts/core/components/inputs/ son inputs genéricos reutilizables en cualquier contexto, sin conocimiento del dominio. Ejemplos: PhoneInput, EmailInput, CurrencyInput.
Los domain field wrappers son específicos de una entidad de negocio y conocen su endpoint, su tipo y su formato de presentación. No pertenecen a core/ porque dependen de datos de un dominio concreto.
| Dimensión | core/components/inputs/ | Domain Field Wrapper |
|---|---|---|
| Reutilización | Cualquier módulo, cualquier campo | Campos que referencian esa entidad específica |
| Conocimiento de dominio | Ninguno | Fijo (endpoint, tipo, label canónico) |
| Configuración | Toda por props | Mínima (solo name, control, overrides) |
| Ubicación | ts/core/components/inputs/ | ts/{dominio}/{Entidad}/components/ |
Diferencia con core/components/form/
ts/core/components/form/ contiene infraestructura técnica de formularios: ControlledAutoComplete, ControlledSelect, FormActions, FormSection, etc. Son primitivos del sistema de forms, sin saber qué entidad de negocio representan.
Un domain field wrapper usa esa infraestructura y la concreta para una entidad: LocalidadField usa ControlledAutoComplete<Localidad> internamente pero ningún consumidor necesita saber ese detalle.
Convención de ubicación
ts/{dominio}/{Entidad}/components/{Entidad}Field.tsxEl wrapper vive dentro del módulo dueño de esa entidad, en su carpeta components/.
Ejemplos concretos:
ts/bases/Localidad/components/LocalidadField.tsx
ts/crm/Cliente/components/ClienteField.tsx
ts/stock/Articulo/components/ArticuloField.tsxEl dominio (bases, crm, stock) corresponde al directorio de módulo React. La entidad (Localidad, Cliente, Articulo) es el recurso de negocio en PascalCase.
Convención de exportación
El wrapper se exporta como named export desde su archivo y se re-exporta desde el barrel del módulo dueño (index.ts).
Los consumidores importan siempre desde el barrel, no desde la ruta interna:
ts
// Correcto: desde el barrel del módulo
import { LocalidadField } from '../../../../bases/Localidad/index.js';
// Incorrecto: ruta interna expuesta
import { LocalidadField } from '../../../../bases/Localidad/components/LocalidadField.js';El barrel de ts/bases/Localidad/index.ts contiene:
ts
export { LocalidadField, formatLocalidadLabel } from './components/LocalidadField.js';
export type { LocalidadFieldProps } from './components/LocalidadField.js';
export type { Localidad, RequestLocalidad } from './types/localidad.types.js';Comportamiento useDefault
LocalidadField tiene una prop useDefault (default: true) que carga automáticamente la localidad marcada como defecto=true en el sistema cuando el campo está vacío y no se proveyó un initialSelectedItem.
Cuándo se aplica el default
El default se aplica solo cuando se cumplen las cuatro condiciones simultáneamente:
useDefaultno fue explícitamente desactivado (useDefault !== false)- No se proveyó
initialSelectedItem(es decir, no es modo edición ni override explícito) - El campo está semánticamente vacío:
null,undefined, o{ id: 0 } - La query del default ya cargó (no es
undefined)
Cómo desactivarlo
Pasar useDefault={false} explícitamente, o pasar initialSelectedItem con cualquier valor (incluyendo null explícito):
tsx
// Desactivar el default (campo arranca vacío)
<LocalidadField name="localidad" control={control} useDefault={false} />
// Modo edición: el initialSelectedItem impide que el default sobreescriba
<LocalidadField
name="localidad"
control={control}
initialSelectedItem={localidadExistente ?? null}
/>Cuando el usuario selecciona una opción, el default no se re-aplica aunque el usuario luego limpie el campo durante la misma sesión.
Ejemplo de uso completo
Caso 1: Uso mínimo (alta de nuevo registro)
tsx
import { LocalidadField } from '../../../../bases/Localidad/index.js';
<LocalidadField
name="localidad"
control={control}
/>El campo arrancará con la localidad por defecto preseleccionada. El label será "Localidad" y el placeholder "Buscar localidad...".
Caso 2: Con override de label
tsx
<LocalidadField
name="localidad"
control={control}
label="Ciudad de origen"
/>Caso 3: Modo edición (registro existente)
tsx
<LocalidadField
name="localidad"
control={control}
initialSelectedItem={contacto.localidad ?? null}
required
/>Pasar initialSelectedItem desactiva el comportamiento useDefault automáticamente, porque el campo ya tiene un valor inicial proveniente del backend.
Caso 4: Desactivar el default explícitamente
tsx
// Campo opcional, sin preselección
<LocalidadField
name="localidadAlternativa"
control={control}
useDefault={false}
/>Cuándo NO usar el wrapper
El wrapper no es adecuado cuando el formulario necesita un comportamiento significativamente diferente al estándar:
- Endpoint diferente: Si el campo busca localidades con filtros adicionales propios del contexto (ej: solo localidades de una provincia específica), el wrapper fijaría el endpoint incorrecto.
- Label con semántica distinta: Si el contexto requiere un label permanentemente diferente de
"Localidad"que no sea un simple override (ej: "Localidad de entrega" con lógica de validación diferente). - Filtrado contextual: Si la búsqueda depende de otro campo del formulario en tiempo real.
En estos casos, usar ControlledAutoComplete directamente inline es la opción correcta.
Ejemplos válidos de uso inline en el sistema:
IndustrialFiltersPanel.tsx— usa localidades en un contexto de filtrado con parámetros dinámicos propios del panel industrial; el endpoint y la lógica no son los estándar de Localidad.EstadisticasGeneralesForm.tsx— similar, el campo de localidad tiene comportamiento diferente al del formulario de alta estándar.
Estos casos de uso inline no son anti-patrones: son los casos para los que el wrapper explícitamente no está pensado.
Cómo extender el patrón — crear un nuevo {Entidad}Field
Pasos para agregar un nuevo domain field wrapper:
1. Identificar si aplica el patrón
Antes de crear un wrapper, verificar que:
- El campo aparece en 2 o más formularios con la misma configuración
- El endpoint,
dataMappingy label canónico son estables (no varían por contexto)
Si es un uso único o muy contextual, el wrapper añade complejidad sin beneficio.
2. Crear el archivo en el módulo dueño
ts/{dominio}/{Entidad}/components/{Entidad}Field.tsxEstructura interna del archivo:
- Función de formateo (
format{Entidad}Label): función pura exportada que produce el label canónico de un item. - Interface de props (
{Entidad}FieldProps): extiende las props deControlledAutoCompleteque el consumidor puede sobreescribir, excluyendoendpointydataMapping(que se fijan internamente). - Componente (
{Entidad}Field): named export que usaControlledAutoComplete<{Entidad}, TFieldValues, TName>con el endpoint y el dataMapping fijados.
Las props mínimas a exponer: name, control, rules?, initialSelectedItem?, disabled?, required?, placeholder?, label?.
Si la entidad tiene un concepto de "registro por defecto" (como defecto=true en Localidad), considerar agregar la prop useDefault con el mismo comportamiento.
3. Agregar al barrel del módulo
En ts/{dominio}/{Entidad}/index.ts, agregar:
ts
export { {Entidad}Field, format{Entidad}Label } from './components/{Entidad}Field.js';
export type { {Entidad}FieldProps } from './components/{Entidad}Field.js';4. Crear tests
Crear ts/{dominio}/{Entidad}/components/{Entidad}Field.test.tsx con:
- Tests unitarios de la función de formateo (función pura, sin React)
- Tests de renderizado con mock de
ControlledAutoCompleteque verifiquen las props pasadas (label, required, disabled)
Ver ts/bases/Localidad/components/LocalidadField.test.tsx como referencia.
5. Migrar los formularios existentes
Reemplazar los bloques ControlledAutoComplete inline en los formularios identificados en el paso 1. Migrar de a un formulario por vez y verificar manualmente antes de continuar.
Implementaciones existentes
| Entidad | Archivo | Barrel | Formularios consumidores |
|---|---|---|---|
Localidad | ts/bases/Localidad/components/LocalidadField.tsx | ts/bases/Localidad/index.ts | ContactoForm, ProveedorForm, MiembroForm/DatosBasicos |
Referencias
- Implementación base:
ts/bases/Localidad/components/LocalidadField.tsx - Barrel del módulo:
ts/bases/Localidad/index.ts - Tests:
ts/bases/Localidad/components/LocalidadField.test.tsx - Infraestructura subyacente:
ts/core/components/form/ControlledAutoComplete.tsx - Documentación AutoComplete: Components/Autocomplete
- Estructura de módulos: Estructura de Módulos React