Forms - Vista General
Paquete: @tiendana/ngx-core/forms
El módulo @tiendana/ngx-core/forms proporciona un ecosistema completo para trabajar con formularios reactivos en Angular. Está diseñado para simplificar el desarrollo, mejorar la experiencia del usuario y mantener la consistencia visual con el Sistema de Diseño Tiendana.
Características Principales
- Sistema de wrapper inteligente:
TdnFormElementaplica automáticamente clases CSS según el estado del control interno - Manejo de errores declarativo: Sistema de directivas para mostrar mensajes de error de forma limpia y eficiente
- Seguimiento de estado reactivo: Signals para rastrear el estado de controles en tiempo real (focus, dirty, valid, etc.)
- Componentes especializados: Inputs personalizados para casos de uso específicos (tags, select, date, time, etc.)
- Validación en tiempo real: Integración perfecta con Angular Reactive Forms
- Transformación de entrada: Directivas para formatear y validar datos mientras el usuario escribe
- Accesibilidad:
tdnControlcableaaria-describedbyyaria-invalidcon el bloque de errores visible, de modo que un campo se anuncia inválido solo cuando el usuario puede ver el motivo - Revelado de errores al enviar:
FormRevealServicelleva al usuario al primer campo inválido en vez de deshabilitar el botón - Estilos integrados: Diseño consistente con
@tiendana/web-foundation
¿Qué incluye?
El módulo proporciona cuatro categorías principales de herramientas:
- Componente Wrapper:
TdnFormElement- Contenedor principal que gestiona estilos y estados - Directivas de Estado y Validación:
tdnControl,tdnErrors,tdnError- Control reactivo y mensajes de error - Directivas de Transformación:
inputLowerCase,inputRegex- Transformación de entrada en tiempo real - Servicio y utilidades:
FormRevealServicepara revelar el primer error al enviar, y los helpers de depuracióngetFormGroupErrors/getFormArrayErrors
Jerarquía de Uso
Orden de implementación:
- TdnFormElement envuelve todo (aplica clases CSS según estado)
- Input + tdnControl dentro del wrapper (rastrea estado con signals)
[tdnErrors]con sustdnErrordespués del input (muestra mensajes de validación y describe el control para lectores de pantalla)- Directivas opcionales se combinan con tdnControl (transforman entrada)
Todos estos elementos trabajan en conjunto para crear formularios reactivos, accesibles y con estilos consistentes.
Inicio Rápido
Ejemplo Básico: Input con Validación
La estructura básica siempre sigue esta jerarquía: TdnFormElement → Input con tdnControl → tdnErrors
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import {
TdnFormElement, // 1. Wrapper (contenedor)
TdnControlDirective, // 2. Estado del input
TdnErrorsDirective, // 3. Manejo de errores
TdnErrorDirective
} from '@tiendana/ngx-core/forms';
@Component({
selector: 'app-example',
imports: [
ReactiveFormsModule,
TdnErrorsDirective,
TdnErrorDirective,
TdnControlDirective,
TdnFormElement
],
template: `
<form [formGroup]="form">
<!-- 1. TdnFormElement: Contenedor principal -->
<tdn-form-element>
<label for="name">Nombre</label>
<!-- 2. tdnControl: Seguimiento de estado -->
<input id="name" tdnControl formControlName="name" maxlength="100" />
<!-- 3. [tdnErrors]: nombre del control; cada tdnError es un atributo -->
<div [tdnErrors]="'name'">
<span tdnError="required" when="touched">
El nombre es obligatorio
</span>
<span tdnError="minlength" when="touched">
Mínimo 3 caracteres
</span>
</div>
</tdn-form-element>
</form>
`
})
export class ExampleComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]]
});
}
tdnError es una directiva de atributo que alterna la propiedad hidden del elemento: el nodo siempre está en el DOM. Por eso se escribe <span tdnError="required" when="touched"> y nunca *tdnError, que no renderiza nada. El bloque [tdnErrors] recibe el nombre del control (o una ruta con puntos, como 'address.street', resuelta sobre el formulario raíz). Más detalle en Manejo de Errores.
Ejemplo con Select Personalizado
<tdn-form-element>
<label>Categoría</label>
<tdn-select
tdnControl
formControlName="category"
[data]="categories"
simple
labelKey="name"
/>
<div [tdnErrors]="'category'">
<span tdnError="required" when="touched">
Selecciona una categoría
</span>
</div>
</tdn-form-element>
Ejemplo con Input Group
<tdn-form-element>
<label>Cantidad</label>
<div class="input-group">
<input type="number" tdnControl formControlName="quantity">
<div class="addon">unidades</div>
</div>
<div [tdnErrors]="'quantity'">
<span tdnError="required" when="touched">
La cantidad es obligatoria
</span>
</div>
</tdn-form-element>
Estados CSS Automáticos
El TdnFormElement aplica automáticamente clases CSS basadas en el estado del control interno:
Ver tabla completa de estados CSS
| Estado | Clase CSS | Cuándo se aplica |
|---|---|---|
| Focus | .tdn-form-element--focus | El input tiene el foco |
| Active | .tdn-form-element--active | El input tiene valor (hasValue) |
| Dirty | .tdn-form-element--dirty | El valor ha sido modificado (o se marcó con markAsDirty()) |
| Touched | .tdn-form-element--touched | El input ha sido tocado (o se marcó con markAsTouched() / markAllAsTouched()) |
| Valid | .tdn-form-element--valid | El control es válido |
| Invalid | .tdn-form-element--invalid | El control es inválido |
| Pending | .tdn-form-element--pending | Validación asíncrona en curso |
| Disabled | .tdn-form-element--disabled | El control está deshabilitado |
Las clases siguen el evento events del control, así que markAllAsTouched() o markAsDirty() (por ejemplo al enviar) repintan el campo de inmediato, sin esperar a la siguiente pulsación. Un tdn-form-element sigue el primer tdnControl que contiene.
El sistema de diseño pinta el borde de error con --invalid y --dirty a la vez, no con --touched. Por eso FormRevealService marca como dirty el control que revela.
Ejemplo de uso en SCSS:
.tdn-form-element {
&--focus {
label {
color: var(--color-forrest);
}
}
// El borde de error de web-foundation depende de --invalid junto con --dirty
&--invalid.tdn-form-element--dirty {
input {
border-color: var(--color-error);
}
}
&--pending {
position: relative;
&::after {
content: '';
// Mostrar spinner de carga
}
}
}
Desplegables fuera del formulario
La lista de tdn-select y el calendario de tdn-input-date no se quedan dentro del campo: al abrirse se mueven a document.body, con posición fixed calculada desde el rectángulo del campo. Así ningún ancestro con transform, filter o contención (una animación de entrada que dejó su estado final, por ejemplo) puede recortarlos o desplazarlos. Esto tiene consecuencias de estilo y de eventos que conviene conocer una sola vez:
- Los estilos deben ser autónomos. Una regla como
.mis-filtros .tdn-select__dropdown-listno coincide, porque la lista ya no es descendiente de nada de tu página. Usa selectores que se sostengan solos:.tdn-select__dropdown-list,.tdn-select__dropdown-list--ontop,.tdn-select__dropdown-list--responsive,.tdn-input-date__dropdown, y las clasesontopyfullscreendel calendario. - Las propiedades CSS personalizadas no se heredan desde donde está el campo. La excepción es
--tdn-select-dropdown-width, que el select lee del host (o de un ancestro) y copia a la lista. settings.classessolo llega al host, no al panel abierto.- Los clics dentro del panel no se propagan: los listeners de
documentywindowno los ven. Un popover o drawer que se cierra con "clic fuera" sigue abierto mientras eliges una opción o escribes en la búsqueda. - La posición usa la ventana, no el documento: se abre debajo si hay espacio en
window.innerHeight, arriba si solo cabe ahí (tdn-select--ontopen el host ytdn-select__dropdown-list--ontopen la lista;ontopen el host y en el calendario detdn-input-date), y el calendario pasa afullscreencuando no cabe en ninguno de los dos lados.
La lista de tdn-select sale del body al cerrarse. El calendario de tdn-input-date se oculta con display: none pero permanece en el body hasta que el campo se destruye.
Detalle de cada componente: TdnSelect y TdnInputDate.
Mejores Prácticas
✅ Recomendado
- Siempre usar
TdnFormElementcomo wrapper para mantener consistencia visual - Aplicar
tdnControla todos los inputs para habilitar seguimiento de estado y el cableado ARIA - Usar
whenen errores para evitar mostrar mensajes prematuramente - Combinar directivas (
inputLowerCase,inputRegex) para mejor UX - Mantener el botón de envío habilitado y llamar a
FormRevealService.revealFirstInvalidcomo primera línea del manejador: un botón deshabilitado oculta por qué el usuario no puede avanzar
❌ Evitar
- No anidar
TdnFormElementdentro de otro - No poner
[tdnErrors]fuera deltdn-form-elementde su control: sus mensajes se muestran, pero el control no queda descrito por el bloque (sinaria-describedby) - No usar
*tdnError: es una directiva de atributo, la forma estructural no renderiza nada - No mezclar validación imperativa con el sistema declarativo
- No olvidar
formControlNameen inputs personalizados - No duplicar validación entre TypeScript y template
Componentes y Directivas
El módulo está organizado en las siguientes categorías:
Orden de Uso Recomendado
Los componentes y directivas deben usarse en este orden jerárquico:
| # | Elemento | Descripción | Uso |
|---|---|---|---|
| 1 | TdnFormElement | Wrapper reutilizable para inputs con label, errores y estilos consistentes | Contenedor principal - Siempre primero |
| 2 | tdnControl | Seguimiento reactivo del estado de controles con signals (focus, dirty, valid, etc.) | Aplica en el input/select dentro del wrapper |
| 3 | tdnErrors / tdnError | Sistema declarativo para mostrar mensajes de error en formularios reactivos | Dentro del wrapper, después del input |
| 4 | inputLowerCase | Convierte automáticamente el texto a minúsculas | Opcional: combina con tdnControl en el input |
| 5 | inputRegex | Filtra la entrada según un patrón regex (solo números, letras, etc.) | Opcional: combina con tdnControl en el input |
| 6 | FormRevealService | Revela el primer campo inválido al enviar: scroll, foco y sacudida | En el manejador de envío del componente |
| 7 | getFormGroupErrors / getFormArrayErrors | Helpers de depuración que registran en console.debug los errores de un FormGroup o FormArray | Solo durante el desarrollo |
Componentes de Input Personalizados
Los inputs personalizados están documentados en su propia subsección. Todos implementan ControlValueAccessor y se integran perfectamente con TdnFormElement, tdnControl y el sistema de validación.
| Componente | Descripción |
|---|---|
| TdnInputDate | Selector de fecha con calendario desplegable y soporte para rangos |
| TdnInputFile | Carga de archivos con drag & drop y vista previa |
| TdnInputImage | Carga de imágenes con vista previa y compresión automática |
| TdnInputPin | Entrada para códigos PIN/OTP con auto-focus entre campos |
| TdnInputTag | Entrada de etiquetas con validación por regex, máximo y duplicados |
| TdnInputTime | Selector de rango horario (desde → hasta), mostrado en 12 h con AM/PM |
| TdnSelect | Select con búsqueda, multi-selección, agrupación y paginación remota con scroll infinito |
| TdnWeeklyScheduleSelector | Selector visual de horarios semanales para citas/disponibilidad |
Validadores Personalizados
El módulo @tiendana/ngx-core/validators proporciona validadores síncronos y asíncronos para casos de uso comunes:
| Validador | Descripción |
|---|---|
| CustomValidators | Validaciones de comparación, archivos, fechas, remotas y NIT colombiano |
| Array Validators | Validación de longitud de arrays (minLengthArray, maxLengthArray) |
| File Read Utilities | Utilidades RxJS para lectura de archivos y conversión de formatos |
Ver la subsección Validadores para documentación completa.