diff --git a/src/cdk/accordion/accordion-item.ts b/src/cdk/accordion/accordion-item.ts index 9c1a856b24c0..4ab73055143a 100644 --- a/src/cdk/accordion/accordion-item.ts +++ b/src/cdk/accordion/accordion-item.ts @@ -7,21 +7,23 @@ */ import { - Output, + effect, + model, + ModelSignal, + DestroyRef, Directive, - EventEmitter, - Input, OnDestroy, - ChangeDetectorRef, - booleanAttribute, + output, + OutputEmitterRef, inject, + input, + InputSignal, OnInit, } from '@angular/core'; +import { outputToObservable, takeUntilDestroyed } from "@angular/core/rxjs-interop"; import {_IdGenerator} from '@angular/cdk/a11y'; import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; import {CDK_ACCORDION, CdkAccordion} from './accordion'; -import {Subscription} from 'rxjs'; - /** * A basic directive expected to be extended and decorated as a component. Sets up all * events and attributes needed to be managed by a CdkAccordion parent. @@ -36,125 +38,85 @@ import {Subscription} from 'rxjs'; ], }) export class CdkAccordionItem implements OnInit, OnDestroy { - accordion = inject(CDK_ACCORDION, {optional: true, skipSelf: true})!; - private _changeDetectorRef = inject(ChangeDetectorRef); - protected _expansionDispatcher = inject(UniqueSelectionDispatcher); + private readonly _destroyRef: DestroyRef = inject(DestroyRef); + private readonly _accordion: CdkAccordion | null = inject(CDK_ACCORDION, {optional: true, skipSelf: true})!; + private readonly _expansionDispatcher: UniqueSelectionDispatcher = inject(UniqueSelectionDispatcher); + /** The unique AccordionItem id. */ + readonly id: string = inject(_IdGenerator).getId('cdk-accordion-child-'); + + /** Unregister function for _expansionDispatcher. */ + private _removeUniqueSelectionListener: () => void = () => {}; - /** Subscription to openAll/closeAll events. */ - private _openCloseAllSubscription = Subscription.EMPTY; /** Event emitted every time the AccordionItem is closed. */ - @Output() readonly closed: EventEmitter = new EventEmitter(); + readonly closed: OutputEmitterRef = output(); /** Event emitted every time the AccordionItem is opened. */ - @Output() readonly opened: EventEmitter = new EventEmitter(); + readonly opened: OutputEmitterRef = output(); /** Event emitted when the AccordionItem is destroyed. */ - @Output() readonly destroyed: EventEmitter = new EventEmitter(); - - /** - * Emits whenever the expanded state of the accordion changes. - * Primarily used to facilitate two-way binding. - * @docs-private - */ - @Output() readonly expandedChange: EventEmitter = new EventEmitter(); - - /** The unique AccordionItem id. */ - readonly id: string = inject(_IdGenerator).getId('cdk-accordion-child-'); + readonly destroyed: OutputEmitterRef = output(); /** Whether the AccordionItem is expanded. */ - @Input({transform: booleanAttribute}) - get expanded(): boolean { - return this._expanded; - } - set expanded(expanded: boolean) { - // Only emit events and update the internal value if the value changes. - if (this._expanded !== expanded) { - this._expanded = expanded; - this.expandedChange.emit(expanded); + readonly expanded: ModelSignal = model(false); - if (expanded) { + /** Whether the AccordionItem is disabled. */ + readonly disabled: InputSignal = input(false); + + constructor(...args: unknown[]); + constructor() { + effect(() => { + if (this.expanded()) { this.opened.emit(); - /** - * In the unique selection dispatcher, the id parameter is the id of the CdkAccordionItem, - * the name value is the id of the accordion. - */ - const accordionId = this.accordion ? this.accordion.id : this.id; + const accordionId: string = this._accordion ? this._accordion.id : this.id; this._expansionDispatcher.notify(this.id, accordionId); } else { this.closed.emit(); } - - // Ensures that the animation will run when the value is set outside of an `@Input`. - // This includes cases like the open, close and toggle methods. - this._changeDetectorRef.markForCheck(); - } + }, { allowSignalWrites: true }); } - private _expanded = false; - - /** Whether the AccordionItem is disabled. */ - @Input({transform: booleanAttribute}) disabled: boolean = false; - - /** Unregister function for _expansionDispatcher. */ - private _removeUniqueSelectionListener: () => void = () => {}; - - constructor(...args: unknown[]); - constructor() {} ngOnInit() { - this._removeUniqueSelectionListener = this._expansionDispatcher.listen( - (id: string, accordionId: string) => { - if ( - this.accordion && - !this.accordion.multi && - this.accordion.id === accordionId && - this.id !== id - ) { - this.expanded = false; - } - }, - ); - - // When an accordion item is hosted in an accordion, subscribe to open/close events. - if (this.accordion) { - this._openCloseAllSubscription = this._subscribeToOpenCloseAllActions(); + if (this._accordion) { + outputToObservable(this._accordion?.openCloseAllActions) + .pipe(takeUntilDestroyed(this._destroyRef)) + .subscribe(expanded => { + if (!this.disabled()) { + this.expanded.set(expanded); + } + }); + + this._removeUniqueSelectionListener = this._expansionDispatcher + .listen((id: string, accordionId: string) => { + if (this._accordion && !this._accordion.multi() && this._accordion.id === accordionId && this.id !== id) { + this.expanded.set(false); + } + }, + ); } - } - /** Emits an event for the accordion item being destroyed. */ - ngOnDestroy() { - this.opened.complete(); - this.closed.complete(); - this.destroyed.emit(); - this.destroyed.complete(); - this._removeUniqueSelectionListener(); - this._openCloseAllSubscription.unsubscribe(); - } + /** Emits an event for the accordion item being destroyed. */ + ngOnDestroy() { + this.destroyed.emit(); + this._removeUniqueSelectionListener(); + } - /** Toggles the expanded state of the accordion item. */ - toggle(): void { - if (!this.disabled) { - this.expanded = !this.expanded; + /** Toggles the expanded state of the accordion item. */ + toggle(): void { + if (!this.disabled()) { + this.expanded.update((prev: boolean) => !prev); } } - /** Sets the expanded state of the accordion item to false. */ - close(): void { - if (!this.disabled) { - this.expanded = false; + /** Sets the expanded state of the accordion item to false. */ + close(): void { + if (!this.disabled()) { + this.expanded.set(false); } } - /** Sets the expanded state of the accordion item to true. */ - open(): void { - if (!this.disabled) { - this.expanded = true; + /** Sets the expanded state of the accordion item to true. */ + open(): void { + if (!this.disabled()) { + this.expanded.set(true); } } - - private _subscribeToOpenCloseAllActions(): Subscription { - return this.accordion._openCloseAllActions.subscribe(expanded => { - // Only change expanded state if item is enabled - if (!this.disabled) { - this.expanded = expanded; - } - }); } -} + diff --git a/src/cdk/accordion/accordion.spec.ts b/src/cdk/accordion/accordion.spec.ts index f0e137a50b7f..5c2c48d425f0 100644 --- a/src/cdk/accordion/accordion.spec.ts +++ b/src/cdk/accordion/accordion.spec.ts @@ -93,20 +93,6 @@ describe('CdkAccordion', () => { expect(item.expanded).toBe(false); }); - - it('should complete the accordion observables on destroy', () => { - const fixture = TestBed.createComponent(SetOfItems); - fixture.detectChanges(); - const stateSpy = jasmine.createSpy('stateChanges complete spy'); - const openCloseSpy = jasmine.createSpy('openCloseAllActions complete spy'); - - fixture.componentInstance.accordion._stateChanges.subscribe({complete: stateSpy}); - fixture.componentInstance.accordion._openCloseAllActions.subscribe({complete: openCloseSpy}); - fixture.destroy(); - - expect(stateSpy).toHaveBeenCalled(); - expect(openCloseSpy).toHaveBeenCalled(); - }); }); @Component({ diff --git a/src/cdk/accordion/accordion.ts b/src/cdk/accordion/accordion.ts index b110380f67c8..0da528be8e87 100644 --- a/src/cdk/accordion/accordion.ts +++ b/src/cdk/accordion/accordion.ts @@ -9,15 +9,10 @@ import { Directive, InjectionToken, - Input, - OnChanges, - OnDestroy, - SimpleChanges, - booleanAttribute, inject, + output, OutputEmitterRef, input, InputSignal, } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; -import {Subject} from 'rxjs'; /** * Injection token that can be used to reference instances of `CdkAccordion`. It serves @@ -34,37 +29,25 @@ export const CDK_ACCORDION = new InjectionToken('CdkAccordion'); exportAs: 'cdkAccordion', providers: [{provide: CDK_ACCORDION, useExisting: CdkAccordion}], }) -export class CdkAccordion implements OnDestroy, OnChanges { - /** Emits when the state of the accordion changes */ - readonly _stateChanges = new Subject(); - - /** Stream that emits true/false when openAll/closeAll is triggered. */ - readonly _openCloseAllActions: Subject = new Subject(); +export class CdkAccordion { + /** Output that emits true/false when openAll/closeAll is triggered. */ + readonly openCloseAllActions: OutputEmitterRef = output() /** A readonly id value to use for unique selection coordination. */ readonly id: string = inject(_IdGenerator).getId('cdk-accordion-'); /** Whether the accordion should allow multiple expanded accordion items simultaneously. */ - @Input({transform: booleanAttribute}) multi: boolean = false; + readonly multi: InputSignal = input(false, { alias: 'multi' }); /** Opens all enabled accordion items in an accordion where multi is enabled. */ openAll(): void { - if (this.multi) { - this._openCloseAllActions.next(true); + if (this.multi()) { + this.openCloseAllActions.emit(true); } } /** Closes all enabled accordion items. */ closeAll(): void { - this._openCloseAllActions.next(false); - } - - ngOnChanges(changes: SimpleChanges) { - this._stateChanges.next(changes); - } - - ngOnDestroy() { - this._stateChanges.complete(); - this._openCloseAllActions.complete(); + this.openCloseAllActions.emit(false); } }