Saltar al contenido principal

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: TdnFormElement aplica 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: tdnControl cablea aria-describedby y aria-invalid con 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: FormRevealService lleva 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:

  1. Componente Wrapper: TdnFormElement - Contenedor principal que gestiona estilos y estados
  2. Directivas de Estado y Validación: tdnControl, tdnErrors, tdnError - Control reactivo y mensajes de error
  3. Directivas de Transformación: inputLowerCase, inputRegex - Transformación de entrada en tiempo real
  4. Servicio y utilidades: FormRevealService para revelar el primer error al enviar, y los helpers de depuración getFormGroupErrors / getFormArrayErrors

Jerarquía de Uso​

Orden de implementación:

  1. TdnFormElement envuelve todo (aplica clases CSS según estado)
  2. Input + tdnControl dentro del wrapper (rastrea estado con signals)
  3. [tdnErrors] con sus tdnError después del input (muestra mensajes de validación y describe el control para lectores de pantalla)
  4. 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)]]
});
}
Cómo se escribe un error

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
EstadoClase CSSCuándo se aplica
Focus.tdn-form-element--focusEl input tiene el foco
Active.tdn-form-element--activeEl input tiene valor (hasValue)
Dirty.tdn-form-element--dirtyEl valor ha sido modificado (o se marcó con markAsDirty())
Touched.tdn-form-element--touchedEl input ha sido tocado (o se marcó con markAsTouched() / markAllAsTouched())
Valid.tdn-form-element--validEl control es válido
Invalid.tdn-form-element--invalidEl control es inválido
Pending.tdn-form-element--pendingValidación asíncrona en curso
Disabled.tdn-form-element--disabledEl 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-list no 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 clases ontop y fullscreen del 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.classes solo llega al host, no al panel abierto.
  • Los clics dentro del panel no se propagan: los listeners de document y window no 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--ontop en el host y tdn-select__dropdown-list--ontop en la lista; ontop en el host y en el calendario de tdn-input-date), y el calendario pasa a fullscreen cuando no cabe en ninguno de los dos lados.
Qué se mueve y cuándo vuelve

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​

  1. Siempre usar TdnFormElement como wrapper para mantener consistencia visual
  2. Aplicar tdnControl a todos los inputs para habilitar seguimiento de estado y el cableado ARIA
  3. Usar when en errores para evitar mostrar mensajes prematuramente
  4. Combinar directivas (inputLowerCase, inputRegex) para mejor UX
  5. Mantener el botón de envío habilitado y llamar a FormRevealService.revealFirstInvalid como primera línea del manejador: un botón deshabilitado oculta por qué el usuario no puede avanzar

❌ Evitar​

  1. No anidar TdnFormElement dentro de otro
  2. No poner [tdnErrors] fuera del tdn-form-element de su control: sus mensajes se muestran, pero el control no queda descrito por el bloque (sin aria-describedby)
  3. No usar *tdnError: es una directiva de atributo, la forma estructural no renderiza nada
  4. No mezclar validación imperativa con el sistema declarativo
  5. No olvidar formControlName en inputs personalizados
  6. 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:

#ElementoDescripciónUso
1TdnFormElementWrapper reutilizable para inputs con label, errores y estilos consistentesContenedor principal - Siempre primero
2tdnControlSeguimiento reactivo del estado de controles con signals (focus, dirty, valid, etc.)Aplica en el input/select dentro del wrapper
3tdnErrors / tdnErrorSistema declarativo para mostrar mensajes de error en formularios reactivosDentro del wrapper, después del input
4inputLowerCaseConvierte automáticamente el texto a minúsculasOpcional: combina con tdnControl en el input
5inputRegexFiltra la entrada según un patrón regex (solo números, letras, etc.)Opcional: combina con tdnControl en el input
6FormRevealServiceRevela el primer campo inválido al enviar: scroll, foco y sacudidaEn el manejador de envío del componente
7getFormGroupErrors / getFormArrayErrorsHelpers de depuración que registran en console.debug los errores de un FormGroup o FormArraySolo 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.

ComponenteDescripción
TdnInputDateSelector de fecha con calendario desplegable y soporte para rangos
TdnInputFileCarga de archivos con drag & drop y vista previa
TdnInputImageCarga de imágenes con vista previa y compresión automática
TdnInputPinEntrada para códigos PIN/OTP con auto-focus entre campos
TdnInputTagEntrada de etiquetas con validación por regex, máximo y duplicados
TdnInputTimeSelector de rango horario (desde → hasta), mostrado en 12 h con AM/PM
TdnSelectSelect con búsqueda, multi-selección, agrupación y paginación remota con scroll infinito
TdnWeeklyScheduleSelectorSelector 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:

ValidadorDescripción
CustomValidatorsValidaciones de comparación, archivos, fechas, remotas y NIT colombiano
Array ValidatorsValidación de longitud de arrays (minLengthArray, maxLengthArray)
File Read UtilitiesUtilidades RxJS para lectura de archivos y conversión de formatos

Ver la subsección Validadores para documentación completa.