fix: adapta escrituras internas a model() y signal queries (toObservable)

This commit is contained in:
2026-07-27 23:40:36 +02:00
parent 3b640dab1b
commit 779f1c1308
12 changed files with 199 additions and 235 deletions

View File

@@ -1,56 +1,60 @@
import { import {
AfterContentInit, ChangeDetectionStrategy,
ChangeDetectionStrategy, Component,
ChangeDetectorRef, HostBinding,
Component, input,
HostBinding, model,
OnChanges, contentChildren
SimpleChanges,
input,
output,
contentChildren
} from '@angular/core'; } from '@angular/core';
import { padding, styleType, textColor } from '../../../styles'; import { padding, styleType, textColor } from '../../../styles';
import { PiAccordionItemDirective } from '../directives/accordion-content.directive'; import { PiAccordionItemDirective } from '../directives/accordion-content.directive';
import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common'; import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common';
import { PiDynamicColorDirective } from '../../../util/color/directives/colors.directive'; import { PiDynamicColorDirective } from '../../../util/color/directives/colors.directive';
import { PiTextViewerComponent } from '../../text/text-viewer/text-viewer.component'; import { PiTextViewerComponent } from '../../text/text-viewer/text-viewer.component';
import { PiChipStatusComponent } from '../../chip/chip-status/chip-status.component'; import { PiChipStatusComponent } from '../../chip/chip-status/chip-status.component';
import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component'; import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component';
import { PiIconComponent } from '../../icon/icon/icon.component'; import { PiIconComponent } from '../../icon/icon/icon.component';
import { TranslatePipe } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
@Component({ @Component({
selector: 'pi-accordion', selector: 'pi-accordion',
templateUrl: './accordion.component.html', templateUrl: './accordion.component.html',
styleUrls: ['../styles/accordion.component.scss'], styleUrls: ['../styles/accordion.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
NgIf, NgIf,
NgFor, NgFor,
PiDynamicColorDirective, PiDynamicColorDirective,
PiTextViewerComponent, PiTextViewerComponent,
NgTemplateOutlet, NgTemplateOutlet,
PiChipStatusComponent, PiChipStatusComponent,
PiIconButtonComponent, PiIconButtonComponent,
PiIconComponent, PiIconComponent,
TranslatePipe, TranslatePipe,
], ],
}) })
export class PiAccordionComponent implements OnChanges, AfterContentInit { export class PiAccordionComponent {
@HostBinding('class') get classes(): string { @HostBinding('class') get classes(): string {
const colorClass = `pi-accordion__${this.color()}`; const colorClass = `pi-accordion__${this.color()}`;
const appearance = this.appearance(); const appearance = this.appearance();
const appearanceClass = const appearanceClass =
appearance !== 'default' && `pi-accordion__appearance--${appearance}`; appearance !== 'default' && `pi-accordion__appearance--${appearance}`;
const direction = this.direction(); const direction = this.direction();
const directionClass = const directionClass =
direction !== 'default' && `pi-accordion__direction--${direction}`; direction !== 'default' && `pi-accordion__direction--${direction}`;
const paddingClass = `pi-accordion__padding--${this.padding()}`; const paddingClass = `pi-accordion__padding--${this.padding()}`;
return ['pi-accordion', colorClass, appearanceClass, directionClass, paddingClass].join(' '); return ['pi-accordion', colorClass, appearanceClass, directionClass, paddingClass].join(' ');
} }
readonly expandedItemIndex = input.required<number>(); /**
* `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<number | null>(null);
readonly appearance = input<'default' | 'outline' | 'fill'>('default'); readonly appearance = input<'default' | 'outline' | 'fill'>('default');
readonly color = input<styleType>('white'); readonly color = input<styleType>('white');
readonly direction = input<'default' | 'horizontal'>('default'); readonly direction = input<'default' | 'horizontal'>('default');
@@ -59,44 +63,22 @@ export class PiAccordionComponent implements OnChanges, AfterContentInit {
readonly hasChevron = input(true); readonly hasChevron = input(true);
readonly expandedItemIndexChange = output<number>();
readonly items = contentChildren(PiAccordionItemDirective); readonly items = contentChildren(PiAccordionItemDirective);
constructor(private cdr: ChangeDetectorRef) {} // `items` es una signal query: Angular reevalúa la plantilla cuando cambia,
// así que el ngAfterContentInit que sólo hacía markForCheck() sobra.
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();
});
}
toggleState = (item: PiAccordionItemDirective, index: number) => { toggleState = (item: PiAccordionItemDirective, index: number) => {
if (this.direction() === 'horizontal' && this.isItemExpanded(index)) return; if (this.direction() === 'horizontal' && this.isItemExpanded(index)) return;
if (item.status() === 'disabled') return; if (item.status() === 'disabled') return;
const expandedItemIndex = this.expandedItemIndex(); this.expandedItemIndex.set(this.expandedItemIndex() === index ? null : index);
if (expandedItemIndex === index) {
this.expandedItemIndex = null;
} else {
this.expandedItemIndex = index;
}
this.expandedItemIndexChange.emit(expandedItemIndex);
}; };
isItemExpanded = (index: number) => this.expandedItemIndex() === index; isItemExpanded = (index: number) => this.expandedItemIndex() === index;
setAccordionItemClasses(item: PiAccordionItemDirective, index: number): string { setAccordionItemClasses(item: PiAccordionItemDirective, index: number): string {
const expandedClass = this.isItemExpanded(index) ? 'is-expanded' : 'is-collapsed'; 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 statusClass = status && 'has-status' + ' ' + `is-${status}`;
const isConfidentialClass = item.isConfidential() ? 'is-confidential' : ''; const isConfidentialClass = item.isConfidential() ? 'is-confidential' : '';
return [expandedClass, statusClass, isConfidentialClass].join(' '); return [expandedClass, statusClass, isConfidentialClass].join(' ');

View File

@@ -3,6 +3,7 @@ import {
Component, Component,
HostBinding, HostBinding,
input, input,
model,
output output
} from '@angular/core'; } from '@angular/core';
import { ComponentFamilyClass } from '../../../styles'; import { ComponentFamilyClass } from '../../../styles';
@@ -29,7 +30,7 @@ export class PiSwitchButtonComponent extends PiBaseButtonComponent {
].join(' '); ].join(' ');
} }
readonly isActive = input(true); readonly isActive = model(true);
readonly toggledTo = output(); readonly toggledTo = output();
readonly contentType = input<'icon' | 'text'>('icon'); readonly contentType = input<'icon' | 'text'>('icon');
@@ -40,10 +41,10 @@ export class PiSwitchButtonComponent extends PiBaseButtonComponent {
toggleSelected(): void { toggleSelected(): void {
if (this.isActive()) { if (this.isActive()) {
this.isActive = false; this.isActive.set(false);
this.toggledTo.emit('off'); this.toggledTo.emit('off');
} else { } else {
this.isActive = true; this.isActive.set(true);
this.toggledTo.emit('on'); this.toggledTo.emit('on');
} }
} }

View File

@@ -1,56 +1,56 @@
// Angular // Angular
import { import {
AfterViewChecked, AfterViewChecked,
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
HostBinding, HostBinding,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Renderer2, Renderer2,
SimpleChanges, SimpleChanges,
input, input,
output, output,
viewChild, viewChild,
contentChildren contentChildren
} from '@angular/core'; } from '@angular/core';
// Shared // Shared
import { ComponentFamilyClass, justify, theming } from '../../../styles'; import { ComponentFamilyClass, justify, theming } from '../../../styles';
import { Device, DeviceConfigsFacade } from '../../../util/device-configs'; import { Device, DeviceConfigsFacade } from '../../../util/device-configs';
import { PiCarouselItemDirective } from '../carousel.directive'; import { PiCarouselItemDirective } from '../carousel.directive';
import { Subject, combineLatest, skip, takeUntil } from 'rxjs'; import { Subject, combineLatest, skip, takeUntil } from 'rxjs';
import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common'; import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common';
import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component'; import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component';
@Component({ @Component({
selector: 'pi-carousel', selector: 'pi-carousel',
templateUrl: './carousel.component.html', templateUrl: './carousel.component.html',
styleUrls: ['./carousel.component.scss'], styleUrls: ['./carousel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
NgIf, NgIf,
PiIconButtonComponent, PiIconButtonComponent,
NgFor, NgFor,
NgTemplateOutlet, NgTemplateOutlet,
], ],
}) })
export class PiCarouselComponent export class PiCarouselComponent
implements OnInit, OnChanges, AfterViewInit, AfterViewChecked, OnDestroy implements OnInit, OnChanges, AfterViewInit, AfterViewChecked, OnDestroy
{ {
@HostBinding('class') get classes(): string { @HostBinding('class') get classes(): string {
const carouselItems = this.carouselItems(); const carouselItems = this.carouselItems();
const itemsNumberClass = const itemsNumberClass =
carouselItems && `pi-carousel__${carouselItems.length}-items`; carouselItems && `pi-carousel__${carouselItems.length}-items`;
const direction = this.direction(); const direction = this.direction();
const directionTypeClass = direction && `pi-carousel__${direction}`; const directionTypeClass = direction && `pi-carousel__${direction}`;
const colorClass = `pi-carousel__${this.color()}`; const colorClass = `pi-carousel__${this.color()}`;
const typeClass = `pi-carousel__${this.type()}`; const typeClass = `pi-carousel__${this.type()}`;
const isDraggableClass = this.isDraggable() ? 'is-draggable' : ''; const isDraggableClass = this.isDraggable() ? 'is-draggable' : '';
const justifyValue = this.justify(); const justifyValue = this.justify();
const justifyClass = justifyValue && `pi-carousel__justify-content--${justifyValue}`; const justifyClass = justifyValue && `pi-carousel__justify-content--${justifyValue}`;
return [ return [
ComponentFamilyClass.Carousel, ComponentFamilyClass.Carousel,
@@ -256,10 +256,8 @@ export class PiCarouselComponent
if (this.isDraggable()) { if (this.isDraggable()) {
this.setDragEventListeners(); this.setDragEventListeners();
} }
// La suscripción a `carouselItems.changes` sólo hacía markForCheck();
this.carouselItems().changes.pipe(takeUntil(this.destroyed$)).subscribe(() => { // al ser `carouselItems` una signal query, Angular ya lo hace solo.
this.cdr.markForCheck();
});
} }
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
@@ -305,13 +303,13 @@ export class PiCarouselComponent
} }
setItemsPerPage(): void { setItemsPerPage(): void {
const itemWidth = this.itemWidth(); const itemWidth = this.itemWidth();
if (!!itemWidth) { if (!!itemWidth) {
this.itemsPerPage = Math.floor(this.carouselContentElement.clientWidth / itemWidth) || 1; this.itemsPerPage = Math.floor(this.carouselContentElement.clientWidth / itemWidth) || 1;
} else { } else {
const itemsDesktop = this.itemsDesktop(); const itemsDesktop = this.itemsDesktop();
const itemsTablet = this.itemsTablet(); const itemsTablet = this.itemsTablet();
const itemsMobile = this.itemsMobile(); const itemsMobile = this.itemsMobile();
switch (this.device()) { switch (this.device()) {
case 'DESKTOP': case 'DESKTOP':
this.itemsPerPage = itemsDesktop ? itemsDesktop : this.carouselItems().length; 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 * 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 * 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); const minDraggingWidth = itemWidth ? 0.8 * itemWidth : 0.8 * (100 / this.itemsPerPage);
this.isDragging = true; this.isDragging = true;
@@ -470,7 +468,7 @@ export class PiCarouselComponent
} }
private carouselTranslation(currentItem: number): void { private carouselTranslation(currentItem: number): void {
const type = this.type(); const type = this.type();
if (type === 'linear' && currentItem <= this.carouselItems().length - this.itemsPerPage) { if (type === 'linear' && currentItem <= this.carouselItems().length - this.itemsPerPage) {
this.carouselItemsTranslation(currentItem, `-${currentItem * 100}%`, this.direction()); this.carouselItemsTranslation(currentItem, `-${currentItem * 100}%`, this.direction());
} else if (type === 'symmetric') { } else if (type === 'symmetric') {
@@ -512,7 +510,7 @@ export class PiCarouselComponent
private setHorizontalCarouselStyle(): void { private setHorizontalCarouselStyle(): void {
this.carouselItemsNativeElements.forEach((item) => { this.carouselItemsNativeElements.forEach((item) => {
const itemWidth = this.itemWidth(); const itemWidth = this.itemWidth();
if (!!itemWidth) { if (!!itemWidth) {
this.renderer.setStyle(item, 'min-width', `${itemWidth}px`); this.renderer.setStyle(item, 'min-width', `${itemWidth}px`);
this.renderer.setStyle(item, 'max-width', `${itemWidth}px`); this.renderer.setStyle(item, 'max-width', `${itemWidth}px`);

View File

@@ -1,25 +1,25 @@
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, forwardRef, input, output } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit, HostBinding, forwardRef, input, model, output } from '@angular/core';
import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component'; import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component';
import { NgIf, NgFor } from '@angular/common'; import { NgIf, NgFor } from '@angular/common';
import { PiIconComponent } from '../../icon/icon/icon.component'; import { PiIconComponent } from '../../icon/icon/icon.component';
@Component({ @Component({
selector: 'pi-paginator', selector: 'pi-paginator',
templateUrl: './paginator.component.html', templateUrl: './paginator.component.html',
styleUrls: ['./paginator.component.scss'], styleUrls: ['./paginator.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
PiIconButtonComponent, PiIconButtonComponent,
NgIf, NgIf,
NgFor, NgFor,
PiIconComponent, PiIconComponent,
forwardRef(() => PiPaginatorComponent) forwardRef(() => PiPaginatorComponent)
], ],
}) })
export class PiPaginatorComponent implements OnInit { export class PiPaginatorComponent implements OnInit {
@HostBinding('class') className = 'pi-paginator'; @HostBinding('class') className = 'pi-paginator';
readonly currentPage = input(1); readonly currentPage = model(1);
readonly pageSize = input.required<number>(); readonly pageSize = input.required<number>();
readonly totalPages = input.required<number>(); readonly totalPages = input.required<number>();
readonly isDisabled = input.required<boolean>(); readonly isDisabled = input.required<boolean>();
@@ -65,7 +65,7 @@ export class PiPaginatorComponent implements OnInit {
this.firstPages = this.decrementPaginatorRangeBy(3); this.firstPages = this.decrementPaginatorRangeBy(3);
this.firstPagesRange = this.firstPagesRange - 3; this.firstPagesRange = this.firstPagesRange - 3;
} }
this.currentPage = updatedCurrentPage; this.currentPage.set(updatedCurrentPage);
this.changePage.emit(updatedCurrentPage); this.changePage.emit(updatedCurrentPage);
} }
} }
@@ -77,7 +77,7 @@ export class PiPaginatorComponent implements OnInit {
this.firstPagesRange = this.firstPagesRange + 3; this.firstPagesRange = this.firstPagesRange + 3;
this.firstPages = this.incrementPaginatorRangeBy(3); this.firstPages = this.incrementPaginatorRangeBy(3);
} }
this.currentPage = updatedCurrentPage; this.currentPage.set(updatedCurrentPage);
this.changePage.emit(updatedCurrentPage); this.changePage.emit(updatedCurrentPage);
} }
} }
@@ -86,7 +86,7 @@ export class PiPaginatorComponent implements OnInit {
if (this.isDisabled() && selectedPage > this.currentPage()) { if (this.isDisabled() && selectedPage > this.currentPage()) {
return; return;
} else { } else {
this.currentPage = selectedPage; this.currentPage.set(selectedPage);
this.changePage.emit(this.currentPage()); this.changePage.emit(this.currentPage());
} }
} }

View File

@@ -7,6 +7,7 @@ import {
SimpleChanges, SimpleChanges,
TemplateRef, TemplateRef,
input, input,
model,
output, output,
contentChild contentChild
} from '@angular/core'; } from '@angular/core';
@@ -59,7 +60,7 @@ export class PiExpansionPanelComponent extends PiBasePanelComponent implements O
} }
readonly type = input<'form' | 'custom'>('form'); readonly type = input<'form' | 'custom'>('form');
readonly isCollapsed = input(false); readonly isCollapsed = model(false);
readonly title = input<string>(); readonly title = input<string>();
readonly subtitle = input<string>(); readonly subtitle = input<string>();
readonly formGroup = input.required<FormGroup>(); readonly formGroup = input.required<FormGroup>();
@@ -72,16 +73,16 @@ export class PiExpansionPanelComponent extends PiBasePanelComponent implements O
readonly expandableContentRef = contentChild<TemplateRef<unknown>>('expandableContent'); readonly expandableContentRef = contentChild<TemplateRef<unknown>>('expandableContent');
override ngOnInit(): void { override ngOnInit(): void {
if (this.status() === 'disabled') this.isCollapsed = true; if (this.status() === 'disabled') this.isCollapsed.set(true);
} }
ngOnChanges({ status }: SimpleChanges): void { ngOnChanges({ status }: SimpleChanges): void {
if (status?.currentValue === 'disabled') this.isCollapsed = true; if (status?.currentValue === 'disabled') this.isCollapsed.set(true);
} }
toggle(): void { toggle(): void {
const status = this.status(); 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 { onEdit(): void {

View File

@@ -6,10 +6,10 @@ import {
ElementRef, ElementRef,
HostBinding, HostBinding,
Injector, Injector,
QueryList,
Renderer2, Renderer2,
input input
} from '@angular/core'; } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { TranslateService, TranslatePipe } from '@ngx-translate/core';
import _ from 'lodash'; import _ from 'lodash';
@@ -145,8 +145,8 @@ export class PiSelectCarouselComponent extends PiBaseSelectComponent implements
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.control.setValue(this.selectedOption); this.control.setValue(this.selectedOption);
merge( merge(
this.childrenOptionsComponents()?.changes || [], toObservable(this.childrenOptionsComponents, { injector: this.injector }),
this.viewChildOptionsComponents()?.changes || [], toObservable(this.viewChildOptionsComponents, { injector: this.injector }),
this.control.valueChanges this.control.valueChanges
).subscribe(() => { ).subscribe(() => {
Promise.resolve().then(() => { Promise.resolve().then(() => {
@@ -180,9 +180,9 @@ export class PiSelectCarouselComponent extends PiBaseSelectComponent implements
*/ */
private updateChildrenOptionsSelectedState(selectedOption: any): void { private updateChildrenOptionsSelectedState(selectedOption: any): void {
// Array of child option components to work with. // Array of child option components to work with.
let childrenOptionsComponent: Array<PiSelectOptionComponent>; let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// Reference to the child option elements for DOM manipulation. // Reference to the child option elements for DOM manipulation.
let childrenOptionsElementRef: QueryList<ElementRef>; let childrenOptionsElementRef: readonly ElementRef[];
// Check if we have child options that are content projected from a parent component. // Check if we have child options that are content projected from a parent component.
const childrenOptionsComponents = this.childrenOptionsComponents(); const childrenOptionsComponents = this.childrenOptionsComponents();
@@ -191,7 +191,7 @@ export class PiSelectCarouselComponent extends PiBaseSelectComponent implements
childrenOptionsElementRef = this.childrenOptionsEl(); childrenOptionsElementRef = this.childrenOptionsEl();
} else { } else {
// If not, it means they're internally defined (default), so we fetch from viewChildren. // If not, it means they're internally defined (default), so we fetch from viewChildren.
childrenOptionsComponent = this.viewChildOptionsComponents; childrenOptionsComponent = this.viewChildOptionsComponents();
childrenOptionsElementRef = this.viewChildOptionsEl(); childrenOptionsElementRef = this.viewChildOptionsEl();
} }
@@ -202,12 +202,12 @@ export class PiSelectCarouselComponent extends PiBaseSelectComponent implements
: false; : false;
// If the current child option matches the selected option, highlight it. // If the current child option matches the selected option, highlight it.
if (isSelected) { if (isSelected) {
this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-selected');
childOption.isSelected.set(true); childOption.isSelected.set(true);
} else { } else {
// Otherwise, ensure it is not highlighted. // Otherwise, ensure it is not highlighted.
this._renderer.removeClass( this._renderer.removeClass(
childrenOptionsElementRef.get(index)?.nativeElement, childrenOptionsElementRef[index]?.nativeElement,
'is-selected' 'is-selected'
); );
childOption.isSelected.set(false); childOption.isSelected.set(false);

View File

@@ -11,6 +11,7 @@ import {
Renderer2, Renderer2,
input input
} from '@angular/core'; } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { TranslateService, TranslatePipe } from '@ngx-translate/core';
import { merge } from 'rxjs'; import { merge } from 'rxjs';
@@ -164,8 +165,8 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.control.setValue(this.selectedOptions); this.control.setValue(this.selectedOptions);
merge( merge(
this.childrenOptionsComponents()?.changes || [], toObservable(this.childrenOptionsComponents, { injector: this.injector }),
this.viewChildOptionsComponents()?.changes || [], toObservable(this.viewChildOptionsComponents, { injector: this.injector }),
this.control.valueChanges this.control.valueChanges
).subscribe(() => { ).subscribe(() => {
Promise.resolve().then(() => { Promise.resolve().then(() => {
@@ -244,9 +245,9 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements
*/ */
private updateChildrenOptionsSelectedState(selectedOptions: any[]): void { private updateChildrenOptionsSelectedState(selectedOptions: any[]): void {
// Array of child option components to work with. // Array of child option components to work with.
let childrenOptionsComponent: Array<PiSelectOptionComponent>; let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// Reference to the child option elements for DOM manipulation. // Reference to the child option elements for DOM manipulation.
let childrenOptionsElementRef: QueryList<ElementRef>; let childrenOptionsElementRef: readonly ElementRef[];
// Check if we have child options that are content projected from a parent component. // Check if we have child options that are content projected from a parent component.
const childrenOptionsComponents = this.childrenOptionsComponents(); const childrenOptionsComponents = this.childrenOptionsComponents();
@@ -255,7 +256,7 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements
childrenOptionsElementRef = this.childrenOptionsEl(); childrenOptionsElementRef = this.childrenOptionsEl();
} else { } else {
// If not, it means they're internally defined (default), so we fetch from viewChildren. // If not, it means they're internally defined (default), so we fetch from viewChildren.
childrenOptionsComponent = this.viewChildOptionsComponents; childrenOptionsComponent = this.viewChildOptionsComponents();
childrenOptionsElementRef = this.viewChildOptionsEl(); childrenOptionsElementRef = this.viewChildOptionsEl();
} }
@@ -264,11 +265,11 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements
(selectedOption) => selectedOption[this.keyIdProperty()] === childOption['id'] (selectedOption) => selectedOption[this.keyIdProperty()] === childOption['id']
); );
if (isSelected) { if (isSelected) {
this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-selected');
childOption.isSelected.set(true); childOption.isSelected.set(true);
} else { } else {
this._renderer.removeClass( this._renderer.removeClass(
childrenOptionsElementRef.get(index)?.nativeElement, childrenOptionsElementRef[index]?.nativeElement,
'is-selected' 'is-selected'
); );
childOption.isSelected.set(false); 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. // Disable all options if the maximum number of selections has been reached.
const disableOthers = selectedOptions.length >= (this.maxLength() || Infinity); const disableOthers = selectedOptions.length >= (this.maxLength() || Infinity);
// Array of child option components to work with. // Array of child option components to work with.
let childrenOptionsComponent: Array<PiSelectOptionComponent>; let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// Reference to the child option elements for DOM manipulation. // Reference to the child option elements for DOM manipulation.
let childrenOptionsElementRef: QueryList<ElementRef>; let childrenOptionsElementRef: readonly ElementRef[];
// Check if we have child options that are content projected from a parent component. // Check if we have child options that are content projected from a parent component.
const childrenOptionsComponents = this.childrenOptionsComponents(); const childrenOptionsComponents = this.childrenOptionsComponents();
@@ -306,7 +307,7 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements
childrenOptionsElementRef = this.childrenOptionsEl(); childrenOptionsElementRef = this.childrenOptionsEl();
} else { } else {
// If not, it means they're internally defined (default), so we fetch from viewChildren. // If not, it means they're internally defined (default), so we fetch from viewChildren.
childrenOptionsComponent = this.viewChildOptionsComponents; childrenOptionsComponent = this.viewChildOptionsComponents();
childrenOptionsElementRef = this.viewChildOptionsEl(); childrenOptionsElementRef = this.viewChildOptionsEl();
} }
@@ -317,10 +318,10 @@ export class PiSelectMultipleComponent extends PiBaseSelectComponent implements
const isDisabled = childOption.isDisabled(); const isDisabled = childOption.isDisabled();
if ((!isSelected && disableOthers) || isDisabled) { if ((!isSelected && disableOthers) || isDisabled) {
this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-disabled'); this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-disabled');
} else { } else {
this._renderer.removeClass( this._renderer.removeClass(
childrenOptionsElementRef.get(index)?.nativeElement, childrenOptionsElementRef[index]?.nativeElement,
'is-disabled' 'is-disabled'
); );
} }

View File

@@ -7,10 +7,10 @@ import {
HostBinding, HostBinding,
HostListener, HostListener,
Injector, Injector,
QueryList,
Renderer2, Renderer2,
input input
} from '@angular/core'; } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { TranslateService, TranslatePipe } from '@ngx-translate/core';
import { merge } from 'rxjs'; import { merge } from 'rxjs';
@@ -161,8 +161,8 @@ export class PiSelectPeriodComponent extends PiBaseSelectComponent implements Af
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.control.setValue(this.selectedOption); this.control.setValue(this.selectedOption);
merge( merge(
this.childrenOptionsComponents()?.changes || [], toObservable(this.childrenOptionsComponents, { injector: this.injector }),
this.viewChildOptionsComponents()?.changes || [], toObservable(this.viewChildOptionsComponents, { injector: this.injector }),
this.control.valueChanges this.control.valueChanges
).subscribe(() => { ).subscribe(() => {
Promise.resolve().then(() => { Promise.resolve().then(() => {
@@ -192,9 +192,9 @@ export class PiSelectPeriodComponent extends PiBaseSelectComponent implements Af
*/ */
private updateChildrenOptionsSelectedState(selectedOption: any): void { private updateChildrenOptionsSelectedState(selectedOption: any): void {
// Array of child option components to work with. // Array of child option components to work with.
let childrenOptionsComponent: Array<PiSelectOptionComponent>; let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// Reference to the child option elements for DOM manipulation. // Reference to the child option elements for DOM manipulation.
let childrenOptionsElementRef: QueryList<ElementRef>; let childrenOptionsElementRef: readonly ElementRef[];
// Check if we have child options that are content projected from a parent component. // Check if we have child options that are content projected from a parent component.
const childrenOptionsComponents = this.childrenOptionsComponents(); const childrenOptionsComponents = this.childrenOptionsComponents();
@@ -203,7 +203,7 @@ export class PiSelectPeriodComponent extends PiBaseSelectComponent implements Af
childrenOptionsElementRef = this.childrenOptionsEl(); childrenOptionsElementRef = this.childrenOptionsEl();
} else { } else {
// If not, it means they're internally defined (default), so we fetch from viewChildren. // If not, it means they're internally defined (default), so we fetch from viewChildren.
childrenOptionsComponent = this.viewChildOptionsComponents; childrenOptionsComponent = this.viewChildOptionsComponents();
childrenOptionsElementRef = this.viewChildOptionsEl(); childrenOptionsElementRef = this.viewChildOptionsEl();
} }
@@ -214,12 +214,12 @@ export class PiSelectPeriodComponent extends PiBaseSelectComponent implements Af
: false; : false;
// If the current child option matches the selected option, highlight it. // If the current child option matches the selected option, highlight it.
if (isSelected) { if (isSelected) {
this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-selected');
childOption.isSelected.set(true); childOption.isSelected.set(true);
} else { } else {
// Otherwise, ensure it is not highlighted. // Otherwise, ensure it is not highlighted.
this._renderer.removeClass( this._renderer.removeClass(
childrenOptionsElementRef.get(index)?.nativeElement, childrenOptionsElementRef[index]?.nativeElement,
'is-selected' 'is-selected'
); );
childOption.isSelected.set(false); childOption.isSelected.set(false);

View File

@@ -1,4 +1,5 @@
import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, HostListener, Injector, QueryList, Renderer2, forwardRef, input } from '@angular/core'; 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 { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { TranslateService, TranslatePipe } from '@ngx-translate/core'; import { TranslateService, TranslatePipe } from '@ngx-translate/core';
import { merge } from 'rxjs'; import { merge } from 'rxjs';
@@ -151,8 +152,8 @@ export class PiSelectComponent extends PiBaseSelectComponent implements AfterCon
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.control.setValue(this.selectedOption); this.control.setValue(this.selectedOption);
merge( merge(
this.childrenOptionsComponents()?.changes || [], toObservable(this.childrenOptionsComponents, { injector: this.injector }),
this.viewChildOptionsComponents()?.changes || [], toObservable(this.viewChildOptionsComponents, { injector: this.injector }),
this.control.valueChanges this.control.valueChanges
).subscribe(() => { ).subscribe(() => {
Promise.resolve().then(() => { Promise.resolve().then(() => {
@@ -182,9 +183,9 @@ export class PiSelectComponent extends PiBaseSelectComponent implements AfterCon
*/ */
private updateChildrenOptionsSelectedState(selectedOption: any): void { private updateChildrenOptionsSelectedState(selectedOption: any): void {
// Array of child option components to work with. // Array of child option components to work with.
let childrenOptionsComponent: Array<PiSelectOptionComponent>; let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// Reference to the child option elements for DOM manipulation. // Reference to the child option elements for DOM manipulation.
let childrenOptionsElementRef: QueryList<ElementRef>; let childrenOptionsElementRef: readonly ElementRef[];
// Check if we have child options that are content projected from a parent component. // Check if we have child options that are content projected from a parent component.
const childrenOptionsComponents = this.childrenOptionsComponents(); const childrenOptionsComponents = this.childrenOptionsComponents();
@@ -193,7 +194,7 @@ export class PiSelectComponent extends PiBaseSelectComponent implements AfterCon
childrenOptionsElementRef = this.childrenOptionsEl(); childrenOptionsElementRef = this.childrenOptionsEl();
} else { } else {
// If not, it means they're internally defined (default), so we fetch from viewChildren. // If not, it means they're internally defined (default), so we fetch from viewChildren.
childrenOptionsComponent = this.viewChildOptionsComponents; childrenOptionsComponent = this.viewChildOptionsComponents();
childrenOptionsElementRef = this.viewChildOptionsEl(); childrenOptionsElementRef = this.viewChildOptionsEl();
} }
@@ -204,11 +205,11 @@ export class PiSelectComponent extends PiBaseSelectComponent implements AfterCon
: false; : false;
// If the current child option matches the selected option, highlight it. // If the current child option matches the selected option, highlight it.
if (isSelected) { if (isSelected) {
this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); this._renderer.addClass(childrenOptionsElementRef[index]?.nativeElement, 'is-selected');
childOption.isSelected.set(true); childOption.isSelected.set(true);
} else { // Otherwise, ensure it is not highlighted. } else { // Otherwise, ensure it is not highlighted.
this._renderer.removeClass( this._renderer.removeClass(
childrenOptionsElementRef.get(index)?.nativeElement, childrenOptionsElementRef[index]?.nativeElement,
'is-selected' 'is-selected'
); );
childOption.isSelected.set(false); childOption.isSelected.set(false);

View File

@@ -1,14 +1,13 @@
import { import {
AfterViewChecked, AfterViewChecked,
AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
HostBinding, HostBinding,
Input,
OnDestroy, OnDestroy,
Renderer2, Renderer2,
effect,
input, input,
output, output,
contentChildren, contentChildren,
@@ -37,7 +36,7 @@ import { TranslatePipe } from '@ngx-translate/core';
TranslatePipe, TranslatePipe,
], ],
}) })
export class PiTabComponent implements AfterViewInit, AfterViewChecked, OnDestroy { export class PiTabComponent implements AfterViewChecked, OnDestroy {
@HostBinding('class') get classes(): string { @HostBinding('class') get classes(): string {
const appearanceClass = `pi-tab__appearance--${this.appearance()}`; const appearanceClass = `pi-tab__appearance--${this.appearance()}`;
const hideTabsClass = this.hideTabs() ? 'hide-tabs' : ''; const hideTabsClass = this.hideTabs() ? 'hide-tabs' : '';
@@ -106,11 +105,9 @@ export class PiTabComponent implements AfterViewInit, AfterViewChecked, OnDestro
return isTabWrapperOverflowing; return isTabWrapperOverflowing;
} }
ngAfterViewInit(): void { // El ngAfterViewInit original sólo suscribía a tabContentItems.changes para
this.tabContentItems().changes.subscribe(() => { // hacer markForCheck(); como ahora es una signal query, Angular ya reevalúa
this.cdr.markForCheck(); // la plantilla cuando cambia.
});
}
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
this.cdr.detectChanges(); this.cdr.detectChanges();

View File

@@ -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]', }) @Directive({ selector: '[pi-text-centered]', })
export class PiTextCenteredDirective { export class PiTextCenteredDirective {
@@ -14,17 +14,13 @@ export class PiTextGroupDirective {
} }
} }
@Directive({ selector: '[pi-disabled]', }) @Directive({ selector: '[pi-disabled]', })
export class PiDisabledDirective implements OnChanges { export class PiDisabledDirective {
@HostBinding('style.opacity') get opacity(): number { @HostBinding('style.opacity') get opacity(): number {
return this.isDisabled() ? 0.2 : 1; 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<boolean>(); readonly isDisabled = input<boolean>();
ngOnChanges(changes: SimpleChanges): void {
if (changes) {
this.isDisabled = changes['isDisabled'].currentValue;
}
}
} }
@Directive({ selector: '[pi-covered]', }) @Directive({ selector: '[pi-covered]', })

View File

@@ -1,49 +1,36 @@
import { Directive, OnChanges, HostBinding, SimpleChanges, input } from '@angular/core'; import { Directive, HostBinding, input } from '@angular/core';
@Directive({ selector: '[pi-disabled]', }) /**
export class PiDisabledDirective implements OnChanges { * Los `ngOnChanges` originales sólo reasignaban a la entrada su propio
@HostBinding('style.opacity') get opacity(): number { * `currentValue`, es decir, eran no-ops. Con signal inputs sobran.
return this.isDisabled() ? 0.33 : 1; */
}
@HostBinding('class') get classes(): string | boolean { @Directive({ selector: '[pi-disabled]', })
return [this.isDisabled() && 'is-disabled'].join(' '); export class PiDisabledDirective {
} @HostBinding('style.opacity') get opacity(): number {
return this.isDisabled() ? 0.33 : 1;
readonly isDisabled = input<boolean>(); }
@HostBinding('class') get classes(): string | boolean {
ngOnChanges(changes: SimpleChanges): void { return [this.isDisabled() && 'is-disabled'].join(' ');
if (changes) { }
this.isDisabled = changes['isDisabled'].currentValue;
} readonly isDisabled = input<boolean>();
} }
}
@Directive({ selector: '[is-active]', })
@Directive({ selector: '[is-active]', }) export class PiIsActiveDirective {
export class PiIsActiveDirective { @HostBinding('class') get classes(): string | boolean {
@HostBinding('class') get classes(): string | boolean { return [this.isActive() ? 'is-active' : '', 'pi-status'].join(' ');
return [this.isActive() ? 'is-active' : '', 'pi-status'].join(' '); }
}
readonly isActive = input.required<boolean>();
readonly isActive = input.required<boolean>(); }
ngOnChanges(changes: SimpleChanges): void { @Directive({ selector: '[is-visited]', })
if (changes) { export class PiIsVisitedDirective {
this.isActive = changes['isActive'].currentValue; @HostBinding('class') get classes(): string | boolean {
} return [this.isVisited() ? 'is-visited' : '', 'pi-status'].join(' ');
} }
}
readonly isVisited = input.required<boolean>();
@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<boolean>();
ngOnChanges(changes: SimpleChanges): void {
if (changes) {
this.isVisited = changes['isVisited'].currentValue;
}
}
}