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
- Busca el primer
TdnControlDirective(tdnControl) dentro de él y refleja sus signals en clases del host. Las clases siguen al primertdnControl; un campo puede tener varios controles, pero solo el primero pinta el wrapper. - Reúne todos los bloques
[tdnErrors]de su contenido (a cualquier profundidad). CadatdnControlle pide el bloque que lo describe medianteerrorsFor(control), y con él escribearia-describedbyyaria-invalid. El emparejamiento es por instancia de control, no por nombre. VertdnControl.
API
Clases CSS aplicadas al host
| Clase CSS | Condición |
|---|---|
.tdn-form-element | Siempre aplicada (clase base) |
.tdn-form-element--disabled | Control deshabilitado |
.tdn-form-element--focus | Control con foco |
.tdn-form-element--active | Control con valor (hasValue) |
.tdn-form-element--dirty | Valor modificado, o control marcado con markAsDirty() |
.tdn-form-element--valid | Control válido |
.tdn-form-element--invalid | Control inválido |
.tdn-form-element--pending | Validación asíncrona en proceso |
.tdn-form-element--touched | Control tocado (blur, markAsTouched() o markAllAsTouched()) |
--dirtyEl 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
| Miembro | Tipo | Descripción |
|---|---|---|
formElement | Signal<TdnControlDirective | undefined> | El primer tdnControl contenido; de él salen las clases |
disabled, focus, active, dirty, valid, invalid, pending, touched | Signal<boolean> | Estado del control interno; false si no hay tdnControl |
errorsFor(control) | (control: AbstractControl | null | undefined) => TdnErrorsDirective | null | Bloque [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
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
- Sin clases manuales: no escribes
[class.error]ni[class.focused]. - Consistencia: todos los campos se comportan igual visualmente.
- Estilos centralizados: viven en
@tiendana/web-foundation. - Accesibilidad: el campo se anuncia inválido solo cuando hay un mensaje visible que lo explica.
- Rendimiento: usa signals y
OnPush.
Buenas Prácticas
-
Usa siempre
tdnControlen el control interno. -
Coloca el bloque
[tdnErrors]dentro del mismotdn-form-element: un bloque fuera del campo muestra sus mensajes, pero el control no queda descrito por él. -
No anides
tdn-form-elementdentro de otro. -
Mantén el envío habilitado y revela los errores con
FormRevealService. -
Asocia cada
labelcon su input (for/id) para que el nombre accesible del campo sea correcto.
Ver También
tdnControl: signals de estado y cableado ARIA- Manejo de Errores:
[tdnErrors]ytdnError FormRevealService: revelar el primer error al enviar