From e3d53856615ee2e0ab15039439a2652831529c6d Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 29 Jul 2025 10:17:30 +0200 Subject: [PATCH] Migrate UIF to use takeuntilDestroyed (#15777) --- .../anon-layout-wrapper.component.ts | 20 +++++------ .../src/async-actions/bit-action.directive.ts | 17 ++++------ .../src/async-actions/bit-submit.directive.ts | 33 +++++++++---------- .../async-actions/form-button.directive.ts | 21 ++++-------- .../src/tabs/tab-group/tab-group.component.ts | 19 ++++------- .../tabs/tab-nav-bar/tab-link.component.ts | 17 ++++------ 6 files changed, 51 insertions(+), 76 deletions(-) diff --git a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts index a55e66845f6..34fdc5b60fc 100644 --- a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts @@ -1,8 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; +import { ChangeDetectorRef, Component, OnInit, inject, DestroyRef } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router"; -import { Subject, filter, switchMap, takeUntil, tap } from "rxjs"; +import { filter, switchMap, tap } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -51,9 +52,7 @@ export interface AnonLayoutWrapperData { templateUrl: "anon-layout-wrapper.component.html", imports: [AnonLayoutComponent, RouterModule], }) -export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { - private destroy$ = new Subject(); - +export class AnonLayoutWrapperComponent implements OnInit { protected pageTitle: string; protected pageSubtitle: string; protected pageIcon: Icon; @@ -70,6 +69,8 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { private changeDetectorRef: ChangeDetectorRef, ) {} + private readonly destroyRef = inject(DestroyRef); + ngOnInit(): void { // Set the initial page data on load this.setAnonLayoutWrapperDataFromRouteData(this.route.snapshot.firstChild?.data); @@ -85,7 +86,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { // reset page data on page changes tap(() => this.resetPageData()), switchMap(() => this.route.firstChild?.data || null), - takeUntil(this.destroy$), + takeUntilDestroyed(this.destroyRef), ) .subscribe((firstChildRouteData: Data | null) => { this.setAnonLayoutWrapperDataFromRouteData(firstChildRouteData); @@ -121,7 +122,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { private listenForServiceDataChanges() { this.anonLayoutWrapperDataService .anonLayoutWrapperData$() - .pipe(takeUntil(this.destroy$)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data: AnonLayoutWrapperData) => { this.setAnonLayoutWrapperData(data); }); @@ -180,9 +181,4 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.hideCardWrapper = null; this.hideIcon = null; } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/libs/components/src/async-actions/bit-action.directive.ts b/libs/components/src/async-actions/bit-action.directive.ts index 2d00dba1a1e..2de8a16dd31 100644 --- a/libs/components/src/async-actions/bit-action.directive.ts +++ b/libs/components/src/async-actions/bit-action.directive.ts @@ -1,7 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, HostListener, model, OnDestroy, Optional } from "@angular/core"; -import { BehaviorSubject, finalize, Subject, takeUntil, tap } from "rxjs"; +import { Directive, HostListener, model, Optional, inject, DestroyRef } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { BehaviorSubject, finalize, tap } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -16,8 +17,7 @@ import { FunctionReturningAwaitable, functionToObservable } from "../utils/funct @Directive({ selector: "[bitAction]", }) -export class BitActionDirective implements OnDestroy { - private destroy$ = new Subject(); +export class BitActionDirective { private _loading$ = new BehaviorSubject(false); /** @@ -40,6 +40,8 @@ export class BitActionDirective implements OnDestroy { readonly handler = model(undefined, { alias: "bitAction" }); + private readonly destroyRef = inject(DestroyRef); + constructor( private buttonComponent: ButtonLikeAbstraction, @Optional() private validationService?: ValidationService, @@ -62,13 +64,8 @@ export class BitActionDirective implements OnDestroy { }, }), finalize(() => (this.loading = false)), - takeUntil(this.destroy$), + takeUntilDestroyed(this.destroyRef), ) .subscribe(); } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/libs/components/src/async-actions/bit-submit.directive.ts b/libs/components/src/async-actions/bit-submit.directive.ts index cafc8d634b9..e7911196fc3 100644 --- a/libs/components/src/async-actions/bit-submit.directive.ts +++ b/libs/components/src/async-actions/bit-submit.directive.ts @@ -1,8 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, OnDestroy, OnInit, Optional, input } from "@angular/core"; +import { Directive, OnInit, Optional, input, inject, DestroyRef } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormGroupDirective } from "@angular/forms"; -import { BehaviorSubject, catchError, filter, of, Subject, switchMap, takeUntil } from "rxjs"; +import { BehaviorSubject, catchError, filter, of, switchMap } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -15,8 +16,9 @@ import { FunctionReturningAwaitable, functionToObservable } from "../utils/funct @Directive({ selector: "[formGroup][bitSubmit]", }) -export class BitSubmitDirective implements OnInit, OnDestroy { - private destroy$ = new Subject(); +export class BitSubmitDirective implements OnInit { + private readonly destroyRef = inject(DestroyRef); + private _loading$ = new BehaviorSubject(false); private _disabled$ = new BehaviorSubject(false); @@ -51,7 +53,7 @@ export class BitSubmitDirective implements OnInit, OnDestroy { }), ); }), - takeUntil(this.destroy$), + takeUntilDestroyed(), ) .subscribe({ next: () => (this.loading = false), @@ -60,13 +62,15 @@ export class BitSubmitDirective implements OnInit, OnDestroy { } ngOnInit(): void { - this.formGroupDirective.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => { - if (this.allowDisabledFormSubmit()) { - this._disabled$.next(false); - } else { - this._disabled$.next(c === "DISABLED"); - } - }); + this.formGroupDirective.statusChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((c) => { + if (this.allowDisabledFormSubmit()) { + this._disabled$.next(false); + } else { + this._disabled$.next(c === "DISABLED"); + } + }); } get disabled() { @@ -85,9 +89,4 @@ export class BitSubmitDirective implements OnInit, OnDestroy { this.disabled = value; this._loading$.next(value); } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/libs/components/src/async-actions/form-button.directive.ts b/libs/components/src/async-actions/form-button.directive.ts index 2bbd8fa87b6..dc8c095fd18 100644 --- a/libs/components/src/async-actions/form-button.directive.ts +++ b/libs/components/src/async-actions/form-button.directive.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, OnDestroy, Optional, input } from "@angular/core"; -import { Subject, takeUntil } from "rxjs"; +import { Directive, Optional, input } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ButtonLikeAbstraction } from "../shared/button-like.abstraction"; @@ -26,9 +26,7 @@ import { BitSubmitDirective } from "./bit-submit.directive"; @Directive({ selector: "button[bitFormButton]", }) -export class BitFormButtonDirective implements OnDestroy { - private destroy$ = new Subject(); - +export class BitFormButtonDirective { readonly type = input(); readonly disabled = input(); @@ -38,7 +36,7 @@ export class BitFormButtonDirective implements OnDestroy { @Optional() actionDirective?: BitActionDirective, ) { if (submitDirective && buttonComponent) { - submitDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => { + submitDirective.loading$.pipe(takeUntilDestroyed()).subscribe((loading) => { if (this.type() === "submit") { buttonComponent.loading.set(loading); } else { @@ -46,7 +44,7 @@ export class BitFormButtonDirective implements OnDestroy { } }); - submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { + submitDirective.disabled$.pipe(takeUntilDestroyed()).subscribe((disabled) => { const disabledValue = this.disabled(); if (disabledValue !== false) { buttonComponent.disabled.set(disabledValue || disabled); @@ -55,18 +53,13 @@ export class BitFormButtonDirective implements OnDestroy { } if (submitDirective && actionDirective) { - actionDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { + actionDirective.loading$.pipe(takeUntilDestroyed()).subscribe((disabled) => { submitDirective.disabled = disabled; }); - submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { + submitDirective.disabled$.pipe(takeUntilDestroyed()).subscribe((disabled) => { actionDirective.disabled = disabled; }); } } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/libs/components/src/tabs/tab-group/tab-group.component.ts b/libs/components/src/tabs/tab-group/tab-group.component.ts index 2bd81375d39..8efb91ba0c5 100644 --- a/libs/components/src/tabs/tab-group/tab-group.component.ts +++ b/libs/components/src/tabs/tab-group/tab-group.component.ts @@ -11,13 +11,14 @@ import { ContentChildren, EventEmitter, Input, - OnDestroy, Output, QueryList, ViewChildren, input, + inject, + DestroyRef, } from "@angular/core"; -import { Subject, takeUntil } from "rxjs"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { TabHeaderComponent } from "../shared/tab-header.component"; import { TabListContainerDirective } from "../shared/tab-list-container.directive"; @@ -40,11 +41,10 @@ let nextId = 0; TabBodyComponent, ], }) -export class TabGroupComponent - implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy -{ +export class TabGroupComponent implements AfterContentChecked, AfterContentInit, AfterViewInit { + private readonly destroyRef = inject(DestroyRef); + private readonly _groupId: number; - private readonly destroy$ = new Subject(); private _indexToSelect: number | null = 0; /** @@ -150,7 +150,7 @@ export class TabGroupComponent ngAfterContentInit() { // Subscribe to any changes in the number of tabs, in order to be able // to re-render content when new tabs are added or removed. - this.tabs.changes.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.tabs.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { const indexToSelect = this._clampTabIndex(this._indexToSelect); // If the selected tab didn't explicitly change, keep the previously @@ -183,11 +183,6 @@ export class TabGroupComponent }); } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - private _clampTabIndex(index: number): number { return Math.min(this.tabs.length - 1, Math.max(index || 0, 0)); } diff --git a/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts b/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts index f1b279c4371..301f9c4b191 100644 --- a/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts +++ b/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts @@ -6,12 +6,13 @@ import { Component, HostListener, Input, - OnDestroy, ViewChild, input, + inject, + DestroyRef, } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { IsActiveMatchOptions, RouterLinkActive, RouterModule } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; import { TabListItemDirective } from "../shared/tab-list-item.directive"; @@ -22,9 +23,8 @@ import { TabNavBarComponent } from "./tab-nav-bar.component"; templateUrl: "tab-link.component.html", imports: [TabListItemDirective, RouterModule], }) -export class TabLinkComponent implements FocusableOption, AfterViewInit, OnDestroy { - private destroy$ = new Subject(); - +export class TabLinkComponent implements FocusableOption, AfterViewInit { + private readonly destroyRef = inject(DestroyRef); @ViewChild(TabListItemDirective) tabItem: TabListItemDirective; @ViewChild("rla") routerLinkActive: RouterLinkActive; @@ -61,12 +61,7 @@ export class TabLinkComponent implements FocusableOption, AfterViewInit, OnDestr // The active state of tab links are tracked via the routerLinkActive directive // We need to watch for changes to tell the parent nav group when the tab is active this.routerLinkActive.isActiveChange - .pipe(takeUntil(this.destroy$)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((_) => this._tabNavBar.updateActiveLink()); } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } }