12 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)
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:
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.
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— 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 |
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.- 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.