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,13 +1,9 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostBinding,
OnChanges,
SimpleChanges,
input,
output,
model,
contentChildren
} from '@angular/core';
import { padding, styleType, textColor } from '../../../styles';
@@ -37,7 +33,7 @@ import { TranslatePipe } from '@ngx-translate/core';
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();
@@ -50,7 +46,15 @@ export class PiAccordionComponent implements OnChanges, AfterContentInit {
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 color = input<styleType>('white');
readonly direction = input<'default' | 'horizontal'>('default');
@@ -59,37 +63,15 @@ export class PiAccordionComponent implements OnChanges, AfterContentInit {
readonly hasChevron = input(true);
readonly expandedItemIndexChange = output<number>();
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;

View File

@@ -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');
}
}

View File

@@ -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 {

View File

@@ -1,4 +1,4 @@
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 { NgIf, NgFor } from '@angular/common';
import { PiIconComponent } from '../../icon/icon/icon.component';
@@ -19,7 +19,7 @@ import { PiIconComponent } from '../../icon/icon/icon.component';
})
export class PiPaginatorComponent implements OnInit {
@HostBinding('class') className = 'pi-paginator';
readonly currentPage = input(1);
readonly currentPage = model(1);
readonly pageSize = input.required<number>();
readonly totalPages = input.required<number>();
readonly isDisabled = input.required<boolean>();
@@ -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());
}
}

View File

@@ -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<string>();
readonly subtitle = input<string>();
readonly formGroup = input.required<FormGroup>();
@@ -72,16 +73,16 @@ export class PiExpansionPanelComponent extends PiBasePanelComponent implements O
readonly expandableContentRef = contentChild<TemplateRef<unknown>>('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 {

View File

@@ -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<PiSelectOptionComponent>;
let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// 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.
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);

View File

@@ -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<PiSelectOptionComponent>;
let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// 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.
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<PiSelectOptionComponent>;
let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// 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.
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'
);
}

View File

@@ -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<PiSelectOptionComponent>;
let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// 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.
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);

View File

@@ -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<PiSelectOptionComponent>;
let childrenOptionsComponent: readonly PiSelectOptionComponent[];
// 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.
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);

View File

@@ -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();

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]', })
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<boolean>();
ngOnChanges(changes: SimpleChanges): void {
if (changes) {
this.isDisabled = changes['isDisabled'].currentValue;
}
}
}
@Directive({ selector: '[pi-covered]', })

View File

@@ -1,7 +1,12 @@
import { Directive, OnChanges, HostBinding, SimpleChanges, input } from '@angular/core';
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 implements OnChanges {
export class PiDisabledDirective {
@HostBinding('style.opacity') get opacity(): number {
return this.isDisabled() ? 0.33 : 1;
}
@@ -10,12 +15,6 @@ export class PiDisabledDirective implements OnChanges {
}
readonly isDisabled = input<boolean>();
ngOnChanges(changes: SimpleChanges): void {
if (changes) {
this.isDisabled = changes['isDisabled'].currentValue;
}
}
}
@Directive({ selector: '[is-active]', })
@@ -25,12 +24,6 @@ export class PiIsActiveDirective {
}
readonly isActive = input.required<boolean>();
ngOnChanges(changes: SimpleChanges): void {
if (changes) {
this.isActive = changes['isActive'].currentValue;
}
}
}
@Directive({ selector: '[is-visited]', })
@@ -40,10 +33,4 @@ export class PiIsVisitedDirective {
}
readonly isVisited = input.required<boolean>();
ngOnChanges(changes: SimpleChanges): void {
if (changes) {
this.isVisited = changes['isVisited'].currentValue;
}
}
}