Skip to content

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 dataMapping con 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óncore/components/inputs/Domain Field Wrapper
ReutilizaciónCualquier módulo, cualquier campoCampos que referencian esa entidad específica
Conocimiento de dominioNingunoFijo (endpoint, tipo, label canónico)
ConfiguraciónToda por propsMínima (solo name, control, overrides)
Ubicaciónts/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.tsx

El 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.tsx

El 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:

  1. useDefault no fue explícitamente desactivado (useDefault !== false)
  2. No se proveyó initialSelectedItem (es decir, no es modo edición ni override explícito)
  3. El campo está semánticamente vacío: null, undefined, o { id: 0 }
  4. 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, dataMapping y 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.tsx

Estructura 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 de ControlledAutoComplete que el consumidor puede sobreescribir, excluyendo endpoint y dataMapping (que se fijan internamente).
  • Componente ({Entidad}Field): named export que usa ControlledAutoComplete<{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 ControlledAutoComplete que 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

EntidadArchivoBarrelFormularios consumidores
Localidadts/bases/Localidad/components/LocalidadField.tsxts/bases/Localidad/index.tsContactoForm, 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