# @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// antes shared/util/ (24 libs) lib/ui// antes shared/ui/ (29 libs) ``` ```bash 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]`: ```ts import { form, required, min, FormField } from '@angular/forms/signals'; @Component({ imports: [FormField, PiTextFieldComponent, PiNumberFieldComponent, PiSlideToggleComponent], template: ` `, }) 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()`, 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()` 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 `` 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 `, 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 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`. - **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`.