Files
pireon-ui/libs/ui/README.md

11 KiB

@pi/ui

Librería de componentes Angular migrada desde monorepo-nx-ddd-demo/libs/shared (Angular 14, NgModules, decoradores) a Angular 22 con componentes standalone y señales.

libs/ui/src/
  index.ts            barrel público de @pi/ui
  lib/styles/         antes shared/styles
  lib/util/<n>/       antes shared/util/<n>   (24 libs)
  lib/ui/<n>/         antes shared/ui/<n>     (29 libs)

Construir: nx build ui · Lint: nx lint ui

Qué se hizo

Prefijo Dl→Pi, dl-→pi- (selectores, clases CSS, nombres de fichero), @deale-workspace/…→rutas relativas
NgModules 41 → 4 (sólo quedan los que aportan providers: icon-registry, loaders, router-store, scroll-to)
Componentes 70, todos con ChangeDetectionStrategy.OnPush
Decoradores 0 restantes: 460 input(), 6 model(), 20 output(), 8 viewChild(), 2 viewChildren(), 17 contentChild(), 6 contentChildren()
Control flow *ngIf/*ngFor/*ngSwitch → @if/@for/@switch (110 @if, 26 @for, 3 @switch). Ya no se importa ninguna directiva de CommonModule
Formularios Los 13 campos de ui/form-field y los 2 de ui/slider pasan de ControlValueAccessor + formControlName a Signal Forms (FormValueControl / FormCheckboxControl)

Se usaron las migraciones oficiales de Angular (@angular/core:standalone, signal-input-migration, output-migration, signal-queries-migration, control-flow-migration, cleanup-unused-imports) y se resolvió a mano lo que no cubren.

Formularios: Signal Forms

Los campos ya no son ControlValueAccessor ni reciben formControlName. Implementan los contratos de Signal Forms, así que basta con enlazar [field]:

import { form, required, min, FormField } from '@angular/forms/signals';

@Component({
  imports: [FormField, PiTextFieldComponent, PiNumberFieldComponent, PiSlideToggleComponent],
  template: `
    <pi-text-field [field]="f.name" placeholder="Nombre" />
    <pi-number-field [field]="f.age" />
    <pi-slide-toggle [field]="f.active" />
  `,
})
export class Ejemplo {
  private readonly model = signal({ name: '', age: null, active: false });

  protected readonly f = form(this.model, (m) => {
    required(m.name);
    min(m.age, 18);
  });
}

La directiva FormField sincroniza sola value, disabled, errors, touched, required, min/max, maxLength… con el campo enlazado.

Componente Contrato Tipo del valor
text, email, password, phone, icon, search, textarea FormValueControl string
number, currency, incremental-number FormValueControl number | null
slide-toggle FormCheckboxControl boolean (usa checked, no value)
incremental-range, range-slider FormValueControl PiRange ({from, to})
slider FormValueControl number

Detalles a tener en cuenta:

  • formControlName y formGroupName desaparecen. incremental-range-field y range-slider ya no leen un FormGroup anidado: reciben el rango completo por value (tipo PiRange, exportado por la librería).
  • isDisabled → disabled, que es el nombre del contrato y ahora es una señal (disabled()).
  • Los mensajes de error venían de control.errors['message'] (un array de claves i18n). Ahora se leen del input errors, usando error.message y cayendo a error.kind si no lo hay. La clase base expone errorMessages().
  • Los campos numéricos usan transformedValue, el mecanismo de Signal Forms para parsear texto→número reportando errores de parseo al campo.
  • ngx-currency se ha eliminado de currency-field. Su directiva currencyMask es un ControlValueAccessor y no funciona sin reactive forms. El formateo (decimal ,, miles ., 2 decimales) se replica con transformedValue + toLocaleString('es-ES'). Se pierde el enmascarado en vivo mientras se escribe: ahora se formatea al salir del campo.
  • slide-toggle ya no extiende PiBaseFormFieldComponent: el contrato de casilla exige checked y prohíbe declarar value. También desaparece el campo isChecked que se sincronizaba por suscripción.
  • touch: los controles emiten esta salida en el blur para que el campo se marque como tocado.

Los selects (ui/select) siguen con ControlValueAccessor; quedaron fuera del alcance de esta migración.

Decisiones que cambian comportamiento

Revisar estos puntos antes de usar la librería en producción:

1. input.required no significa lo mismo que el antiguo !. La migración convierte @Input() x!: T en input.required<T>(), pero en Angular 14 no existían las entradas obligatorias: el ! sólo era una aserción de asignación definida. Se devolvieron a opcionales las que las propias plantillas de la librería demostraban que no eran obligatorias (allowPropagation, flexDirection, value de text-viewer, gap/icon/label de divider, link/gap de headline, device/justify/itemWidth de carousel, las de grid-item…). Las input.required que quedan endurecen la API respecto al original; conviene repasarlas contra el uso real en las aplicaciones.

2. Entradas con accessor → input()/model() + effect(). Seis entradas tenían getter/setter con efectos secundarios. El efecto se ejecuta durante la detección de cambios, no de forma síncrona en la asignación:

  • PiTabComponent.defaultSelectedTab — el onTabChange() del setter pasa a un effect().
  • PiBaseFormFieldComponent.isDisabled — pasa a input(); el estado efectivo a leer sigue siendo disabled (lo escriben también el CVA y el efecto).
  • PiSearchFieldComponent.isExpanded — model(); la emisión de isActive pasa a un effect().
  • PiSelectOptionComponent.isSelected — model() (los selects padre lo asignan sobre las instancias hijas); su markForCheck() manual ya no hace falta.
  • PiBaseMenuComponent.menuItems — el setter era un pass-through puro.
  • PiSelectMultipleComponent.selectedOptions — sólo tenía getter, así que el @Input() nunca pudo asignarse desde fuera: era un valor derivado.

3. PiAccordionComponent.expandedItemIndex pasa a model(). Tenía un two-way binding hecho a mano (expandedItemIndex + expandedItemIndexChange). model() genera el evento automáticamente. Ojo: el código original emitía el valor anterior al plegar/desplegar (emit(expandedItemIndex) antes de reasignar); model() emite el nuevo. Parece que el original tenía un bug. Su tipo pasa a number | null porque toggleState asigna null al colapsar.

4. Suscripciones a .changes eliminadas. Las signal queries no exponen QueryList.changes. Donde sólo se usaba para markForCheck() (accordion, tab, carousel) se eliminó: Angular ya reevalúa la plantilla al cambiar la señal. En los 4 selects se sustituyó por toObservable().

5. Salidas que emitían any. changePage y toggledTo eran new EventEmitter() sin genérico. Ahora son output<number>() y output<'on' | 'off'>().

6. Aserciones ! en plantillas. *ngIf="icon()" no estrecha el tipo (TypeScript no estrecha resultados de llamada entre expresiones), así que hay [svgIcon]="icon()!" en ~30 sitios. Mantiene el runtime idéntico al original. Se pueden eliminar migrando a @if (icon(); as icon).

7. Precedencia de pipes en select-multiple. x | truncate : 33 || (placeholder() | translate) se parseaba como truncate : (33 || …), así que el fallback nunca se aplicaba. Ahora está parentizado como se pretendía.

8. Los 3 selects se convirtieron a control flow a mano. La migración automática se rindió con ellos: tenían un *ngIf ... else que apuntaba a un <ng-template> con un [ngSwitch] dentro. Al reescribirlos hubo que renombrar plantillas duplicadas (#defaultSelected aparecía dos veces en ramas distintas del switch) a #defaultSelectedFill/#defaultSelectedOutline, y en select-period #defaultOption → #defaultMonthOption/#defaultYearOption. Los *ngFor sin trackBy pasaron a track <item>, que es lo que hacía ngFor por defecto. En data-grid y select-carousel la migración generó track trackByFn(i, item) pero esos trackByFn sólo declaran index: se corrigió la aridad a track trackByFn(i).

Colisiones de nombres en el barrel

El origen tiene símbolos duplicados. En index.ts gana el primero y el otro se omite (queda anotado en el fichero). Siguen siendo accesibles por ruta directa:

Símbolo Se exporta desde Se omite de
PiSlideToggleComponent ui/form-field ui/slide-toggle
PiDisabledDirective util/status ui/text
featureKey, getFeatureState, initialState util/device-configs util/list-configs

APIs de terceros actualizadas

  • ngx-translate 18 — no hay TranslateModule. PiSharedUtilTranslateModule → providePiTranslate(); en plantillas se importa TranslatePipe/TranslateDirective.
  • apollo-angular 14 — no hay ApolloModule. PiSharedUtilDatoCmsModule → providePiDatoCms().
  • ngx-currency 19 — CurrencyMaskInputMode→NgxCurrencyInputMode, NgxCurrencyModule→NgxCurrencyDirective.
  • chart.js 4 — chart.js/dist/types/utils no es importable; los tipos se derivan de ChartOptions.
  • Angular — TransferState/StateKey/makeStateKey vienen de @angular/core.

Pendiente

  • Tests: no se copiaron los *.spec.ts del origen.
  • Sector vivía en deale-saas/util/models (lib de aplicación, fuera de alcance); se trajo la interfaz a util/models.
  • ngrx 21 con Angular 22: instalado con --legacy-peer-deps (aún no declara soporte para 22).
  • Lint: nx lint ui falla con avisos heredados del código original — prefer-inject (51), directive-selector (46), no-explicit-any (35), no-unused-expressions (33), a11y (click-events-have-key-events, interactive-supports-focus, label-has-associated-control) y no-unused-vars. Nada bloquea el build. Para el primer grupo hay migración oficial: nx g @angular/core:inject-migration.