Skip to content

turns accordion class into signals #30330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 64 additions & 102 deletions src/cdk/accordion/accordion-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -36,125 +38,85 @@ import {Subscription} from 'rxjs';
],
})
export class CdkAccordionItem implements OnInit, OnDestroy {
accordion = inject<CdkAccordion>(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<CdkAccordion>(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<void> = new EventEmitter<void>();
readonly closed: OutputEmitterRef<void> = output<void>();
/** Event emitted every time the AccordionItem is opened. */
@Output() readonly opened: EventEmitter<void> = new EventEmitter<void>();
readonly opened: OutputEmitterRef<void> = output<void>();
/** Event emitted when the AccordionItem is destroyed. */
@Output() readonly destroyed: EventEmitter<void> = new EventEmitter<void>();

/**
* Emits whenever the expanded state of the accordion changes.
* Primarily used to facilitate two-way binding.
* @docs-private
*/
@Output() readonly expandedChange: EventEmitter<boolean> = new EventEmitter<boolean>();

/** The unique AccordionItem id. */
readonly id: string = inject(_IdGenerator).getId('cdk-accordion-child-');
readonly destroyed: OutputEmitterRef<void> = output<void>();

/** 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<boolean> = model<boolean>(false);

if (expanded) {
/** Whether the AccordionItem is disabled. */
readonly disabled: InputSignal<boolean> = input<boolean>(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;
}
});
}
}

14 changes: 0 additions & 14 deletions src/cdk/accordion/accordion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
33 changes: 8 additions & 25 deletions src/cdk/accordion/accordion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,37 +29,25 @@ export const CDK_ACCORDION = new InjectionToken<CdkAccordion>('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<SimpleChanges>();

/** Stream that emits true/false when openAll/closeAll is triggered. */
readonly _openCloseAllActions: Subject<boolean> = new Subject<boolean>();
export class CdkAccordion {
/** Output that emits true/false when openAll/closeAll is triggered. */
readonly openCloseAllActions: OutputEmitterRef<boolean> = output<boolean>()

/** 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<boolean> = input<boolean>(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);
}
}
Loading