@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:
formControlNameyformGroupNamedesaparecen.incremental-range-fieldyrange-sliderya no leen unFormGroupanidado: reciben el rango completo porvalue(tipoPiRange, 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 inputerrors, usandoerror.messagey cayendo aerror.kindsi no lo hay. La clase base exponeerrorMessages(). - Los campos numéricos usan
transformedValue, el mecanismo de Signal Forms para parsear texto→número reportando errores de parseo al campo. ngx-currencyse ha eliminado decurrency-field. Su directivacurrencyMaskes un ControlValueAccessor y no funciona sin reactive forms. El formateo (decimal,, miles., 2 decimales) se replica contransformedValue+toLocaleString('es-ES'). Se pierde el enmascarado en vivo mientras se escribe: ahora se formatea al salir del campo.slide-toggleya no extiendePiBaseFormFieldComponent: el contrato de casilla exigecheckedy prohíbe declararvalue. También desaparece el campoisCheckedque se sincronizaba por suscripción.touch: los controles emiten esta salida en elblurpara 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— elonTabChange()del setter pasa a uneffect().PiBaseFormFieldComponent.isDisabled— pasó ainput()y después, con la migración a Signal Forms, se renombró adisabled(ver la sección de formularios).PiSearchFieldComponent.isExpanded—model(); la emisión deisActivepasa a uneffect().PiSelectOptionComponent.isSelected—model()(los selects padre lo asignan sobre las instancias hijas); sumarkForCheck()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 importaTranslatePipe/TranslateDirective. - apollo-angular 14 — no hay
ApolloModule.PiSharedUtilDatoCmsModule→providePiDatoCms(). - ngx-currency 19 —
CurrencyMaskInputMode→NgxCurrencyInputMode,NgxCurrencyModule→NgxCurrencyDirective. - chart.js 4 —
chart.js/dist/types/utilsno es importable; los tipos se derivan deChartOptions. - Angular —
TransferState/StateKey/makeStateKeyvienen de@angular/core.
Pendiente
- Tests: no se copiaron los
*.spec.tsdel origen. Sectorvivía endeale-saas/util/models(lib de aplicación, fuera de alcance); se trajo la interfaz autil/models.- ngrx 21 con Angular 22: instalado con
--legacy-peer-deps(aún no declara soporte para 22). - Lint:
nx lint uifalla 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) yno-unused-vars. Nada bloquea el build. Para el primer grupo hay migración oficial:nx g @angular/core:inject-migration.