Saltar al contenido principal

TdnFormElement

Paquete: @tiendana/ngx-core/forms

TdnFormElement es un componente wrapper que aplica automáticamente clases CSS según el estado del control de formulario que contiene. Elimina los [class.error] y [class.focused] manuales y es además el punto donde cada control encuentra su bloque de errores para el cableado ARIA.

Características​

✅ Clases de estado automáticas (--focus, --active, --dirty, --valid, --invalid, --pending, --touched, --disabled)
✅ Se repinta con markAllAsTouched() y markAsDirty(), no solo al teclear
✅ Empareja cada tdnControl con su bloque [tdnErrors] por instancia de control
✅ Soporta varios controles en un mismo campo, cada uno con su bloque de errores
✅ Estilos de web-foundation incluidos para el borde de error, el estado pendiente y los mensajes

Selector​

tdn-form-element

Usa ViewEncapsulation.None y ChangeDetectionStrategy.OnPush, y proyecta su contenido tal cual.

Cómo Funciona​

  1. Busca el primer TdnControlDirective (tdnControl) dentro de él y refleja sus signals en clases del host. Las clases siguen al primer tdnControl; un campo puede tener varios controles, pero solo el primero pinta el wrapper.
  2. Reúne todos los bloques [tdnErrors] de su contenido (a cualquier profundidad). Cada tdnControl le pide el bloque que lo describe mediante errorsFor(control), y con él escribe aria-describedby y aria-invalid. El emparejamiento es por instancia de control, no por nombre. Ver tdnControl.

API​

Clases CSS aplicadas al host​

Clase CSSCondición
.tdn-form-elementSiempre aplicada (clase base)
.tdn-form-element--disabledControl deshabilitado
.tdn-form-element--focusControl con foco
.tdn-form-element--activeControl con valor (hasValue)
.tdn-form-element--dirtyValor modificado, o control marcado con markAsDirty()
.tdn-form-element--validControl válido
.tdn-form-element--invalidControl inválido
.tdn-form-element--pendingValidación asíncrona en proceso
.tdn-form-element--touchedControl tocado (blur, markAsTouched() o markAllAsTouched())
El borde de error necesita --dirty

El sistema de diseño pinta el borde y el color de la etiqueta de error con .tdn-form-element--invalid.tdn-form-element--dirty a la vez; --touched no basta. Si revelas errores al enviar, FormRevealService marca como dirty el control que revela justamente por esto.

Propiedades y métodos​

MiembroTipoDescripción
formElementSignal<TdnControlDirective | undefined>El primer tdnControl contenido; de él salen las clases
disabled, focus, active, dirty, valid, invalid, pending, touchedSignal<boolean>Estado del control interno; false si no hay tdnControl
errorsFor(control)(control: AbstractControl | null | undefined) => TdnErrorsDirective | nullBloque [tdnErrors] del campo que sigue ese control; null si no hay

Uso Básico​

import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { TdnFormElement, TdnControlDirective } from '@tiendana/ngx-core/forms';

@Component({
selector: 'app-my-form',
imports: [ReactiveFormsModule, TdnFormElement, TdnControlDirective],
template: `
<form [formGroup]="form">
<tdn-form-element>
<label for="email">Email</label>
<input
id="email"
type="email"
formControlName="email"
tdnControl
placeholder="tu@email.com"
>
</tdn-form-element>

<tdn-form-element>
<label for="password">Contraseña</label>
<input
id="password"
type="password"
formControlName="password"
tdnControl
>
</tdn-form-element>
</form>
`
})
export class MyFormComponent {
private readonly fb = inject(FormBuilder);

readonly form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
});
}

Estilos Personalizados​

Los estilos base vienen de @tiendana/web-foundation. Para ajustes propios apóyate en los modificadores; usa tokens del sistema de diseño, nunca colores fijos.

Ejemplo de estilos que responden al estado
.tdn-form-element {
// Estado de foco
&--focus {
label {
color: var(--color-forrest);
}
}

// Error: el sistema de diseño exige --invalid junto con --dirty
&--invalid.tdn-form-element--dirty {
input {
border-color: var(--color-error);
}
}

// Deshabilitado
&--disabled {
opacity: 0.6;
pointer-events: none;
}
}

Ejemplo Completo con Validación​

El botón de envío permanece habilitado: un botón deshabilitado con [disabled]="form.invalid" oculta por qué el usuario no puede avanzar. En su lugar, FormRevealService marca el formulario como tocado, lleva al usuario al primer campo con error y sus mensajes aparecen.

