diff --git a/src/material/autocomplete/autocomplete.ts b/src/material/autocomplete/autocomplete.ts index 82b5be53389e..0383324e17e1 100644 --- a/src/material/autocomplete/autocomplete.ts +++ b/src/material/autocomplete/autocomplete.ts @@ -12,6 +12,7 @@ import { ChangeDetectorRef, Component, ContentChildren, + DestroyRef, ElementRef, EventEmitter, InjectionToken, @@ -35,7 +36,7 @@ import { } from '../core'; import {_IdGenerator, ActiveDescendantKeyManager} from '@angular/cdk/a11y'; import {Platform} from '@angular/cdk/platform'; -import {Subscription} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Event object that is emitted when an autocomplete option is selected. */ export class MatAutocompleteSelectedEvent { @@ -116,7 +117,7 @@ export class MatAutocomplete implements AfterContentInit, OnDestroy { private _elementRef = inject>(ElementRef); protected _defaults = inject(MAT_AUTOCOMPLETE_DEFAULT_OPTIONS); protected _animationsDisabled = _animationsDisabled(); - private _activeOptionChanges = Subscription.EMPTY; + private readonly _destroyRef = inject(DestroyRef); /** Manages active item in option list based on key events. */ _keyManager: ActiveDescendantKeyManager; @@ -266,7 +267,7 @@ export class MatAutocomplete implements AfterContentInit, OnDestroy { this._keyManager = new ActiveDescendantKeyManager(this.options) .withWrap() .skipPredicate(this._skipPredicate); - this._activeOptionChanges = this._keyManager.change.subscribe(index => { + this._keyManager.change.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(index => { if (this.isOpen) { this.optionActivated.emit({source: this, option: this.options.toArray()[index] || null}); } @@ -278,7 +279,6 @@ export class MatAutocomplete implements AfterContentInit, OnDestroy { ngOnDestroy() { this._keyManager?.destroy(); - this._activeOptionChanges.unsubscribe(); } /** diff --git a/src/material/bottom-sheet/bottom-sheet-container.ts b/src/material/bottom-sheet/bottom-sheet-container.ts index 66dddd36b02c..ad440ea66698 100644 --- a/src/material/bottom-sheet/bottom-sheet-container.ts +++ b/src/material/bottom-sheet/bottom-sheet-container.ts @@ -16,9 +16,9 @@ import { ViewEncapsulation, inject, } from '@angular/core'; -import {Subscription} from 'rxjs'; import {CdkPortalOutlet} from '@angular/cdk/portal'; import {_animationsDisabled} from '../core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; const ENTER_ANIMATION = '_mat-bottom-sheet-enter'; const EXIT_ANIMATION = '_mat-bottom-sheet-exit'; @@ -53,7 +53,6 @@ const EXIT_ANIMATION = '_mat-bottom-sheet-exit'; imports: [CdkPortalOutlet], }) export class MatBottomSheetContainer extends CdkDialogContainer implements OnDestroy { - private _breakpointSubscription: Subscription; protected _animationsDisabled = _animationsDisabled(); /** The state of the bottom sheet animations. */ @@ -75,8 +74,9 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes const breakpointObserver = inject(BreakpointObserver); - this._breakpointSubscription = breakpointObserver + breakpointObserver .observe([Breakpoints.Medium, Breakpoints.Large, Breakpoints.XLarge]) + .pipe(takeUntilDestroyed()) .subscribe(() => { const classList = (this._elementRef.nativeElement as HTMLElement).classList; @@ -121,7 +121,6 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes override ngOnDestroy() { super.ngOnDestroy(); - this._breakpointSubscription.unsubscribe(); this._destroyed = true; } diff --git a/src/material/chips/chip-grid.ts b/src/material/chips/chip-grid.ts index 24763b6c5a72..c7830056c8f7 100644 --- a/src/material/chips/chip-grid.ts +++ b/src/material/chips/chip-grid.ts @@ -34,11 +34,11 @@ import { import {_ErrorStateTracker, ErrorStateMatcher} from '../core'; import {MatFormFieldControl} from '../form-field'; import {merge, Observable, Subject} from 'rxjs'; -import {takeUntil} from 'rxjs/operators'; import {MatChipEvent} from './chip'; import {MatChipRow} from './chip-row'; import {MatChipSet} from './chip-set'; import {MatChipTextControl} from './chip-text-control'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Change event object that is emitted when the chip grid value has changed. */ export class MatChipGridChange { @@ -277,13 +277,13 @@ export class MatChipGrid } ngAfterContentInit() { - this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => { + this.chipBlurChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => { this._blur(); this.stateChanges.next(); }); merge(this.chipFocusChanges, this._chips.changes) - .pipe(takeUntil(this._destroyed)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(() => this.stateChanges.next()); } diff --git a/src/material/chips/chip-listbox.ts b/src/material/chips/chip-listbox.ts index 3547fcc73222..b9440e15955e 100644 --- a/src/material/chips/chip-listbox.ts +++ b/src/material/chips/chip-listbox.ts @@ -16,20 +16,20 @@ import { forwardRef, inject, Input, - OnDestroy, Output, QueryList, ViewEncapsulation, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {Observable} from 'rxjs'; -import {startWith, takeUntil} from 'rxjs/operators'; +import {startWith} from 'rxjs/operators'; import {TAB} from '@angular/cdk/keycodes'; import {MatChip, MatChipEvent} from './chip'; import {MatChipOption, MatChipSelectionChange} from './chip-option'; import {MatChipSet} from './chip-set'; import {MatChipAction} from './chip-action'; import {MAT_CHIPS_DEFAULT_OPTIONS} from './tokens'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Change event object that is emitted when the chip listbox value has changed. */ export class MatChipListboxChange { @@ -82,10 +82,7 @@ export const MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR: any = { encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MatChipListbox - extends MatChipSet - implements AfterContentInit, OnDestroy, ControlValueAccessor -{ +export class MatChipListbox extends MatChipSet implements AfterContentInit, ControlValueAccessor { /** * Function when touched. Set as part of ControlValueAccessor implementation. * @docs-private @@ -199,18 +196,20 @@ export class MatChipListbox override _chips: QueryList = undefined!; ngAfterContentInit() { - this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { - if (this.value !== undefined) { - Promise.resolve().then(() => { - this._setSelectionByValue(this.value, false); - }); - } - // Update listbox selectable/multiple properties on chips - this._syncListboxProperties(); - }); + this._chips.changes + .pipe(startWith(null), takeUntilDestroyed(this._destroyRef)) + .subscribe(() => { + if (this.value !== undefined) { + Promise.resolve().then(() => { + this._setSelectionByValue(this.value, false); + }); + } + // Update listbox selectable/multiple properties on chips + this._syncListboxProperties(); + }); - this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => this._blur()); - this.chipSelectionChanges.pipe(takeUntil(this._destroyed)).subscribe(event => { + this.chipBlurChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => this._blur()); + this.chipSelectionChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(event => { if (!this.multiple) { this._chips.forEach(chip => { if (chip !== event.source) { diff --git a/src/material/chips/chip-set.ts b/src/material/chips/chip-set.ts index 6e9f87ab42bc..5790832ced95 100644 --- a/src/material/chips/chip-set.ts +++ b/src/material/chips/chip-set.ts @@ -22,11 +22,13 @@ import { booleanAttribute, numberAttribute, inject, + DestroyRef, } from '@angular/core'; -import {Observable, Subject, merge} from 'rxjs'; -import {startWith, switchMap, takeUntil} from 'rxjs/operators'; +import {Observable, merge} from 'rxjs'; +import {startWith, switchMap} from 'rxjs/operators'; import {MatChip, MatChipEvent} from './chip'; import {MatChipAction, MatChipContent} from './chip-action'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Basic container component for the MatChip component. @@ -53,6 +55,7 @@ export class MatChipSet implements AfterViewInit, OnDestroy { protected _elementRef = inject>(ElementRef); protected _changeDetectorRef = inject(ChangeDetectorRef); private _dir = inject(Directionality, {optional: true}); + protected readonly _destroyRef = inject(DestroyRef); /** Index of the last destroyed chip that had focus. */ private _lastDestroyedFocusedChipIndex: number | null = null; @@ -60,9 +63,6 @@ export class MatChipSet implements AfterViewInit, OnDestroy { /** Used to manage focus within the chip list. */ protected _keyManager: FocusKeyManager; - /** Subject that emits when the component has been destroyed. */ - protected _destroyed = new Subject(); - /** Role to use if it hasn't been overwritten by the user. */ protected _defaultRole = 'presentation'; @@ -146,8 +146,6 @@ export class MatChipSet implements AfterViewInit, OnDestroy { ngOnDestroy() { this._keyManager?.destroy(); this._chipActions.destroy(); - this._destroyed.next(); - this._destroyed.complete(); } /** Checks whether any of the chips is focused. */ @@ -249,7 +247,7 @@ export class MatChipSet implements AfterViewInit, OnDestroy { // Keep the manager active index in sync so that navigation picks // up from the current chip if the user clicks into the list directly. - this.chipFocusChanges.pipe(takeUntil(this._destroyed)).subscribe(({chip}) => { + this.chipFocusChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(({chip}) => { const action = chip._getSourceAction(document.activeElement as Element); if (action) { @@ -258,7 +256,7 @@ export class MatChipSet implements AfterViewInit, OnDestroy { }); this._dir?.change - .pipe(takeUntil(this._destroyed)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(direction => this._keyManager.withHorizontalOrientation(direction)); } @@ -273,42 +271,46 @@ export class MatChipSet implements AfterViewInit, OnDestroy { /** Listens to changes in the chip set and syncs up the state of the individual chips. */ private _trackChipSetChanges() { - this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { - if (this.disabled) { - // Since this happens after the content has been - // checked, we need to defer it to the next tick. - Promise.resolve().then(() => this._syncChipsState()); - } + this._chips.changes + .pipe(startWith(null), takeUntilDestroyed(this._destroyRef)) + .subscribe(() => { + if (this.disabled) { + // Since this happens after the content has been + // checked, we need to defer it to the next tick. + Promise.resolve().then(() => this._syncChipsState()); + } - this._redirectDestroyedChipFocus(); - }); + this._redirectDestroyedChipFocus(); + }); } /** Starts tracking the destroyed chips in order to capture the focused one. */ private _trackDestroyedFocusedChip() { - this.chipDestroyedChanges.pipe(takeUntil(this._destroyed)).subscribe((event: MatChipEvent) => { - // If the focused chip is destroyed, save its index so that we can move focus to the next - // chip. We only save the index here, rather than move the focus immediately, because we want - // to wait until the chip is removed from the chip list before focusing the next one. This - // allows us to keep focus on the same index if the chip gets swapped out. - const chipArray = this._chips.toArray(); - const chipIndex = chipArray.indexOf(event.chip); - const hasFocus = event.chip._hasFocus(); - const wasLastFocused = - event.chip._hadFocusOnRemove && - this._keyManager.activeItem && - event.chip._getActions().includes(this._keyManager.activeItem); - - // Note that depending on the timing, the chip might've already lost focus by the - // time we check this. We need the `wasLastFocused` as a fallback to detect such cases. - // In `wasLastFocused` we also need to ensure that the chip actually had focus when it was - // deleted so that we don't steal away the user's focus after they've moved on from the chip. - const shouldMoveFocus = hasFocus || wasLastFocused; - - if (this._isValidIndex(chipIndex) && shouldMoveFocus) { - this._lastDestroyedFocusedChipIndex = chipIndex; - } - }); + this.chipDestroyedChanges + .pipe(takeUntilDestroyed(this._destroyRef)) + .subscribe((event: MatChipEvent) => { + // If the focused chip is destroyed, save its index so that we can move focus to the next + // chip. We only save the index here, rather than move the focus immediately, because we want + // to wait until the chip is removed from the chip list before focusing the next one. This + // allows us to keep focus on the same index if the chip gets swapped out. + const chipArray = this._chips.toArray(); + const chipIndex = chipArray.indexOf(event.chip); + const hasFocus = event.chip._hasFocus(); + const wasLastFocused = + event.chip._hadFocusOnRemove && + this._keyManager.activeItem && + event.chip._getActions().includes(this._keyManager.activeItem); + + // Note that depending on the timing, the chip might've already lost focus by the + // time we check this. We need the `wasLastFocused` as a fallback to detect such cases. + // In `wasLastFocused` we also need to ensure that the chip actually had focus when it was + // deleted so that we don't steal away the user's focus after they've moved on from the chip. + const shouldMoveFocus = hasFocus || wasLastFocused; + + if (this._isValidIndex(chipIndex) && shouldMoveFocus) { + this._lastDestroyedFocusedChipIndex = chipIndex; + } + }); } /** diff --git a/src/material/datepicker/calendar.ts b/src/material/datepicker/calendar.ts index 436070604eae..1424a404648a 100644 --- a/src/material/datepicker/calendar.ts +++ b/src/material/datepicker/calendar.ts @@ -26,7 +26,7 @@ import { inject, } from '@angular/core'; import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '../core'; -import {Subject, Subscription} from 'rxjs'; +import {Subject} from 'rxjs'; import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body'; import {createMissingDateImplError} from './datepicker-errors'; import {MatDatepickerIntl} from './datepicker-intl'; @@ -44,6 +44,7 @@ import {_IdGenerator, CdkMonitorFocus} from '@angular/cdk/a11y'; import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/private'; import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform'; import {MatTooltip} from '../tooltip'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Possible views for the calendar. @@ -270,8 +271,6 @@ export class MatCalendar implements AfterContentInit, AfterViewChecked, OnDes /** A portal containing the header component type for this calendar. */ _calendarHeaderPortal: Portal; - private _intlChanges: Subscription; - /** * Used for scheduling that focus should be moved to the active cell on the next tick. * We need to schedule it, rather than do it immediately, because we have to wait @@ -433,10 +432,12 @@ export class MatCalendar implements AfterContentInit, AfterViewChecked, OnDes } } - this._intlChanges = inject(MatDatepickerIntl).changes.subscribe(() => { - this._changeDetectorRef.markForCheck(); - this.stateChanges.next(); - }); + inject(MatDatepickerIntl) + .changes.pipe(takeUntilDestroyed()) + .subscribe(() => { + this._changeDetectorRef.markForCheck(); + this.stateChanges.next(); + }); } ngAfterContentInit() { @@ -455,7 +456,6 @@ export class MatCalendar implements AfterContentInit, AfterViewChecked, OnDes } ngOnDestroy() { - this._intlChanges.unsubscribe(); this.stateChanges.complete(); } diff --git a/src/material/expansion/expansion-panel-header.ts b/src/material/expansion/expansion-panel-header.ts index a9641381d280..c64fafcdb8bb 100644 --- a/src/material/expansion/expansion-panel-header.ts +++ b/src/material/expansion/expansion-panel-header.ts @@ -21,8 +21,9 @@ import { ViewEncapsulation, inject, HostAttributeToken, + DestroyRef, } from '@angular/core'; -import {EMPTY, merge, Subscription} from 'rxjs'; +import {EMPTY, merge} from 'rxjs'; import {filter} from 'rxjs/operators'; import {MatAccordionTogglePosition} from './accordion-base'; import { @@ -32,6 +33,7 @@ import { } from './expansion-panel'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {_StructuralStylesLoader} from '../core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Header element of a ``. @@ -63,8 +65,7 @@ export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, Focusa private _element = inject(ElementRef); private _focusMonitor = inject(FocusMonitor); private _changeDetectorRef = inject(ChangeDetectorRef); - - private _parentChangeSubscription = Subscription.EMPTY; + private readonly _destroyRef = inject(DestroyRef); constructor(...args: unknown[]); @@ -86,7 +87,7 @@ export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, Focusa // Since the toggle state depends on an @Input on the panel, we // need to subscribe and trigger change detection manually. - this._parentChangeSubscription = merge( + merge( panel.opened, panel.closed, accordionHideToggleChange, @@ -99,7 +100,10 @@ export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, Focusa // Avoids focus being lost if the panel contained the focused element and was closed. panel.closed - .pipe(filter(() => panel._containsFocus())) + .pipe( + filter(() => panel._containsFocus()), + takeUntilDestroyed(this._destroyRef), + ) .subscribe(() => this._focusMonitor.focusVia(this._element, 'program')); if (defaultOptions) { @@ -217,7 +221,6 @@ export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, Focusa } ngOnDestroy() { - this._parentChangeSubscription.unsubscribe(); this._focusMonitor.stopMonitoring(this._element); } } diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts index cc16af869f0c..7cff1a147494 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -19,6 +19,7 @@ import { Component, ContentChild, ContentChildren, + DestroyRef, ElementRef, InjectionToken, Input, @@ -36,8 +37,8 @@ import { viewChild, } from '@angular/core'; import {AbstractControlDirective, ValidatorFn} from '@angular/forms'; -import {Subject, Subscription, merge} from 'rxjs'; -import {filter, map, pairwise, startWith, takeUntil} from 'rxjs/operators'; +import {merge} from 'rxjs'; +import {filter, map, pairwise, startWith} from 'rxjs/operators'; import {ThemePalette, _animationsDisabled} from '../core'; import {MAT_ERROR, MatError} from './directives/error'; import { @@ -56,6 +57,7 @@ import { getMatFormFieldDuplicatedHintError, getMatFormFieldMissingControlError, } from './form-field-errors'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Type for the available floatLabel values. */ export type FloatLabelType = 'always' | 'auto'; @@ -191,6 +193,7 @@ export class MatFormField { _elementRef = inject(ElementRef); private _changeDetectorRef = inject(ChangeDetectorRef); + private readonly _destroyRef = inject(DestroyRef); private _platform = inject(Platform); private _idGenerator = inject(_IdGenerator); private _ngZone = inject(NgZone); @@ -332,14 +335,11 @@ export class MatFormField this._explicitFormFieldControl = value; } - private _destroyed = new Subject(); + // private _destroyed = new Subject(); private _isFocused: boolean | null = null; private _explicitFormFieldControl: MatFormFieldControl; private _previousControl: MatFormFieldControl | null = null; private _previousControlValidatorFn: ValidatorFn | null = null; - private _stateChanges: Subscription | undefined; - private _valueChanges: Subscription | undefined; - private _describedByChanges: Subscription | undefined; private _outlineLabelOffsetResizeObserver: ResizeObserver | null = null; protected readonly _animationsDisabled = _animationsDisabled(); @@ -422,11 +422,6 @@ export class MatFormField ngOnDestroy() { this._outlineLabelOffsetResizeObserver?.disconnect(); - this._stateChanges?.unsubscribe(); - this._valueChanges?.unsubscribe(); - this._describedByChanges?.unsubscribe(); - this._destroyed.next(); - this._destroyed.complete(); } /** @@ -471,15 +466,13 @@ export class MatFormField } // Subscribe to changes in the child control state in order to update the form field UI. - this._stateChanges?.unsubscribe(); - this._stateChanges = control.stateChanges.subscribe(() => { + control.stateChanges.subscribe(() => { this._updateFocusState(); this._changeDetectorRef.markForCheck(); }); // Updating the `aria-describedby` touches the DOM. Only do it if it actually needs to change. - this._describedByChanges?.unsubscribe(); - this._describedByChanges = control.stateChanges + control.stateChanges .pipe( startWith([undefined, undefined] as const), map(() => [control.errorState, control.userAriaDescribedBy] as const), @@ -490,12 +483,10 @@ export class MatFormField ) .subscribe(() => this._syncDescribedByIds()); - this._valueChanges?.unsubscribe(); - // Run change detection if the value changes. if (control.ngControl && control.ngControl.valueChanges) { - this._valueChanges = control.ngControl.valueChanges - .pipe(takeUntil(this._destroyed)) + control.ngControl.valueChanges + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(() => this._changeDetectorRef.markForCheck()); } } diff --git a/src/material/icon/icon.ts b/src/material/icon/icon.ts index 9544ea38eea4..408030c3edac 100644 --- a/src/material/icon/icon.ts +++ b/src/material/icon/icon.ts @@ -21,12 +21,13 @@ import { ViewEncapsulation, HostAttributeToken, DOCUMENT, + DestroyRef, } from '@angular/core'; import {ThemePalette} from '../core'; -import {Subscription} from 'rxjs'; import {take} from 'rxjs/operators'; import {MatIconRegistry} from './icon-registry'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Default options for `mat-icon`. */ export interface MatIconDefaultOptions { @@ -152,6 +153,7 @@ export class MatIcon implements OnInit, AfterViewChecked, OnDestroy { private _iconRegistry = inject(MatIconRegistry); private _location = inject(MAT_ICON_LOCATION); private readonly _errorHandler = inject(ErrorHandler); + private readonly _destroyRef = inject(DestroyRef); private _defaultColor: ThemePalette; /** @@ -236,9 +238,6 @@ export class MatIcon implements OnInit, AfterViewChecked, OnDestroy { /** Keeps track of the elements and attributes that we've prefixed with the current path. */ private _elementsWithExternalReferences?: Map; - /** Subscription to the current in-progress SVG icon request. */ - private _currentIconFetch = Subscription.EMPTY; - constructor(...args: unknown[]); constructor() { @@ -316,8 +315,6 @@ export class MatIcon implements OnInit, AfterViewChecked, OnDestroy { } ngOnDestroy() { - this._currentIconFetch.unsubscribe(); - if (this._elementsWithExternalReferences) { this._elementsWithExternalReferences.clear(); } @@ -449,7 +446,6 @@ export class MatIcon implements OnInit, AfterViewChecked, OnDestroy { private _updateSvgIcon(rawName: string | undefined) { this._svgNamespace = null; this._svgName = null; - this._currentIconFetch.unsubscribe(); if (rawName) { const [namespace, iconName] = this._splitIconName(rawName); @@ -462,9 +458,9 @@ export class MatIcon implements OnInit, AfterViewChecked, OnDestroy { this._svgName = iconName; } - this._currentIconFetch = this._iconRegistry + this._iconRegistry .getNamedSvgIcon(iconName, namespace) - .pipe(take(1)) + .pipe(take(1), takeUntilDestroyed(this._destroyRef)) .subscribe( svg => this._setSvgElement(svg), (err: Error) => { diff --git a/src/material/list/selection-list.ts b/src/material/list/selection-list.ts index 9a8d186c4cea..94b45347ed16 100644 --- a/src/material/list/selection-list.ts +++ b/src/material/list/selection-list.ts @@ -17,6 +17,7 @@ import { ChangeDetectorRef, Component, ContentChildren, + DestroyRef, ElementRef, EventEmitter, Input, @@ -34,10 +35,9 @@ import { } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {ThemePalette} from '../core'; -import {Subject} from 'rxjs'; -import {takeUntil} from 'rxjs/operators'; import {MatListBase} from './list-base'; import {MatListOption, SELECTION_LIST, SelectionList} from './list-option'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; export const MAT_SELECTION_LIST_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, @@ -81,14 +81,12 @@ export class MatSelectionList _element = inject>(ElementRef); private _ngZone = inject(NgZone); private _renderer = inject(Renderer2); + private readonly _destroyRef = inject(DestroyRef); private _initialized = false; private _keyManager: FocusKeyManager; private _listenerCleanups: (() => void)[] | undefined; - /** Emits when the list has been destroyed. */ - private _destroyed = new Subject(); - /** Whether the list has been destroyed. */ private _isDestroyed: boolean; @@ -207,8 +205,6 @@ export class MatSelectionList ngOnDestroy() { this._keyManager?.destroy(); this._listenerCleanups?.forEach(current => current()); - this._destroyed.next(); - this._destroyed.complete(); this._isDestroyed = true; } @@ -291,7 +287,7 @@ export class MatSelectionList /** Watches for changes in the selected state of the options and updates the list accordingly. */ private _watchForSelectionChange() { - this.selectedOptions.changed.pipe(takeUntil(this._destroyed)).subscribe(event => { + this.selectedOptions.changed.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(event => { // Sync external changes to the model back to the options. for (let item of event.added) { item.selected = true; @@ -445,7 +441,7 @@ export class MatSelectionList this._keyManager.change.subscribe(activeItemIndex => this._setActiveOption(activeItemIndex)); // If the active item is removed from the list, reset back to the first one. - this._items.changes.pipe(takeUntil(this._destroyed)).subscribe(() => { + this._items.changes.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => { const activeItem = this._keyManager.activeItem; if (!activeItem || this._items.toArray().indexOf(activeItem) === -1) { diff --git a/src/material/menu/context-menu-trigger.ts b/src/material/menu/context-menu-trigger.ts index 0d39ba7834ea..411a1a4c94b1 100644 --- a/src/material/menu/context-menu-trigger.ts +++ b/src/material/menu/context-menu-trigger.ts @@ -8,6 +8,7 @@ import { booleanAttribute, + DestroyRef, Directive, DOCUMENT, EventEmitter, @@ -24,11 +25,11 @@ import { ViewportRuler, } from '@angular/cdk/overlay'; import {_getEventTarget, _getShadowRoot} from '@angular/cdk/platform'; -import {Subscription} from 'rxjs'; import {skipWhile} from 'rxjs/operators'; import {MatMenuPanel} from './menu-panel'; import {_animationsDisabled} from '../core'; import {MenuCloseReason} from './menu'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Trigger that opens a menu whenever the user right-clicks within its host element. @@ -50,7 +51,7 @@ export class MatContextMenuTrigger extends MatMenuTriggerBase implements OnDestr private _document = inject(DOCUMENT); private _viewportRuler = inject(ViewportRuler); private _scrollDispatcher = inject(ScrollDispatcher); - private _scrollSubscription: Subscription | undefined; + private readonly _destroyRef = inject(DestroyRef); /** References the menu instance that the trigger is associated with. */ @Input({alias: 'matContextMenuTriggerFor', required: true}) @@ -90,7 +91,6 @@ export class MatContextMenuTrigger extends MatMenuTriggerBase implements OnDestr override ngOnDestroy(): void { super.ngOnDestroy(); - this._scrollSubscription?.unsubscribe(); } /** Handler for `contextmenu` events. */ @@ -110,7 +110,6 @@ export class MatContextMenuTrigger extends MatMenuTriggerBase implements OnDestr protected override _destroyMenu(reason: MenuCloseReason): void { super._destroyMenu(reason); - this._scrollSubscription?.unsubscribe(); } protected override _getOverlayOrigin() { @@ -177,16 +176,18 @@ export class MatContextMenuTrigger extends MatMenuTriggerBase implements OnDestr this._initializePoint(event.clientX, event.clientY); this._triggerPressedControl = event.ctrlKey; super._openMenu(true); - this._scrollSubscription?.unsubscribe(); - this._scrollSubscription = this._scrollDispatcher.scrolled(0).subscribe(() => { - // When passing a point to the connected position strategy, the position - // won't update as the user is scrolling so we have to do it manually. - const position = this._viewportRuler.getViewportScrollPosition(); - const point = this._point; - point.y = point.initialY + (point.initialScrollY - position.top); - point.x = point.initialX + (point.initialScrollX - position.left); - this._updatePosition(); - }); + this._scrollDispatcher + .scrolled(0) + .pipe(takeUntilDestroyed(this._destroyRef)) + .subscribe(() => { + // When passing a point to the connected position strategy, the position + // won't update as the user is scrolling so we have to do it manually. + const position = this._viewportRuler.getViewportScrollPosition(); + const point = this._point; + point.y = point.initialY + (point.initialScrollY - position.top); + point.x = point.initialX + (point.initialScrollX - position.left); + this._updatePosition(); + }); } /** Initializes the point representing the origin relative to which the menu will be rendered. */ diff --git a/src/material/menu/menu-content.ts b/src/material/menu/menu-content.ts index ef6a3e3683a6..db9c74c15234 100644 --- a/src/material/menu/menu-content.ts +++ b/src/material/menu/menu-content.ts @@ -20,7 +20,6 @@ import { inject, DOCUMENT, } from '@angular/core'; -import {Subject} from 'rxjs'; /** * Injection token that can be used to reference instances of `MatMenuContent`. It serves @@ -45,9 +44,6 @@ export class MatMenuContent implements OnDestroy { private _portal: TemplatePortal | undefined; private _outlet: DomPortalOutlet | undefined; - /** Emits when the menu content has been attached. */ - readonly _attached = new Subject(); - constructor(...args: unknown[]); constructor() {} @@ -85,7 +81,6 @@ export class MatMenuContent implements OnDestroy { // it needs to check for new menu items and update the `@ContentChild` in `MatMenu`. this._changeDetectorRef.markForCheck(); this._portal.attach(this._outlet, context); - this._attached.next(); } /** diff --git a/src/material/paginator/paginator.ts b/src/material/paginator/paginator.ts index 847d1c60a6c1..143a64ccd74d 100644 --- a/src/material/paginator/paginator.ts +++ b/src/material/paginator/paginator.ts @@ -27,8 +27,9 @@ import {MatSelect} from '../select'; import {MatIconButton} from '../button'; import {MatTooltip} from '../tooltip'; import {MatFormField, MatFormFieldAppearance} from '../form-field'; -import {Observable, ReplaySubject, Subscription} from 'rxjs'; +import {Observable, ReplaySubject} from 'rxjs'; import {MatPaginatorIntl} from './paginator-intl'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** The default page size if there is no page size and there are no provided page size options. */ const DEFAULT_PAGE_SIZE = 50; @@ -118,7 +119,6 @@ export class MatPaginator implements OnInit, OnDestroy { /** ID for the DOM node containing the paginator's items per page label. */ readonly _pageSizeLabelId = inject(_IdGenerator).getId('mat-paginator-page-size-label-'); - private _intlChanges: Subscription; private _isInitialized = false; private _initializedStream = new ReplaySubject(1); @@ -203,12 +203,13 @@ export class MatPaginator implements OnInit, OnDestroy { constructor(...args: unknown[]); constructor() { - const _intl = this._intl; const defaults = inject(MAT_PAGINATOR_DEFAULT_OPTIONS, { optional: true, }); - this._intlChanges = _intl.changes.subscribe(() => this._changeDetectorRef.markForCheck()); + this._intl.changes + .pipe(takeUntilDestroyed()) + .subscribe(() => this._changeDetectorRef.markForCheck()); if (defaults) { const {pageSize, pageSizeOptions, hidePageSize, showFirstLastButtons} = defaults; @@ -241,7 +242,6 @@ export class MatPaginator implements OnInit, OnDestroy { ngOnDestroy() { this._initializedStream.complete(); - this._intlChanges.unsubscribe(); } /** Advances to the next page if it exists. */ diff --git a/src/material/radio/radio.ts b/src/material/radio/radio.ts index 5b81743aaaf1..5b9216ed171b 100644 --- a/src/material/radio/radio.ts +++ b/src/material/radio/radio.ts @@ -36,6 +36,7 @@ import { numberAttribute, HostAttributeToken, Renderer2, + DestroyRef, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import { @@ -45,8 +46,8 @@ import { _StructuralStylesLoader, _animationsDisabled, } from '../core'; -import {Subscription} from 'rxjs'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Change event object emitted by radio button and radio group. */ export class MatRadioChange { @@ -116,8 +117,9 @@ export const MAT_RADIO_DEFAULT_OPTIONS = new InjectionToken void = () => {}; @@ -281,17 +280,13 @@ export class MatRadioGroup implements AfterContentInit, OnDestroy, ControlValueA // buttons depends on it. Note that we don't clear the `value`, because the radio button // may be swapped out with a similar one and there are some internal apps that depend on // that behavior. - this._buttonChanges = this._radios.changes.subscribe(() => { + this._radios.changes.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => { if (this.selected && !this._radios.find(radio => radio === this.selected)) { this._selected = null; } }); } - ngOnDestroy() { - this._buttonChanges?.unsubscribe(); - } - /** * Mark this group as being "touched" (for ngModel). Meant to be called by the contained * radio buttons upon their blur. diff --git a/src/material/select/select.ts b/src/material/select/select.ts index b05f7f8b6e3a..91f9890785bd 100644 --- a/src/material/select/select.ts +++ b/src/material/select/select.ts @@ -62,6 +62,7 @@ import { Renderer2, Injector, signal, + DestroyRef, } from '@angular/core'; import { AbstractControl, @@ -85,13 +86,14 @@ import { } from '../core'; import {MAT_FORM_FIELD, MatFormField, MatFormFieldControl} from '../form-field'; import {defer, merge, Observable, Subject} from 'rxjs'; -import {filter, map, startWith, switchMap, take, takeUntil} from 'rxjs/operators'; +import {filter, map, startWith, switchMap, take} from 'rxjs/operators'; import { getMatSelectDynamicMultipleError, getMatSelectNonArrayValueError, getMatSelectNonFunctionValueError, } from './select-errors'; import {NgClass} from '@angular/common'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Injection token that determines the scroll handling while a select is open. */ export const MAT_SELECT_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>( @@ -198,6 +200,7 @@ export class MatSelect ControlValueAccessor, MatFormFieldControl { + protected readonly _destroyRef = inject(DestroyRef); protected _viewportRuler = inject(ViewportRuler); protected _changeDetectorRef = inject(ChangeDetectorRef); readonly _elementRef = inject(ElementRef); @@ -314,9 +317,6 @@ export class MatSelect */ private _previousControl: AbstractControl | null | undefined; - /** Emits whenever the component is destroyed. */ - protected readonly _destroy = new Subject(); - /** Tracks the error state of the select. */ private _errorStateTracker: _ErrorStateTracker; @@ -633,7 +633,7 @@ export class MatSelect this.stateChanges.next(); this._viewportRuler .change() - .pipe(takeUntil(this._destroy)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(() => { if (this.panelOpen) { this._overlayWidth = this._getOverlayWidth(this._preferredOverlayOrigin); @@ -648,15 +648,17 @@ export class MatSelect this._initKeyManager(); - this._selectionModel.changed.pipe(takeUntil(this._destroy)).subscribe(event => { + this._selectionModel.changed.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(event => { event.added.forEach(option => option.select()); event.removed.forEach(option => option.deselect()); }); - this.options.changes.pipe(startWith(null), takeUntil(this._destroy)).subscribe(() => { - this._resetOptions(); - this._initializeSelection(); - }); + this.options.changes + .pipe(startWith(null), takeUntilDestroyed(this._destroyRef)) + .subscribe(() => { + this._resetOptions(); + this._initializeSelection(); + }); } ngDoCheck() { @@ -709,8 +711,6 @@ export class MatSelect ngOnDestroy() { this._cleanupDetach?.(); this._keyManager?.destroy(); - this._destroy.next(); - this._destroy.complete(); this.stateChanges.complete(); this._clearFromModal(); } @@ -1267,9 +1267,8 @@ export class MatSelect /** Drops current option subscriptions and IDs and resets from scratch. */ private _resetOptions(): void { - const changedOrDestroyed = merge(this.options.changes, this._destroy); - - this.optionSelectionChanges.pipe(takeUntil(changedOrDestroyed)).subscribe(event => { + // Subscribe to selection changes + this.optionSelectionChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(event => { this._onSelect(event.source, event.isUserInput); if (event.isUserInput && !this.multiple && this._panelOpen) { @@ -1278,14 +1277,10 @@ export class MatSelect } }); - // Listen to changes in the internal state of the options and react accordingly. - // Handles cases like the labels of the selected options changing. + // Subscribe to internal state changes of each option merge(...this.options.map(option => option._stateChanges)) - .pipe(takeUntil(changedOrDestroyed)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(() => { - // `_stateChanges` can fire as a result of a change in the label's DOM value which may - // be the result of an expression changing. We have to use `detectChanges` in order - // to avoid "changed after checked" errors (see #14793). this._changeDetectorRef.detectChanges(); this.stateChanges.next(); }); diff --git a/src/material/slider/slider-input.ts b/src/material/slider/slider-input.ts index 5631c1992a99..a79e9e9bef04 100644 --- a/src/material/slider/slider-input.ts +++ b/src/material/slider/slider-input.ts @@ -23,7 +23,6 @@ import { signal, } from '@angular/core'; import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {Subject} from 'rxjs'; import { _MatThumb, MatSliderDragEvent, @@ -254,9 +253,6 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA /** Defined when a user is using a form control to manage slider value & validation. */ private _formControl: FormControl | undefined; - /** Emits when the component is destroyed. */ - protected readonly _destroyed = new Subject(); - /** * Indicates whether UI updates should be skipped. * @@ -297,8 +293,6 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA ngOnDestroy(): void { this._listenerCleanups.forEach(cleanup => cleanup()); - this._destroyed.next(); - this._destroyed.complete(); this.dragStart.complete(); this.dragEnd.complete(); } diff --git a/src/material/slider/slider.ts b/src/material/slider/slider.ts index 1250ff837768..5be31c584130 100644 --- a/src/material/slider/slider.ts +++ b/src/material/slider/slider.ts @@ -34,7 +34,7 @@ import { RippleGlobalOptions, ThemePalette, } from '../core'; -import {Subscription} from 'rxjs'; + import { _MatThumb, _MatTickMark, @@ -49,6 +49,7 @@ import { } from './slider-interface'; import {MatSliderVisualThumb} from './slider-thumb'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; // TODO(wagnermaciel): maybe handle the following edge case: // 1. start dragging discrete slider @@ -372,9 +373,6 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { /** Whether animations have been disabled. */ _noopAnimations = _animationsDisabled(); - /** Subscription to changes to the directionality (LTR / RTL) context for the application. */ - private _dirChangeSubscription: Subscription; - /** Observer used to monitor size changes in the slider. */ private _resizeObserver: ResizeObserver | null; @@ -423,7 +421,7 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader); if (this._dir) { - this._dirChangeSubscription = this._dir.change.subscribe(() => this._onDirChange()); + this._dir.change.pipe(takeUntilDestroyed()).subscribe(() => this._onDirChange()); this._isRtl = this._dir.value === 'rtl'; } } @@ -499,7 +497,6 @@ export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider { } ngOnDestroy(): void { - this._dirChangeSubscription.unsubscribe(); this._resizeObserver?.disconnect(); this._resizeObserver = null; } diff --git a/src/material/stepper/step-header.ts b/src/material/stepper/step-header.ts index db473527aab7..71a366964204 100644 --- a/src/material/stepper/step-header.ts +++ b/src/material/stepper/step-header.ts @@ -18,7 +18,6 @@ import { AfterViewInit, inject, } from '@angular/core'; -import {Subscription} from 'rxjs'; import {MatStepLabel} from './step-label'; import {MatStepperIntl} from './stepper-intl'; import {MatStepperIconContext} from './stepper-icon'; @@ -27,6 +26,7 @@ import {_StructuralStylesLoader, MatRipple, ThemePalette} from '../core'; import {MatIcon} from '../icon'; import {NgTemplateOutlet} from '@angular/common'; import {_CdkPrivateStyleLoader, _VisuallyHiddenLoader} from '@angular/cdk/private'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'mat-step-header', @@ -46,8 +46,6 @@ export class MatStepHeader extends CdkStepHeader implements AfterViewInit, OnDes _intl = inject(MatStepperIntl); private _focusMonitor = inject(FocusMonitor); - private _intlSubscription: Subscription; - /** State of the given step. */ @Input() state: StepState; @@ -93,7 +91,7 @@ export class MatStepHeader extends CdkStepHeader implements AfterViewInit, OnDes styleLoader.load(_StructuralStylesLoader); styleLoader.load(_VisuallyHiddenLoader); const changeDetectorRef = inject(ChangeDetectorRef); - this._intlSubscription = this._intl.changes.subscribe(() => changeDetectorRef.markForCheck()); + this._intl.changes.pipe(takeUntilDestroyed()).subscribe(() => changeDetectorRef.markForCheck()); } ngAfterViewInit() { @@ -101,7 +99,6 @@ export class MatStepHeader extends CdkStepHeader implements AfterViewInit, OnDes } ngOnDestroy() { - this._intlSubscription.unsubscribe(); this._focusMonitor.stopMonitoring(this._elementRef); } diff --git a/src/material/stepper/stepper.ts b/src/material/stepper/stepper.ts index 82f3f6db9079..881c31a3026c 100644 --- a/src/material/stepper/stepper.ts +++ b/src/material/stepper/stepper.ts @@ -14,6 +14,7 @@ import { Component, ContentChild, ContentChildren, + DestroyRef, ElementRef, EventEmitter, inject, @@ -34,13 +35,12 @@ import {AbstractControl, FormGroupDirective, NgForm} from '@angular/forms'; import {_animationsDisabled, ErrorStateMatcher, ThemePalette} from '../core'; import {Platform} from '@angular/cdk/platform'; import {CdkPortalOutlet, TemplatePortal} from '@angular/cdk/portal'; -import {Subscription} from 'rxjs'; -import {takeUntil, map, startWith, switchMap} from 'rxjs/operators'; - +import {map, startWith, switchMap} from 'rxjs/operators'; import {MatStepHeader} from './step-header'; import {MatStepLabel} from './step-label'; import {MatStepperIcon, MatStepperIconContext} from './stepper-icon'; import {MatStepContent} from './step-content'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'mat-step', @@ -57,10 +57,10 @@ import {MatStepContent} from './step-content'; 'hidden': '', // Hide the steps so they don't affect the layout. }, }) -export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentInit, OnDestroy { +export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentInit { private _errorStateMatcher = inject(ErrorStateMatcher, {skipSelf: true}); private _viewContainerRef = inject(ViewContainerRef); - private _isSelected = Subscription.EMPTY; + private readonly _destroyRef = inject(DestroyRef); /** Content for step label given by ``. */ // We need an initializer here to avoid a TS error. @@ -82,7 +82,7 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI _portal: TemplatePortal; ngAfterContentInit() { - this._isSelected = this._stepper.steps.changes + this._stepper.steps.changes .pipe( switchMap(() => { return this._stepper.selectionChange.pipe( @@ -90,6 +90,7 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI startWith(this._stepper.selected === this), ); }), + takeUntilDestroyed(this._destroyRef), ) .subscribe(isSelected => { if (isSelected && this._lazyContent && !this._portal) { @@ -98,10 +99,6 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI }); } - ngOnDestroy() { - this._isSelected.unsubscribe(); - } - /** Custom error state matcher that additionally checks for validity of interacted form. */ isErrorState(control: AbstractControl | null, form: FormGroupDirective | NgForm | null): boolean { const originalErrorState = this._errorStateMatcher.isErrorState(control, form); @@ -139,6 +136,7 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI export class MatStepper extends CdkStepper implements AfterViewInit, AfterContentInit, OnDestroy { private _ngZone = inject(NgZone); private _renderer = inject(Renderer2); + private readonly _destroyRef = inject(DestroyRef); private _animationsDisabled = _animationsDisabled(); private _cleanupTransition: (() => void) | undefined; protected _isAnimating = signal(false); @@ -218,10 +216,12 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten this._icons.forEach(({name, templateRef}) => (this._iconOverrides[name] = templateRef)); // Mark the component for change detection whenever the content children query changes - this.steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => this._stateChanged()); + this.steps.changes + .pipe(takeUntilDestroyed(this._destroyRef)) + .subscribe(() => this._stateChanged()); // Transition events won't fire if animations are disabled so we simulate them. - this.selectedIndexChange.pipe(takeUntil(this._destroyed)).subscribe(() => { + this.selectedIndexChange.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => { const duration = this._getAnimationDuration(); if (duration === '0ms' || duration === '0s') { this._onAnimationDone(); @@ -260,7 +260,7 @@ export class MatStepper extends CdkStepper implements AfterViewInit, AfterConten if (typeof queueMicrotask === 'function') { let hasEmittedInitial = false; this._animatedContainers.changes - .pipe(startWith(null), takeUntil(this._destroyed)) + .pipe(startWith(null), takeUntilDestroyed(this._destroyRef)) .subscribe(() => queueMicrotask(() => { // Simulate the initial `animationDone` event diff --git a/src/material/tabs/paginated-tab-header.ts b/src/material/tabs/paginated-tab-header.ts index 407b49418c48..51be00c85502 100644 --- a/src/material/tabs/paginated-tab-header.ts +++ b/src/material/tabs/paginated-tab-header.ts @@ -17,6 +17,7 @@ import { AfterContentInit, AfterViewInit, ChangeDetectorRef, + DestroyRef, Directive, ElementRef, EventEmitter, @@ -35,6 +36,7 @@ import { import {EMPTY, Observable, Observer, Subject, merge, of as observableOf, timer} from 'rxjs'; import {debounceTime, filter, skip, startWith, switchMap, takeUntil} from 'rxjs/operators'; import {_animationsDisabled} from '../core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Config used to bind passive event listeners */ const passiveEventListenerOptions = { @@ -71,6 +73,7 @@ export type MatPaginatedTabHeaderItem = FocusableOption & {elementRef: ElementRe export abstract class MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy { + protected readonly _destroyRef = inject(DestroyRef); protected _elementRef = inject>(ElementRef); protected _changeDetectorRef = inject(ChangeDetectorRef); private _viewportRuler = inject(ViewportRuler); @@ -97,9 +100,6 @@ export abstract class MatPaginatedTabHeader /** Whether the header should scroll to the selected index after the view has been checked. */ private _selectedIndexChanged = false; - /** Emits when the component is destroyed. */ - protected readonly _destroyed = new Subject(); - /** Whether the controls for pagination should be displayed */ _showPaginationControls = false; @@ -200,11 +200,13 @@ export abstract class MatPaginatedTabHeader // re-align. const resize = this._sharedResizeObserver .observe(this._elementRef.nativeElement) - .pipe(debounceTime(32), takeUntil(this._destroyed)); + .pipe(debounceTime(32), takeUntilDestroyed(this._destroyRef)); // Note: We do not actually need to watch these events for proper functioning of the tabs, // the resize events above should capture any viewport resize that we care about. However, // removing this is fairly breaking for screenshot tests, so we're leaving it here for now. - const viewportResize = this._viewportRuler.change(150).pipe(takeUntil(this._destroyed)); + const viewportResize = this._viewportRuler + .change(150) + .pipe(takeUntilDestroyed(this._destroyRef)); const realign = () => { this.updatePagination(); @@ -230,7 +232,7 @@ export abstract class MatPaginatedTabHeader // On dir change or resize, realign the ink bar and update the orientation of // the key manager if the direction has changed. merge(dirChange, viewportResize, resize, this._items.changes, this._itemsResized()) - .pipe(takeUntil(this._destroyed)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(() => { // We need to defer this to give the browser some time to recalculate // the element dimensions. The call has to be wrapped in `NgZone.run`, @@ -316,8 +318,6 @@ export abstract class MatPaginatedTabHeader ngOnDestroy() { this._eventCleanups.forEach(cleanup => cleanup()); this._keyManager?.destroy(); - this._destroyed.next(); - this._destroyed.complete(); this._stopScrolling.complete(); } @@ -639,12 +639,10 @@ export abstract class MatPaginatedTabHeader // Start a timer after the delay and keep firing based on the interval. timer(HEADER_SCROLL_DELAY, HEADER_SCROLL_INTERVAL) - // Keep the timer going until something tells it to stop or the component is destroyed. - .pipe(takeUntil(merge(this._stopScrolling, this._destroyed))) + .pipe(takeUntilDestroyed(this._destroyRef), takeUntil(merge(this._stopScrolling))) .subscribe(() => { const {maxScrollDistance, distance} = this._scrollHeader(direction); - // Stop the timer if we've reached the start or the end. if (distance === 0 || distance >= maxScrollDistance) { this._stopInterval(); } diff --git a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts index c40ff234a247..8e683ade69e6 100644 --- a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts @@ -38,12 +38,13 @@ import { import {_IdGenerator, FocusableOption, FocusMonitor} from '@angular/cdk/a11y'; import {MatInkBar, InkBarItem} from '../ink-bar'; import {BehaviorSubject, Subject} from 'rxjs'; -import {startWith, takeUntil} from 'rxjs/operators'; +import {startWith} from 'rxjs/operators'; import {ENTER, SPACE} from '@angular/cdk/keycodes'; import {MAT_TABS_CONFIG, MatTabsConfig} from '../tab-config'; import {MatPaginatedTabHeader, MatPaginatedTabHeaderItem} from '../paginated-tab-header'; import {CdkObserveContent} from '@angular/cdk/observers'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Navigation component matching the styles of the tab group header. @@ -189,14 +190,14 @@ export class MatTabNav extends MatPaginatedTabHeader implements AfterContentInit // We need this to run before the `changes` subscription in parent to ensure that the // selectedIndex is up-to-date by the time the super class starts looking for it. this._items.changes - .pipe(startWith(null), takeUntil(this._destroyed)) + .pipe(startWith(null), takeUntilDestroyed(this._destroyRef)) .subscribe(() => this.updateActiveLink()); super.ngAfterContentInit(); // Turn the `change` stream into a signal to try and avoid "changed after checked" errors. - this._keyManager!.change.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => - this._focusedItem.set(this._keyManager?.activeItem || null), + this._keyManager!.change.pipe(startWith(null), takeUntilDestroyed(this._destroyRef)).subscribe( + () => this._focusedItem.set(this._keyManager?.activeItem || null), ); } @@ -357,11 +358,9 @@ export class MatTabLink this.rippleConfig.animation = {enterDuration: 0, exitDuration: 0}; } - this._tabNavBar._fitInkBarToContent - .pipe(takeUntil(this._destroyed)) - .subscribe(fitInkBarToContent => { - this.fitInkBarToContent = fitInkBarToContent; - }); + this._tabNavBar._fitInkBarToContent.pipe(takeUntilDestroyed()).subscribe(fitInkBarToContent => { + this.fitInkBarToContent = fitInkBarToContent; + }); } /** Focuses the tab link. */ diff --git a/src/material/timepicker/timepicker.ts b/src/material/timepicker/timepicker.ts index 042e0524a321..5b97558ed641 100644 --- a/src/material/timepicker/timepicker.ts +++ b/src/material/timepicker/timepicker.ts @@ -13,6 +13,7 @@ import { ChangeDetectionStrategy, Component, computed, + DestroyRef, effect, ElementRef, inject, @@ -60,7 +61,7 @@ import { parseInterval, validateAdapter, } from './util'; -import {Subscription} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Event emitted when a value is selected in the timepicker. */ export interface MatTimepickerSelected { @@ -127,6 +128,7 @@ export interface MatTimepickerConnectedInput { ], }) export class MatTimepicker implements OnDestroy, MatOptionParentComponent { + private readonly _destroyRef = inject(DestroyRef); private _dir = inject(Directionality, {optional: true}); private _viewContainerRef = inject(ViewContainerRef); private _injector = inject(Injector); @@ -143,7 +145,6 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { private _overlayRef: OverlayRef | null = null; private _portal: TemplatePortal | null = null; private _optionsCacheKey: string | null = null; - private _localeChanges: Subscription; private _onOpenRender: AfterRenderRef | null = null; protected _panelTemplate = viewChild.required>('panelTemplate'); @@ -307,7 +308,6 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { ngOnDestroy(): void { this._keyManager.destroy(); - this._localeChanges.unsubscribe(); this._onOpenRender?.destroy(); this._overlayRef?.dispose(); } @@ -486,7 +486,7 @@ export class MatTimepicker implements OnDestroy, MatOptionParentComponent { /** Sets up the logic that updates the timepicker when the locale changes. */ private _handleLocaleChanges(): void { // Re-generate the options list if the locale changes. - this._localeChanges = this._dateAdapter.localeChanges.subscribe(() => { + this._dateAdapter.localeChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => { this._optionsCacheKey = null; if (this.isOpen()) { diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index 7245a9bd7abf..a43e952881c6 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -import {takeUntil} from 'rxjs/operators'; + import { BooleanInput, coerceBooleanProperty, @@ -31,6 +31,7 @@ import { afterNextRender, Injector, DOCUMENT, + DestroyRef, } from '@angular/core'; import {NgClass} from '@angular/common'; import {normalizePassiveListenerOptions, Platform} from '@angular/cdk/platform'; @@ -54,6 +55,7 @@ import { import {ComponentPortal} from '@angular/cdk/portal'; import {Observable, Subject} from 'rxjs'; import {_animationsDisabled} from '../core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** Possible positions for a tooltip. */ export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after'; @@ -173,6 +175,7 @@ const MAX_WIDTH = 200; }, }) export class MatTooltip implements OnDestroy, AfterViewInit { + private readonly _destroyRef = inject(DestroyRef); private _elementRef = inject>(ElementRef); private _ngZone = inject(NgZone); private _platform = inject(Platform); @@ -350,9 +353,6 @@ export class MatTooltip implements OnDestroy, AfterViewInit { /** Timer started at the last `touchstart` event. */ private _touchstartTimeout: null | ReturnType = null; - /** Emits when the component is destroyed. */ - private readonly _destroyed = new Subject(); - /** Whether ngOnDestroyed has been called. */ private _isDestroyed = false; @@ -392,7 +392,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { this._focusMonitor .monitor(this._elementRef) - .pipe(takeUntil(this._destroyed)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(origin => { // Note that the focus monitor runs outside the Angular zone. if (!origin) { @@ -425,9 +425,6 @@ export class MatTooltip implements OnDestroy, AfterViewInit { }); this._passiveListeners.length = 0; - this._destroyed.next(); - this._destroyed.complete(); - this._isDestroyed = true; this._ariaDescriber.removeDescription(nativeElement, this.message, 'tooltip'); @@ -450,7 +447,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { instance._mouseLeaveHideDelay = this._hideDelay; instance .afterHidden() - .pipe(takeUntil(this._destroyed)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(() => this._detach()); this._setTooltipClass(this._tooltipClass); this._updateTooltipMessage(); @@ -510,7 +507,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { .withViewportMargin(this._viewportMargin) .withScrollableContainers(scrollableAncestors); - strategy.positionChanges.pipe(takeUntil(this._destroyed)).subscribe(change => { + strategy.positionChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(change => { this._updateCurrentPositionClass(change.connectionPair); if (this._tooltipInstance) { @@ -534,17 +531,17 @@ export class MatTooltip implements OnDestroy, AfterViewInit { this._overlayRef .detachments() - .pipe(takeUntil(this._destroyed)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(() => this._detach()); this._overlayRef .outsidePointerEvents() - .pipe(takeUntil(this._destroyed)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(() => this._tooltipInstance?._handleBodyInteraction()); this._overlayRef .keydownEvents() - .pipe(takeUntil(this._destroyed)) + .pipe(takeUntilDestroyed(this._destroyRef)) .subscribe(event => { if (this._isTooltipVisible() && event.keyCode === ESCAPE && !hasModifierKey(event)) { event.preventDefault(); @@ -559,7 +556,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { if (!this._dirSubscribed) { this._dirSubscribed = true; - this._dir.change.pipe(takeUntil(this._destroyed)).subscribe(() => { + this._dir.change.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => { if (this._overlayRef) { this._updatePosition(this._overlayRef); } diff --git a/src/material/tree/node.ts b/src/material/tree/node.ts index ef7c17a1d970..7b00c798997a 100644 --- a/src/material/tree/node.ts +++ b/src/material/tree/node.ts @@ -26,7 +26,7 @@ import { import {NoopTreeKeyManager, TreeKeyManagerItem, TreeKeyManagerStrategy} from '@angular/cdk/a11y'; /** - * Determinte if argument TreeKeyManager is the NoopTreeKeyManager. This function is safe to use with SSR. + * Determine if argument TreeKeyManager is the NoopTreeKeyManager. This function is safe to use with SSR. */ function isNoopTreeKeyManager( keyManager: TreeKeyManagerStrategy,