Release @pi/ui 0.0.9: a field shows its errors once touched

An untouched field with errors was painted red from the start, so a
form with required fields opened full of «Campo obligatorio». Text
fields, selects, checkboxes, radio groups and file uploads now wait
until the field is touched (blur, or the form's submit()).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 20:25:39 +02:00
parent 2dd05241fc
commit 43d5dcf1d3
7 changed files with 28 additions and 6 deletions

View File

@@ -70,6 +70,12 @@ hecho **no se pueden enlazar a mano** esas entradas en un elemento que ya use
`[formField]`: Angular lo rechaza con `NG8022`. Los validadores van en el
`form()`, no en la plantilla.
Los errores se ven cuando el campo está **tocado**: al salir de él, o al
enviar con `submit()` de Signal Forms, que marca todo el formulario. Un campo
obligatorio vacío no sale en rojo nada más abrir el formulario. Para volver a
abrir un formulario como nuevo, `f().reset(valor)` pone los valores y quita el
estado de tocado.
Se puede ver funcionando en la app `playground` (`nx serve playground`).
## Theming: tokens CSS

View File

@@ -1,6 +1,6 @@
{
"name": "@pi/ui",
"version": "0.0.8",
"version": "0.0.9",
"description": "Librería de componentes Angular standalone (botones, campos con Signal Forms, cards, charts, drawer/hub/modal, theming vía tokens CSS...).",
"license": "MIT",
"generators": "./generators.json",

View File

@@ -59,6 +59,7 @@ export class PiFileUploadComponent implements FormValueControl<File[]> {
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
readonly disabled = input(false);
readonly touched = input(false);
readonly required = input(false);
readonly name = input('');
@@ -94,7 +95,10 @@ export class PiFileUploadComponent implements FormValueControl<File[]> {
this.errors().map((error) => error.message ?? error.kind),
);
protected readonly hasError = computed(() => this.errors().length > 0);
/** El error se ve cuando el campo está tocado (al salir de él o al enviar el formulario), no antes. */
protected readonly hasError = computed(
() => this.touched() && this.errors().length > 0,
);
/* --- Arrastrar y soltar ----------------------------------------------- */

View File

@@ -76,7 +76,10 @@ export abstract class PiBaseFormFieldComponent<
this.errors().map((error) => error.message ?? error.kind),
);
protected readonly hasError = computed(() => this.errors().length > 0);
/** El error se ve cuando el campo está tocado (al salir de él o al enviar el formulario), no antes. */
protected readonly hasError = computed(
() => this.touched() && this.errors().length > 0,
);
/**
* `MatInput` normalmente deriva `errorState` de un `NgControl` (`ngModel`,

View File

@@ -49,6 +49,7 @@ export class PiCheckboxComponent implements FormCheckboxControl {
readonly checked = model(false);
readonly disabled = input(false);
readonly touched = input(false);
readonly required = input(false);
readonly name = input('');
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
@@ -70,7 +71,10 @@ export class PiCheckboxComponent implements FormCheckboxControl {
this.errors().map((error) => error.message ?? error.kind),
);
protected readonly hasError = computed(() => this.errors().length > 0);
/** El error se ve cuando el campo está tocado (al salir de él o al enviar el formulario), no antes. */
protected readonly hasError = computed(
() => this.touched() && this.errors().length > 0,
);
/**
* `name` solo lo rellena la directiva `FormField`; suelto llega vacío. Sin

View File

@@ -57,6 +57,7 @@ export class PiRadioGroupComponent<
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
readonly disabled = input(false);
readonly touched = input(false);
readonly required = input(false);
readonly name = input('');
@@ -77,7 +78,10 @@ export class PiRadioGroupComponent<
this.errors().map((error) => error.message ?? error.kind),
);
protected readonly hasError = computed(() => this.errors().length > 0);
/** El error se ve cuando el campo está tocado (al salir de él o al enviar el formulario), no antes. */
protected readonly hasError = computed(
() => this.touched() && this.errors().length > 0,
);
/** Agrupa los `<input type="radio">`; debe ser único por grupo. */
private readonly uniqueName = `pi-radio-group-${nextUniqueId++}`;

View File

@@ -51,8 +51,9 @@ export abstract class PiBaseSelectComponent<
/** Se emite cuando el usuario interactúa; `FormField` lo usa para marcar `touched`. */
readonly touch = output<void>();
/** El error se ve cuando el campo está tocado, no antes. */
protected get hasError(): boolean {
return this.errors().length > 0;
return this.touched() && this.errors().length > 0;
}
// ======================