Ver ejemplo completo con validación asíncrona y revelado de errores
import { Component, ElementRef, inject } from '@angular/core';
import {
ReactiveFormsModule,
FormBuilder,
Validators,
AsyncValidatorFn,
AbstractControl
} from '@angular/forms';
import {
FormRevealService,
TdnFormElement,
TdnControlDirective,
TdnErrorsDirective,
TdnErrorDirective
} from '@tiendana/ngx-core/forms';
import { of, delay, map } from 'rxjs';

function emailExistsValidator(): AsyncValidatorFn {
return (control: AbstractControl) =>
of(control.value).pipe(
delay(1000),
map(email => (email === 'test@test.com' ? { emailExists: true } : null))
);
}

@Component({
selector: 'app-register-form',
imports: [
ReactiveFormsModule,
TdnFormElement,
TdnControlDirective,
TdnErrorsDirective,
TdnErrorDirective
],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<tdn-form-element>
<label for="email">Correo Electrónico</label>
<input id="email" type="email" formControlName="email" tdnControl placeholder="tu@email.com">
<div [tdnErrors]="'email'">
<span tdnError="required" when="touched">El email es obligatorio</span>
<span tdnError="email" when="touched">Ingresa un email válido</span>
<span tdnError="emailExists" when="touched">Este email ya está registrado</span>
</div>
</tdn-form-element>

<tdn-form-element>
<label for="name">Nombre Completo</label>
<input id="name" type="text" formControlName="name" tdnControl placeholder="Juan Pérez">
<div [tdnErrors]="'name'">
<span tdnError="required" when="touched">El nombre es obligatorio</span>
<span tdnError="minlength" when="dirty">Mínimo 3 caracteres</span>
</div>
</tdn-form-element>

<tdn-form-element>
<label for="password">Contraseña</label>
<input id="password" type="password" formControlName="password" tdnControl>
<div [tdnErrors]="'password'">
<span tdnError="required" when="touched">La contraseña es obligatoria</span>
<span tdnError="minlength" when="dirty">Mínimo 8 caracteres</span>
</div>
</tdn-form-element>

<button type="submit" class="bttn bttn--primary">Registrarse</button>
</form>
`
})
export class RegisterFormComponent {
private readonly fb = inject(FormBuilder);
private readonly reveal = inject(FormRevealService);
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);

readonly form = this.fb.group({
email: ['', {
validators: [Validators.required, Validators.email],
asyncValidators: [emailExistsValidator()]
}],
name: ['', [Validators.required, Validators.minLength(3)]],
password: ['', [Validators.required, Validators.minLength(8)]]
});

onSubmit(): void {
if (this.reveal.revealFirstInvalid(this.form, this.host.nativeElement)) return;

console.log('Form submitted:', this.form.value);
}
}

Uso con Componentes Personalizados​

TdnFormElement funciona con cualquier control que lleve tdnControl y un NgControl:

<tdn-form-element>
<label>Calificación</label>
<app-star-rating formControlName="rating" tdnControl />
</tdn-form-element>

Para que un componente propio reciba aria-describedby y aria-invalid automáticos, marca con data-tdn-aria-target el elemento interno que recibe el foco. Sin ese atributo hay que describirlo a mano; ver tdnControl.

Requisitos​

Dependencia requerida

TdnFormElement requiere que el control interno tenga la directiva tdnControl. Sin ella, las clases de estado no se aplican y el control no recibe el cableado ARIA.

<!-- ❌ Incorrecto: falta tdnControl -->
<tdn-form-element>
<input formControlName="name">
</tdn-form-element>

<!-- ✅ Correcto -->
<tdn-form-element>
<input formControlName="name" tdnControl>
</tdn-form-element>

Ventajas​

  1. Sin clases manuales: no escribes [class.error] ni [class.focused].
  2. Consistencia: todos los campos se comportan igual visualmente.
  3. Estilos centralizados: viven en @tiendana/web-foundation.
  4. Accesibilidad: el campo se anuncia inválido solo cuando hay un mensaje visible que lo explica.
  5. Rendimiento: usa signals y OnPush.

Buenas Prácticas​

  1. Usa siempre tdnControl en el control interno.

  2. Coloca el bloque [tdnErrors] dentro del mismo tdn-form-element: un bloque fuera del campo muestra sus mensajes, pero el control no queda descrito por él.

  3. No anides tdn-form-element dentro de otro.

  4. Mantén el envío habilitado y revela los errores con FormRevealService.

  5. Asocia cada label con su input (for / id) para que el nombre accesible del campo sea correcto.

Ver También​