Files
pireon-ui/libs/ui

@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)
npm start              # catálogo de componentes en http://localhost:4200
npm run build          # construye la librería
npm run build:playground
npm run build:all      # librería + playground
npm run lint
npm run format

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 [formField] — ése es el selector de la directiva FormField, no [field]:

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

@Component({
  imports: [FormField, PiTextFieldComponent, PiNumberFieldComponent, PiSlideToggleComponent],
  template: `
    <pi-text-field [formField]="f.name" placeholder="Nombre" />
    <pi-number-field [formField]="f.age" />
    <pi-slide-toggle [formField]="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. De 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.

Se puede ver funcionando en la app playground (nx serve playground).

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.

El loader de servidor de traducciones ya no está en el barrel. util/translate/loaders/translate-server.loader usa fs, path y process; exportarlo desde @pi/ui rompía el bundle de navegador de cualquier app que importase la librería. Se importa por su ruta directa desde el arranque de SSR.

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 — pasó a input() y después, con la migración a Signal Forms, se renombró a disabled (ver la sección de formularios).
  • 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.