From 779f1c1308073bf0c7f5e61546b51af03706f11b Mon Sep 17 00:00:00 2001 From: Josep Piguillem Date: Mon, 27 Jul 2026 23:40:36 +0200 Subject: [PATCH] fix: adapta escrituras internas a model() y signal queries (toObservable) --- .../accordion/accordion.component.ts | 110 ++++++++---------- .../switch-button/switch-button.component.ts | 7 +- .../carousel/carousel/carousel.component.ts | 86 +++++++------- .../paginator/paginator.component.ts | 40 +++---- .../expansion-panel.component.ts | 9 +- .../select-carousel.component.ts | 16 +-- .../select-multiple.component.ts | 25 ++-- .../select-period/select-period.component.ts | 16 +-- .../lib/ui/select/select/select.component.ts | 15 +-- libs/ui/src/lib/ui/tab/tab/tab.component.ts | 13 +-- .../text/directives/text-styles.directive.ts | 12 +- .../status/directives/status.directive.ts | 85 ++++++-------- 12 files changed, 199 insertions(+), 235 deletions(-) diff --git a/libs/ui/src/lib/ui/accordion/accordion/accordion.component.ts b/libs/ui/src/lib/ui/accordion/accordion/accordion.component.ts index df3e800..9ffe8ad 100644 --- a/libs/ui/src/lib/ui/accordion/accordion/accordion.component.ts +++ b/libs/ui/src/lib/ui/accordion/accordion/accordion.component.ts @@ -1,56 +1,60 @@ -import { - AfterContentInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - HostBinding, - OnChanges, - SimpleChanges, - input, - output, - contentChildren +import { + ChangeDetectionStrategy, + Component, + HostBinding, + input, + model, + contentChildren } from '@angular/core'; import { padding, styleType, textColor } from '../../../styles'; -import { PiAccordionItemDirective } from '../directives/accordion-content.directive'; -import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common'; -import { PiDynamicColorDirective } from '../../../util/color/directives/colors.directive'; -import { PiTextViewerComponent } from '../../text/text-viewer/text-viewer.component'; -import { PiChipStatusComponent } from '../../chip/chip-status/chip-status.component'; -import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component'; -import { PiIconComponent } from '../../icon/icon/icon.component'; +import { PiAccordionItemDirective } from '../directives/accordion-content.directive'; +import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common'; +import { PiDynamicColorDirective } from '../../../util/color/directives/colors.directive'; +import { PiTextViewerComponent } from '../../text/text-viewer/text-viewer.component'; +import { PiChipStatusComponent } from '../../chip/chip-status/chip-status.component'; +import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component'; +import { PiIconComponent } from '../../icon/icon/icon.component'; import { TranslatePipe } from '@ngx-translate/core'; -@Component({ - selector: 'pi-accordion', - templateUrl: './accordion.component.html', - styleUrls: ['../styles/accordion.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - NgIf, - NgFor, - PiDynamicColorDirective, - PiTextViewerComponent, - NgTemplateOutlet, - PiChipStatusComponent, - PiIconButtonComponent, - PiIconComponent, - TranslatePipe, - ], +@Component({ + selector: 'pi-accordion', + templateUrl: './accordion.component.html', + styleUrls: ['../styles/accordion.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + NgIf, + NgFor, + PiDynamicColorDirective, + PiTextViewerComponent, + NgTemplateOutlet, + PiChipStatusComponent, + PiIconButtonComponent, + PiIconComponent, + TranslatePipe, + ], }) -export class PiAccordionComponent implements OnChanges, AfterContentInit { +export class PiAccordionComponent { @HostBinding('class') get classes(): string { const colorClass = `pi-accordion__${this.color()}`; - const appearance = this.appearance(); + const appearance = this.appearance(); const appearanceClass = appearance !== 'default' && `pi-accordion__appearance--${appearance}`; - const direction = this.direction(); + const direction = this.direction(); const directionClass = direction !== 'default' && `pi-accordion__direction--${direction}`; const paddingClass = `pi-accordion__padding--${this.padding()}`; return ['pi-accordion', colorClass, appearanceClass, directionClass, paddingClass].join(' '); } - readonly expandedItemIndex = input.required(); + /** + * `model` en vez de `input` + output manual: el componente escribe este valor + * al plegar/desplegar. El evento `expandedItemIndexChange` que antes se + * declaraba a mano lo genera `model()` automáticamente. + * + * El tipo incluye `null` porque `toggleState` lo asigna al colapsar; antes + * estaba declarado como `number` y esa asignación no compilaba. + */ + readonly expandedItemIndex = model(null); readonly appearance = input<'default' | 'outline' | 'fill'>('default'); readonly color = input('white'); readonly direction = input<'default' | 'horizontal'>('default'); @@ -59,44 +63,22 @@ export class PiAccordionComponent implements OnChanges, AfterContentInit { readonly hasChevron = input(true); - readonly expandedItemIndexChange = output(); - readonly items = contentChildren(PiAccordionItemDirective); - constructor(private cdr: ChangeDetectorRef) {} - - ngOnChanges(changes: SimpleChanges): void { - if ( - changes['expandedItemIndex'] && - changes['expandedItemIndex'].currentValue !== changes['expandedItemIndex'].previousValue - ) { - this.expandedItemIndex = changes['expandedItemIndex'].currentValue; - } - } - - ngAfterContentInit(): void { - this.items().changes.subscribe(() => { - this.cdr.markForCheck(); - }); - } + // `items` es una signal query: Angular reevalúa la plantilla cuando cambia, + // así que el ngAfterContentInit que sólo hacía markForCheck() sobra. toggleState = (item: PiAccordionItemDirective, index: number) => { if (this.direction() === 'horizontal' && this.isItemExpanded(index)) return; if (item.status() === 'disabled') return; - const expandedItemIndex = this.expandedItemIndex(); - if (expandedItemIndex === index) { - this.expandedItemIndex = null; - } else { - this.expandedItemIndex = index; - } - this.expandedItemIndexChange.emit(expandedItemIndex); + this.expandedItemIndex.set(this.expandedItemIndex() === index ? null : index); }; isItemExpanded = (index: number) => this.expandedItemIndex() === index; setAccordionItemClasses(item: PiAccordionItemDirective, index: number): string { const expandedClass = this.isItemExpanded(index) ? 'is-expanded' : 'is-collapsed'; - const status = item.status(); + const status = item.status(); const statusClass = status && 'has-status' + ' ' + `is-${status}`; const isConfidentialClass = item.isConfidential() ? 'is-confidential' : ''; return [expandedClass, statusClass, isConfidentialClass].join(' '); diff --git a/libs/ui/src/lib/ui/button/switch-button/switch-button.component.ts b/libs/ui/src/lib/ui/button/switch-button/switch-button.component.ts index b5d8771..92c7233 100644 --- a/libs/ui/src/lib/ui/button/switch-button/switch-button.component.ts +++ b/libs/ui/src/lib/ui/button/switch-button/switch-button.component.ts @@ -3,6 +3,7 @@ import { Component, HostBinding, input, + model, output } from '@angular/core'; import { ComponentFamilyClass } from '../../../styles'; @@ -29,7 +30,7 @@ export class PiSwitchButtonComponent extends PiBaseButtonComponent { ].join(' '); } - readonly isActive = input(true); + readonly isActive = model(true); readonly toggledTo = output(); readonly contentType = input<'icon' | 'text'>('icon'); @@ -40,10 +41,10 @@ export class PiSwitchButtonComponent extends PiBaseButtonComponent { toggleSelected(): void { if (this.isActive()) { - this.isActive = false; + this.isActive.set(false); this.toggledTo.emit('off'); } else { - this.isActive = true; + this.isActive.set(true); this.toggledTo.emit('on'); } } diff --git a/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts index 100351f..db215db 100644 --- a/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts +++ b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts @@ -1,56 +1,56 @@ // Angular -import { - AfterViewChecked, - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - HostBinding, - OnChanges, - OnDestroy, - OnInit, - Renderer2, - SimpleChanges, - input, - output, - viewChild, - contentChildren +import { + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + OnChanges, + OnDestroy, + OnInit, + Renderer2, + SimpleChanges, + input, + output, + viewChild, + contentChildren } from '@angular/core'; // Shared import { ComponentFamilyClass, justify, theming } from '../../../styles'; import { Device, DeviceConfigsFacade } from '../../../util/device-configs'; import { PiCarouselItemDirective } from '../carousel.directive'; -import { Subject, combineLatest, skip, takeUntil } from 'rxjs'; -import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common'; +import { Subject, combineLatest, skip, takeUntil } from 'rxjs'; +import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common'; import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component'; -@Component({ - selector: 'pi-carousel', - templateUrl: './carousel.component.html', - styleUrls: ['./carousel.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - NgIf, - PiIconButtonComponent, - NgFor, - NgTemplateOutlet, - ], +@Component({ + selector: 'pi-carousel', + templateUrl: './carousel.component.html', + styleUrls: ['./carousel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + NgIf, + PiIconButtonComponent, + NgFor, + NgTemplateOutlet, + ], }) export class PiCarouselComponent implements OnInit, OnChanges, AfterViewInit, AfterViewChecked, OnDestroy { @HostBinding('class') get classes(): string { - const carouselItems = this.carouselItems(); + const carouselItems = this.carouselItems(); const itemsNumberClass = carouselItems && `pi-carousel__${carouselItems.length}-items`; - const direction = this.direction(); + const direction = this.direction(); const directionTypeClass = direction && `pi-carousel__${direction}`; const colorClass = `pi-carousel__${this.color()}`; const typeClass = `pi-carousel__${this.type()}`; const isDraggableClass = this.isDraggable() ? 'is-draggable' : ''; - const justifyValue = this.justify(); + const justifyValue = this.justify(); const justifyClass = justifyValue && `pi-carousel__justify-content--${justifyValue}`; return [ ComponentFamilyClass.Carousel, @@ -256,10 +256,8 @@ export class PiCarouselComponent if (this.isDraggable()) { this.setDragEventListeners(); } - - this.carouselItems().changes.pipe(takeUntil(this.destroyed$)).subscribe(() => { - this.cdr.markForCheck(); - }); + // La suscripción a `carouselItems.changes` sólo hacía markForCheck(); + // al ser `carouselItems` una signal query, Angular ya lo hace solo. } ngAfterViewChecked(): void { @@ -305,13 +303,13 @@ export class PiCarouselComponent } setItemsPerPage(): void { - const itemWidth = this.itemWidth(); + const itemWidth = this.itemWidth(); if (!!itemWidth) { this.itemsPerPage = Math.floor(this.carouselContentElement.clientWidth / itemWidth) || 1; } else { - const itemsDesktop = this.itemsDesktop(); - const itemsTablet = this.itemsTablet(); - const itemsMobile = this.itemsMobile(); + const itemsDesktop = this.itemsDesktop(); + const itemsTablet = this.itemsTablet(); + const itemsMobile = this.itemsMobile(); switch (this.device()) { case 'DESKTOP': this.itemsPerPage = itemsDesktop ? itemsDesktop : this.carouselItems().length; @@ -436,7 +434,7 @@ export class PiCarouselComponent * Sets a minimum dragging width based on either 80% of the item width if provided * Otherwise 80% of the width of one item in terms of the number of items displayed per page */ - const itemWidth = this.itemWidth(); + const itemWidth = this.itemWidth(); const minDraggingWidth = itemWidth ? 0.8 * itemWidth : 0.8 * (100 / this.itemsPerPage); this.isDragging = true; @@ -470,7 +468,7 @@ export class PiCarouselComponent } private carouselTranslation(currentItem: number): void { - const type = this.type(); + const type = this.type(); if (type === 'linear' && currentItem <= this.carouselItems().length - this.itemsPerPage) { this.carouselItemsTranslation(currentItem, `-${currentItem * 100}%`, this.direction()); } else if (type === 'symmetric') { @@ -512,7 +510,7 @@ export class PiCarouselComponent private setHorizontalCarouselStyle(): void { this.carouselItemsNativeElements.forEach((item) => { - const itemWidth = this.itemWidth(); + const itemWidth = this.itemWidth(); if (!!itemWidth) { this.renderer.setStyle(item, 'min-width', `${itemWidth}px`); this.renderer.setStyle(item, 'max-width', `${itemWidth}px`); diff --git a/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts index fcea2b2..0e045e6 100644 --- a/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts +++ b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts @@ -1,25 +1,25 @@ -import { ChangeDetectionStrategy, Component, OnInit, HostBinding, forwardRef, input, output } from '@angular/core'; -import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component'; -import { NgIf, NgFor } from '@angular/common'; -import { PiIconComponent } from '../../icon/icon/icon.component'; +import { ChangeDetectionStrategy, Component, OnInit, HostBinding, forwardRef, input, model, output } from '@angular/core'; +import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component'; +import { NgIf, NgFor } from '@angular/common'; +import { PiIconComponent } from '../../icon/icon/icon.component'; -@Component({ - selector: 'pi-paginator', - templateUrl: './paginator.component.html', - styleUrls: ['./paginator.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - PiIconButtonComponent, - NgIf, - NgFor, - PiIconComponent, - forwardRef(() => PiPaginatorComponent) -], +@Component({ + selector: 'pi-paginator', + templateUrl: './paginator.component.html', + styleUrls: ['./paginator.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + PiIconButtonComponent, + NgIf, + NgFor, + PiIconComponent, + forwardRef(() => PiPaginatorComponent) +], }) export class PiPaginatorComponent implements OnInit { @HostBinding('class') className = 'pi-paginator'; - readonly currentPage = input(1); + readonly currentPage = model(1); readonly pageSize = input.required(); readonly totalPages = input.required(); readonly isDisabled = input.required(); @@ -65,7 +65,7 @@ export class PiPaginatorComponent implements OnInit { this.firstPages = this.decrementPaginatorRangeBy(3); this.firstPagesRange = this.firstPagesRange - 3; } - this.currentPage = updatedCurrentPage; + this.currentPage.set(updatedCurrentPage); this.changePage.emit(updatedCurrentPage); } } @@ -77,7 +77,7 @@ export class PiPaginatorComponent implements OnInit { this.firstPagesRange = this.firstPagesRange + 3; this.firstPages = this.incrementPaginatorRangeBy(3); } - this.currentPage = updatedCurrentPage; + this.currentPage.set(updatedCurrentPage); this.changePage.emit(updatedCurrentPage); } } @@ -86,7 +86,7 @@ export class PiPaginatorComponent implements OnInit { if (this.isDisabled() && selectedPage > this.currentPage()) { return; } else { - this.currentPage = selectedPage; + this.currentPage.set(selectedPage); this.changePage.emit(this.currentPage()); } } diff --git a/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts index 066ec19..0a46d08 100644 --- a/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts +++ b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts @@ -7,6 +7,7 @@ import { SimpleChanges, TemplateRef, input, + model, output, contentChild } from '@angular/core'; @@ -59,7 +60,7 @@ export class PiExpansionPanelComponent extends PiBasePanelComponent implements O } readonly type = input<'form' | 'custom'>('form'); - readonly isCollapsed = input(false); + readonly isCollapsed = model(false); readonly title = input(); readonly subtitle = input(); readonly formGroup = input.required(); @@ -72,16 +73,16 @@ export class PiExpansionPanelComponent extends PiBasePanelComponent implements O readonly expandableContentRef = contentChild>('expandableContent'); override ngOnInit(): void { - if (this.status() === 'disabled') this.isCollapsed = true; + if (this.status() === 'disabled') this.isCollapsed.set(true); } ngOnChanges({ status }: SimpleChanges): void { - if (status?.currentValue === 'disabled') this.isCollapsed = true; + if (status?.currentValue === 'disabled') this.isCollapsed.set(true); } toggle(): void { const status = this.status(); - if (status !== 'disabled' && status !== 'info') this.isCollapsed = !this.isCollapsed(); + if (status !== 'disabled' && status !== 'info') this.isCollapsed.set(!this.isCollapsed()); } onEdit(): void { diff --git a/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts index 1619735..2709655 100644 --- a/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts +++ b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts @@ -6,10 +6,10 @@ import { ElementRef, HostBinding, Injector, - QueryList, Renderer2, input } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import _ from 'lodash'; @@ -145,8 +145,8 @@ export class PiSelectCarouselComponent extends PiBaseSelectComponent implements ngAfterContentInit(): void { this.control.setValue(this.selectedOption); merge( - this.childrenOptionsComponents()?.changes || [], - this.viewChildOptionsComponents()?.changes || [], + toObservable(this.childrenOptionsComponents, { injector: this.injector }), + toObservable(this.viewChildOptionsComponents, { injector: this.injector }), this.control.valueChanges ).subscribe(() => { Promise.resolve().then(() => { @@ -180,9 +180,9 @@ export class PiSelectCarouselComponent extends PiBaseSelectComponent implements */ private updateChildrenOptionsSelectedState(selectedOption: any): void { // Array of child option components to work with. - let childrenOptionsComponent: Array; + let childrenOptionsComponent: readonly PiSelectOptionComponent[]; // Reference to the child option elements for DOM manipulation. - let childrenOptionsElementRef: QueryList; + let childrenOptionsElementRef: readonly ElementRef[]; // Check if we have child options that are content projected from a parent component. const childrenOptionsComponents = this.childrenOptionsComponents(); @@ -191,7 +191,7 @@ export class PiSelectCarouselComponent extends PiBaseSelectComponent implements childrenOptionsElementRef = this.childrenOptionsEl(); } else { // If not, it means they're internally defined (default), so we fetch from viewChildren. - childrenOptionsComponent = this.viewChildOptionsComponents; + childrenOptionsComponent = this.viewChildOptionsComponents(); childrenOptionsElementRef = this.viewChildOptionsEl(); } @@ -202,12 +202,12 @@ export class PiSelectCarouselComponent extends PiBaseSelectComponent implements : false; // If the current child option matches the selected option, highlight it. if (isSelected) { - this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); + this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-selected'); childOption.isSelected.set(true); } else { // Otherwise, ensure it is not highlighted. this._renderer.removeClass( - childrenOptionsElementRef.get(index)?.nativeElement, + childrenOptionsElementRef[index]?.nativeElement, 'is-selected' ); childOption.isSelected.set(false); diff --git a/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts index dff7080..cc579e6 100644 --- a/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts +++ b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts @@ -11,6 +11,7 @@ import { Renderer2, input } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { merge } from 'rxjs'; @@ -164,8 +165,8 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements ngAfterContentInit(): void { this.control.setValue(this.selectedOptions); merge( - this.childrenOptionsComponents()?.changes || [], - this.viewChildOptionsComponents()?.changes || [], + toObservable(this.childrenOptionsComponents, { injector: this.injector }), + toObservable(this.viewChildOptionsComponents, { injector: this.injector }), this.control.valueChanges ).subscribe(() => { Promise.resolve().then(() => { @@ -244,9 +245,9 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements */ private updateChildrenOptionsSelectedState(selectedOptions: any[]): void { // Array of child option components to work with. - let childrenOptionsComponent: Array; + let childrenOptionsComponent: readonly PiSelectOptionComponent[]; // Reference to the child option elements for DOM manipulation. - let childrenOptionsElementRef: QueryList; + let childrenOptionsElementRef: readonly ElementRef[]; // Check if we have child options that are content projected from a parent component. const childrenOptionsComponents = this.childrenOptionsComponents(); @@ -255,7 +256,7 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements childrenOptionsElementRef = this.childrenOptionsEl(); } else { // If not, it means they're internally defined (default), so we fetch from viewChildren. - childrenOptionsComponent = this.viewChildOptionsComponents; + childrenOptionsComponent = this.viewChildOptionsComponents(); childrenOptionsElementRef = this.viewChildOptionsEl(); } @@ -264,11 +265,11 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements (selectedOption) => selectedOption[this.keyIdProperty()] === childOption['id'] ); if (isSelected) { - this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); + this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-selected'); childOption.isSelected.set(true); } else { this._renderer.removeClass( - childrenOptionsElementRef.get(index)?.nativeElement, + childrenOptionsElementRef[index]?.nativeElement, 'is-selected' ); childOption.isSelected.set(false); @@ -295,9 +296,9 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements // Disable all options if the maximum number of selections has been reached. const disableOthers = selectedOptions.length >= (this.maxLength() || Infinity); // Array of child option components to work with. - let childrenOptionsComponent: Array; + let childrenOptionsComponent: readonly PiSelectOptionComponent[]; // Reference to the child option elements for DOM manipulation. - let childrenOptionsElementRef: QueryList; + let childrenOptionsElementRef: readonly ElementRef[]; // Check if we have child options that are content projected from a parent component. const childrenOptionsComponents = this.childrenOptionsComponents(); @@ -306,7 +307,7 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements childrenOptionsElementRef = this.childrenOptionsEl(); } else { // If not, it means they're internally defined (default), so we fetch from viewChildren. - childrenOptionsComponent = this.viewChildOptionsComponents; + childrenOptionsComponent = this.viewChildOptionsComponents(); childrenOptionsElementRef = this.viewChildOptionsEl(); } @@ -317,10 +318,10 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements const isDisabled = childOption.isDisabled(); if ((!isSelected && disableOthers) || isDisabled) { - this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-disabled'); + this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-disabled'); } else { this._renderer.removeClass( - childrenOptionsElementRef.get(index)?.nativeElement, + childrenOptionsElementRef[index]?.nativeElement, 'is-disabled' ); } diff --git a/libs/ui/src/lib/ui/select/select-period/select-period.component.ts b/libs/ui/src/lib/ui/select/select-period/select-period.component.ts index f549e53..22ef2d6 100644 --- a/libs/ui/src/lib/ui/select/select-period/select-period.component.ts +++ b/libs/ui/src/lib/ui/select/select-period/select-period.component.ts @@ -7,10 +7,10 @@ import { HostBinding, HostListener, Injector, - QueryList, Renderer2, input } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { merge } from 'rxjs'; @@ -161,8 +161,8 @@ export class PiSelectPeriodComponent extends PiBaseSelectComponent implements Af ngAfterContentInit(): void { this.control.setValue(this.selectedOption); merge( - this.childrenOptionsComponents()?.changes || [], - this.viewChildOptionsComponents()?.changes || [], + toObservable(this.childrenOptionsComponents, { injector: this.injector }), + toObservable(this.viewChildOptionsComponents, { injector: this.injector }), this.control.valueChanges ).subscribe(() => { Promise.resolve().then(() => { @@ -192,9 +192,9 @@ export class PiSelectPeriodComponent extends PiBaseSelectComponent implements Af */ private updateChildrenOptionsSelectedState(selectedOption: any): void { // Array of child option components to work with. - let childrenOptionsComponent: Array; + let childrenOptionsComponent: readonly PiSelectOptionComponent[]; // Reference to the child option elements for DOM manipulation. - let childrenOptionsElementRef: QueryList; + let childrenOptionsElementRef: readonly ElementRef[]; // Check if we have child options that are content projected from a parent component. const childrenOptionsComponents = this.childrenOptionsComponents(); @@ -203,7 +203,7 @@ export class PiSelectPeriodComponent extends PiBaseSelectComponent implements Af childrenOptionsElementRef = this.childrenOptionsEl(); } else { // If not, it means they're internally defined (default), so we fetch from viewChildren. - childrenOptionsComponent = this.viewChildOptionsComponents; + childrenOptionsComponent = this.viewChildOptionsComponents(); childrenOptionsElementRef = this.viewChildOptionsEl(); } @@ -214,12 +214,12 @@ export class PiSelectPeriodComponent extends PiBaseSelectComponent implements Af : false; // If the current child option matches the selected option, highlight it. if (isSelected) { - this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); + this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-selected'); childOption.isSelected.set(true); } else { // Otherwise, ensure it is not highlighted. this._renderer.removeClass( - childrenOptionsElementRef.get(index)?.nativeElement, + childrenOptionsElementRef[index]?.nativeElement, 'is-selected' ); childOption.isSelected.set(false); diff --git a/libs/ui/src/lib/ui/select/select/select.component.ts b/libs/ui/src/lib/ui/select/select/select.component.ts index a8ae8d3..f34dcd8 100644 --- a/libs/ui/src/lib/ui/select/select/select.component.ts +++ b/libs/ui/src/lib/ui/select/select/select.component.ts @@ -1,4 +1,5 @@ import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, HostListener, Injector, QueryList, Renderer2, forwardRef, input } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { merge } from 'rxjs'; @@ -151,8 +152,8 @@ export class PiSelectComponent extends PiBaseSelectComponent implements AfterCon ngAfterContentInit(): void { this.control.setValue(this.selectedOption); merge( - this.childrenOptionsComponents()?.changes || [], - this.viewChildOptionsComponents()?.changes || [], + toObservable(this.childrenOptionsComponents, { injector: this.injector }), + toObservable(this.viewChildOptionsComponents, { injector: this.injector }), this.control.valueChanges ).subscribe(() => { Promise.resolve().then(() => { @@ -182,9 +183,9 @@ export class PiSelectComponent extends PiBaseSelectComponent implements AfterCon */ private updateChildrenOptionsSelectedState(selectedOption: any): void { // Array of child option components to work with. - let childrenOptionsComponent: Array; + let childrenOptionsComponent: readonly PiSelectOptionComponent[]; // Reference to the child option elements for DOM manipulation. - let childrenOptionsElementRef: QueryList; + let childrenOptionsElementRef: readonly ElementRef[]; // Check if we have child options that are content projected from a parent component. const childrenOptionsComponents = this.childrenOptionsComponents(); @@ -193,7 +194,7 @@ export class PiSelectComponent extends PiBaseSelectComponent implements AfterCon childrenOptionsElementRef = this.childrenOptionsEl(); } else { // If not, it means they're internally defined (default), so we fetch from viewChildren. - childrenOptionsComponent = this.viewChildOptionsComponents; + childrenOptionsComponent = this.viewChildOptionsComponents(); childrenOptionsElementRef = this.viewChildOptionsEl(); } @@ -204,11 +205,11 @@ export class PiSelectComponent extends PiBaseSelectComponent implements AfterCon : false; // If the current child option matches the selected option, highlight it. if (isSelected) { - this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); + this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-selected'); childOption.isSelected.set(true); } else { // Otherwise, ensure it is not highlighted. this._renderer.removeClass( - childrenOptionsElementRef.get(index)?.nativeElement, + childrenOptionsElementRef[index]?.nativeElement, 'is-selected' ); childOption.isSelected.set(false); diff --git a/libs/ui/src/lib/ui/tab/tab/tab.component.ts b/libs/ui/src/lib/ui/tab/tab/tab.component.ts index c422eae..caf3549 100644 --- a/libs/ui/src/lib/ui/tab/tab/tab.component.ts +++ b/libs/ui/src/lib/ui/tab/tab/tab.component.ts @@ -1,14 +1,13 @@ import { AfterViewChecked, - AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, - Input, OnDestroy, Renderer2, + effect, input, output, contentChildren, @@ -37,7 +36,7 @@ import { TranslatePipe } from '@ngx-translate/core'; TranslatePipe, ], }) -export class PiTabComponent implements AfterViewInit, AfterViewChecked, OnDestroy { +export class PiTabComponent implements AfterViewChecked, OnDestroy { @HostBinding('class') get classes(): string { const appearanceClass = `pi-tab__appearance--${this.appearance()}`; const hideTabsClass = this.hideTabs() ? 'hide-tabs' : ''; @@ -106,11 +105,9 @@ export class PiTabComponent implements AfterViewInit, AfterViewChecked, OnDestro return isTabWrapperOverflowing; } - ngAfterViewInit(): void { - this.tabContentItems().changes.subscribe(() => { - this.cdr.markForCheck(); - }); - } + // El ngAfterViewInit original sólo suscribía a tabContentItems.changes para + // hacer markForCheck(); como ahora es una signal query, Angular ya reevalúa + // la plantilla cuando cambia. ngAfterViewChecked(): void { this.cdr.detectChanges(); diff --git a/libs/ui/src/lib/ui/text/directives/text-styles.directive.ts b/libs/ui/src/lib/ui/text/directives/text-styles.directive.ts index e4cd303..956307c 100644 --- a/libs/ui/src/lib/ui/text/directives/text-styles.directive.ts +++ b/libs/ui/src/lib/ui/text/directives/text-styles.directive.ts @@ -1,4 +1,4 @@ -import { Directive, HostBinding, OnChanges, SimpleChanges, input } from '@angular/core'; +import { Directive, HostBinding, input } from '@angular/core'; @Directive({ selector: '[pi-text-centered]', }) export class PiTextCenteredDirective { @@ -14,17 +14,13 @@ export class PiTextGroupDirective { } } @Directive({ selector: '[pi-disabled]', }) -export class PiDisabledDirective implements OnChanges { +export class PiDisabledDirective { @HostBinding('style.opacity') get opacity(): number { return this.isDisabled() ? 0.2 : 1; } + // El ngOnChanges original sólo reasignaba a la entrada su propio + // currentValue (no-op). Con signal inputs sobra. readonly isDisabled = input(); - - ngOnChanges(changes: SimpleChanges): void { - if (changes) { - this.isDisabled = changes['isDisabled'].currentValue; - } - } } @Directive({ selector: '[pi-covered]', }) diff --git a/libs/ui/src/lib/util/status/directives/status.directive.ts b/libs/ui/src/lib/util/status/directives/status.directive.ts index 5482bc8..3c7774e 100644 --- a/libs/ui/src/lib/util/status/directives/status.directive.ts +++ b/libs/ui/src/lib/util/status/directives/status.directive.ts @@ -1,49 +1,36 @@ -import { Directive, OnChanges, HostBinding, SimpleChanges, input } from '@angular/core'; - -@Directive({ selector: '[pi-disabled]', }) -export class PiDisabledDirective implements OnChanges { - @HostBinding('style.opacity') get opacity(): number { - return this.isDisabled() ? 0.33 : 1; - } - @HostBinding('class') get classes(): string | boolean { - return [this.isDisabled() && 'is-disabled'].join(' '); - } - - readonly isDisabled = input(); - - ngOnChanges(changes: SimpleChanges): void { - if (changes) { - this.isDisabled = changes['isDisabled'].currentValue; - } - } -} - -@Directive({ selector: '[is-active]', }) -export class PiIsActiveDirective { - @HostBinding('class') get classes(): string | boolean { - return [this.isActive() ? 'is-active' : '', 'pi-status'].join(' '); - } - - readonly isActive = input.required(); - - ngOnChanges(changes: SimpleChanges): void { - if (changes) { - this.isActive = changes['isActive'].currentValue; - } - } -} - -@Directive({ selector: '[is-visited]', }) -export class PiIsVisitedDirective { - @HostBinding('class') get classes(): string | boolean { - return [this.isVisited() ? 'is-visited' : '', 'pi-status'].join(' '); - } - - readonly isVisited = input.required(); - - ngOnChanges(changes: SimpleChanges): void { - if (changes) { - this.isVisited = changes['isVisited'].currentValue; - } - } -} +import { Directive, HostBinding, input } from '@angular/core'; + +/** + * Los `ngOnChanges` originales sólo reasignaban a la entrada su propio + * `currentValue`, es decir, eran no-ops. Con signal inputs sobran. + */ + +@Directive({ selector: '[pi-disabled]', }) +export class PiDisabledDirective { + @HostBinding('style.opacity') get opacity(): number { + return this.isDisabled() ? 0.33 : 1; + } + @HostBinding('class') get classes(): string | boolean { + return [this.isDisabled() && 'is-disabled'].join(' '); + } + + readonly isDisabled = input(); +} + +@Directive({ selector: '[is-active]', }) +export class PiIsActiveDirective { + @HostBinding('class') get classes(): string | boolean { + return [this.isActive() ? 'is-active' : '', 'pi-status'].join(' '); + } + + readonly isActive = input.required(); +} + +@Directive({ selector: '[is-visited]', }) +export class PiIsVisitedDirective { + @HostBinding('class') get classes(): string | boolean { + return [this.isVisited() ? 'is-visited' : '', 'pi-status'].join(' '); + } + + readonly isVisited = input.required(); +}