From be64a198d3e2d44d7645915fe03b05e2f5d119ce Mon Sep 17 00:00:00 2001 From: Mehmet Erim <34455572+mehmet-erim@users.noreply.github.com> Date: Mon, 20 Jul 2020 09:40:28 +0300 Subject: [PATCH 01/11] Update Nightly-Builds.md --- docs/en/Nightly-Builds.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/Nightly-Builds.md b/docs/en/Nightly-Builds.md index 1a66af32e1..315f5109b9 100644 --- a/docs/en/Nightly-Builds.md +++ b/docs/en/Nightly-Builds.md @@ -29,13 +29,13 @@ Now, you can install preview / nightly packages to your project from Nuget Brows The latest version of preview NPM packages can be installed by the running below command in the root folder of application: ```bash -abp switch-to-preview +abp switch-to-preview --npm ``` If you're using the ABP Framework preview packages, you can switch back to stable version using this command: ```bash -abp switch-to-stable +abp switch-to-stable --npm ``` -See the [ABP CLI documentation](./CLI.md) for more information. \ No newline at end of file +See the [ABP CLI documentation](./CLI.md) for more information. From c19a986858f9931e4f6e8cce68da9da6bf2fe697 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 22 Jul 2020 12:19:18 +0300 Subject: [PATCH 02/11] feat: add a subscription service --- .../packages/core/src/lib/services/index.ts | 1 + .../src/lib/services/subscription.service.ts | 50 ++++++++++ .../lib/tests/subscription.service.spec.ts | 95 +++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/services/subscription.service.ts create mode 100644 npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index 064dcbd35e..e5e5286710 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -11,4 +11,5 @@ export * from './profile.service'; export * from './rest.service'; export * from './routes.service'; export * from './session-state.service'; +export * from './subscription.service'; export * from './track-by.service'; diff --git a/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts b/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts new file mode 100644 index 0000000000..81739d10ae --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import type { OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import type { Observable, PartialObserver } from 'rxjs'; + +@Injectable() +export class SubscriptionService implements OnDestroy { + private subscription = new Subscription(); + + get isClosed() { + return this.subscription.closed; + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } + + reset() { + this.subscription.unsubscribe(); + this.subscription = new Subscription(); + } + + subscribe( + source$: Observable, + next?: (value: T) => void, + error?: (error: any) => void, + ): Subscription; + subscribe(source$: Observable, observer?: PartialObserver): Subscription; + subscribe( + source$: Observable, + nextOrObserver?: PartialObserver | Next, + error?: (error: any) => void, + ): Subscription { + const subscription = source$.subscribe(nextOrObserver as Next, error); + this.subscription.add(subscription); + return subscription; + } + + unsubscribe(subscription: Subscription | undefined | null) { + if (!subscription) return; + this.subscription.remove(subscription); + subscription.unsubscribe(); + } + + unsubscribeAll() { + this.subscription.unsubscribe(); + } +} + +type Next = (value: T) => void; diff --git a/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts new file mode 100644 index 0000000000..95df285fb9 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts @@ -0,0 +1,95 @@ +import { of, Subscription, timer } from 'rxjs'; +import { SubscriptionService } from '../services/subscription.service'; + +describe('SubscriptionService', () => { + let service: SubscriptionService; + + beforeEach(() => { + service = new SubscriptionService(); + }); + + afterEach(() => { + service['subscription'].unsubscribe(); + }); + + describe('#subscribe', () => { + it('should subscribe to given observable with next and error functions and return the Subscription instance', () => { + const next = jest.fn(); + const error = jest.fn(); + const subscription = service.subscribe(of(null), next, error); + expect(subscription).toBeInstanceOf(Subscription); + expect(next).toHaveBeenCalledWith(null); + expect(next).toHaveBeenCalledTimes(1); + expect(error).not.toHaveBeenCalled(); + }); + + it('should subscribe to given observable with observer and return the Subscription instance', () => { + const observer = { next: jest.fn(), complete: jest.fn() }; + const subscription = service.subscribe(of(null), observer); + expect(subscription).toBeInstanceOf(Subscription); + expect(observer.next).toHaveBeenCalledWith(null); + expect(observer.next).toHaveBeenCalledTimes(1); + expect(observer.complete).toHaveBeenCalledTimes(1); + }); + }); + + describe('#isClosed', () => { + it('should return true if subscriptions are alive and false if not', () => { + service.subscribe(timer(1000), () => {}); + expect(service.isClosed).toBe(false); + + service['subscription'].unsubscribe(); + expect(service.isClosed).toBe(true); + }); + }); + + describe('#unsubscribeAll', () => { + it('should close all subscriptions and the parent subscription', () => { + const sub1 = service.subscribe(timer(1000), () => {}); + const sub2 = service.subscribe(timer(1000), () => {}); + + expect(sub1.closed).toBe(false); + expect(sub2.closed).toBe(false); + expect(service.isClosed).toBe(false); + + service.unsubscribeAll(); + + expect(sub1.closed).toBe(true); + expect(sub2.closed).toBe(true); + expect(service.isClosed).toBe(true); + }); + }); + + describe('#reset', () => { + it('should close all subscriptions but not the parent subscription', () => { + const sub1 = service.subscribe(timer(1000), () => {}); + const sub2 = service.subscribe(timer(1000), () => {}); + + expect(sub1.closed).toBe(false); + expect(sub2.closed).toBe(false); + expect(service.isClosed).toBe(false); + + service.reset(); + + expect(sub1.closed).toBe(true); + expect(sub2.closed).toBe(true); + expect(service.isClosed).toBe(false); + }); + }); + + describe('#unsubscribe', () => { + it('should unsubscribe from given subscription only', () => { + const sub1 = service.subscribe(timer(1000), () => {}); + const sub2 = service.subscribe(timer(1000), () => {}); + expect(service.isClosed).toBe(false); + + service.unsubscribe(sub1); + expect(sub1.closed).toBe(true); + expect(service.isClosed).toBe(false); + + service.unsubscribe(sub2); + expect(sub2.closed).toBe(true); + expect(service.isClosed).toBe(false); + }); + }); +}); From 8d4180419fccfddd5c13c1fe9adffde200928a2e Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 22 Jul 2020 12:54:14 +0300 Subject: [PATCH 03/11] feat: add removeOne method to SubscriptionService --- .../src/lib/services/subscription.service.ts | 16 +++++++++------ .../lib/tests/subscription.service.spec.ts | 20 ++++++++++++++++--- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts b/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts index 81739d10ae..eadec95b61 100644 --- a/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts @@ -15,6 +15,11 @@ export class SubscriptionService implements OnDestroy { this.subscription.unsubscribe(); } + removeOne(subscription: Subscription | undefined | null) { + if (!subscription) return; + this.subscription.remove(subscription); + } + reset() { this.subscription.unsubscribe(); this.subscription = new Subscription(); @@ -36,15 +41,14 @@ export class SubscriptionService implements OnDestroy { return subscription; } - unsubscribe(subscription: Subscription | undefined | null) { - if (!subscription) return; - this.subscription.remove(subscription); - subscription.unsubscribe(); - } - unsubscribeAll() { this.subscription.unsubscribe(); } + + unsubscribeOne(subscription: Subscription | undefined | null) { + this.removeOne(subscription); + subscription.unsubscribe(); + } } type Next = (value: T) => void; diff --git a/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts index 95df285fb9..c803455083 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts @@ -77,19 +77,33 @@ describe('SubscriptionService', () => { }); }); - describe('#unsubscribe', () => { + describe('#unsubscribeOne', () => { it('should unsubscribe from given subscription only', () => { const sub1 = service.subscribe(timer(1000), () => {}); const sub2 = service.subscribe(timer(1000), () => {}); expect(service.isClosed).toBe(false); - service.unsubscribe(sub1); + service.unsubscribeOne(sub1); expect(sub1.closed).toBe(true); expect(service.isClosed).toBe(false); - service.unsubscribe(sub2); + service.unsubscribeOne(sub2); expect(sub2.closed).toBe(true); expect(service.isClosed).toBe(false); }); }); + + describe('#removeOne', () => { + it('should remove given subscription from list of subscriptions', () => { + const sub1 = service.subscribe(timer(1000), () => {}); + const sub2 = service.subscribe(timer(1000), () => {}); + expect(service.isClosed).toBe(false); + + service.removeOne(sub1); + expect(sub1.closed).toBe(false); + expect(service.isClosed).toBe(false); + + sub1.unsubscribe(); + }); + }); }); From 6f1a42373dc03c01a281f78bf2167765ec707c85 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 22 Jul 2020 13:04:27 +0300 Subject: [PATCH 04/11] docs: explain how the subscription service works --- docs/en/UI/Angular/Subscription-Service.md | 203 +++++++++++++++++++++ docs/en/UI/Angular/Track-By-Service.md | 2 +- docs/en/docs-nav.json | 4 + 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 docs/en/UI/Angular/Subscription-Service.md diff --git a/docs/en/UI/Angular/Subscription-Service.md b/docs/en/UI/Angular/Subscription-Service.md new file mode 100644 index 0000000000..b92cd26c59 --- /dev/null +++ b/docs/en/UI/Angular/Subscription-Service.md @@ -0,0 +1,203 @@ +# Easy Unsubscription for Your Observables + +`SubscriptionService` is a utility service to provide an easy unsubscription from RxJS observables in Angular components and directives. Please see [why you should unsubscribe from observables on instance destruction](https://angular.io/guide/lifecycle-hooks#cleaning-up-on-instance-destruction). + +## Getting Started + +You have to provide the `SubscriptionService` at component or directive level, because it is **not provided in root** and it works in sync with component/directive lifecycle. Only after then you can inject and start using it. + +```js +import { SubscriptionService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent { + count$ = interval(1000); + + constructor(private subscription: SubscriptionService) { + this.subscription.subscribe(this.count$, console.log); + } +} +``` + +The values emitted by the `count$` will be logged until the component is destroyed. You will not have to `unsubscribe` manually. + +> Please do not try to use a singleton `SubscriptionService`. It simply will not work. + +## Usage + +### How to Subscribe to Observables + +You can pass a `next` function and an `error` function. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + const source$ = interval(1000); + const nextFn = value => console.log(value * 2); + const errorFn = error => { + console.error(error); + return of(null); + }; + + this.subscription.subscribe(source$, nextFn, errorFn); + } +} +``` + +Or, you can pass an observer. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + const source$ = interval(1000); + const observer = { + next: value => console.log(value * 2), + complete: () => console.log('DONE'), + }; + + this.subscription.subscribe(source$, observer); + } +} +``` + +Ths `subscribe` method returns the individual subscription, so that you may use it later on. Please see topics below for details. + +### How to Unsubscribe Before Instance Destruction + +There are two ways to do that. If you are not going to subscribe again, you may use the `unsubscribeAll` method. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.subscription.subscribe(interval(1000), console.log); + } + + onSomeEvent() { + this.subscription.unsubscribeAll(); + } +} +``` + +This will clear all subscriptions, but you will not be able to subscribe again. If you are planning to add another subscription, you may use the `reset` method instead. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.subscription.subscribe(interval(1000), console.log); + } + + onSomeEvent() { + this.subscription.reset(); + this.subscription.subscribe(interval(1000), console.warn); + } +} +``` + +### How to Unsubscribe From a Single Subscription + +Sometimes, you may need to unsubscribe from a particular subscription but leave others alive. In such a case, you may use the `unsubscribeOne` method. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + countSubscription: Subscription; + + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.countSubscription = this.subscription.subscribe( + interval(1000), + console.log + ); + } + + onSomeEvent() { + this.subscription.unsubscribeOne(this.countSubscription); + console.log(this.countSubscription.closed); // true + } +} +``` + +### How to Remove a Single Subscription From Tracked Subscriptions + +You may want to take control of a particular subscription. In such a case, you may use the `removeOne` method to remove it from tracked subscriptions. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + countSubscription: Subscription; + + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.countSubscription = this.subscription.subscribe( + interval(1000), + console.log + ); + } + + onSomeEvent() { + this.subscription.removeOne(this.countSubscription); + console.log(this.countSubscription.closed); // false + } +} +``` + +### How to Check If Unsubscribed From All + +Please use `isClosed` getter to check if `unsubscribeAll` was called before. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.subscription.subscribe(interval(1000), console.log); + } + + onSomeEvent() { + console.log(this.subscription.isClosed); // false + } +} +``` + +## What's Next? + +- [ListService](./List-Service.md) diff --git a/docs/en/UI/Angular/Track-By-Service.md b/docs/en/UI/Angular/Track-By-Service.md index 447cc4505a..050706accd 100644 --- a/docs/en/UI/Angular/Track-By-Service.md +++ b/docs/en/UI/Angular/Track-By-Service.md @@ -116,4 +116,4 @@ class DemoComponent { ## What's Next? -- [ListService](./List-Service.md) +- [SubscriptionService](./Subscription-Service.md) diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index a6ab09dfee..85f1567406 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -434,6 +434,10 @@ "text": "TrackByService", "path": "UI/Angular/Track-By-Service.md" }, + { + "text": "SubscriptionService", + "path": "UI/Angular/Subscription-Service.md" + }, { "text": "ListService", "path": "UI/Angular/List-Service.md" From 0b2c9096cb088f5d5d9b90f870164546ae37315a Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 22 Jul 2020 14:06:25 +0300 Subject: [PATCH 05/11] feat: rename subscription service methods --- .../src/lib/services/subscription.service.ts | 38 +++++++++---------- .../lib/tests/subscription.service.spec.ts | 34 ++++++++--------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts b/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts index eadec95b61..9296585d2a 100644 --- a/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/subscription.service.ts @@ -11,27 +11,13 @@ export class SubscriptionService implements OnDestroy { return this.subscription.closed; } - ngOnDestroy(): void { - this.subscription.unsubscribe(); - } - - removeOne(subscription: Subscription | undefined | null) { - if (!subscription) return; - this.subscription.remove(subscription); - } - - reset() { - this.subscription.unsubscribe(); - this.subscription = new Subscription(); - } - - subscribe( + addOne( source$: Observable, next?: (value: T) => void, error?: (error: any) => void, ): Subscription; - subscribe(source$: Observable, observer?: PartialObserver): Subscription; - subscribe( + addOne(source$: Observable, observer?: PartialObserver): Subscription; + addOne( source$: Observable, nextOrObserver?: PartialObserver | Next, error?: (error: any) => void, @@ -41,14 +27,28 @@ export class SubscriptionService implements OnDestroy { return subscription; } - unsubscribeAll() { + closeAll() { this.subscription.unsubscribe(); } - unsubscribeOne(subscription: Subscription | undefined | null) { + closeOne(subscription: Subscription | undefined | null) { this.removeOne(subscription); subscription.unsubscribe(); } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } + + removeOne(subscription: Subscription | undefined | null) { + if (!subscription) return; + this.subscription.remove(subscription); + } + + reset() { + this.subscription.unsubscribe(); + this.subscription = new Subscription(); + } } type Next = (value: T) => void; diff --git a/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts index c803455083..d0c8c4754f 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts @@ -12,11 +12,11 @@ describe('SubscriptionService', () => { service['subscription'].unsubscribe(); }); - describe('#subscribe', () => { + describe('#addOne', () => { it('should subscribe to given observable with next and error functions and return the Subscription instance', () => { const next = jest.fn(); const error = jest.fn(); - const subscription = service.subscribe(of(null), next, error); + const subscription = service.addOne(of(null), next, error); expect(subscription).toBeInstanceOf(Subscription); expect(next).toHaveBeenCalledWith(null); expect(next).toHaveBeenCalledTimes(1); @@ -25,7 +25,7 @@ describe('SubscriptionService', () => { it('should subscribe to given observable with observer and return the Subscription instance', () => { const observer = { next: jest.fn(), complete: jest.fn() }; - const subscription = service.subscribe(of(null), observer); + const subscription = service.addOne(of(null), observer); expect(subscription).toBeInstanceOf(Subscription); expect(observer.next).toHaveBeenCalledWith(null); expect(observer.next).toHaveBeenCalledTimes(1); @@ -35,7 +35,7 @@ describe('SubscriptionService', () => { describe('#isClosed', () => { it('should return true if subscriptions are alive and false if not', () => { - service.subscribe(timer(1000), () => {}); + service.addOne(timer(1000), () => {}); expect(service.isClosed).toBe(false); service['subscription'].unsubscribe(); @@ -43,16 +43,16 @@ describe('SubscriptionService', () => { }); }); - describe('#unsubscribeAll', () => { + describe('#closeAll', () => { it('should close all subscriptions and the parent subscription', () => { - const sub1 = service.subscribe(timer(1000), () => {}); - const sub2 = service.subscribe(timer(1000), () => {}); + const sub1 = service.addOne(timer(1000), () => {}); + const sub2 = service.addOne(timer(1000), () => {}); expect(sub1.closed).toBe(false); expect(sub2.closed).toBe(false); expect(service.isClosed).toBe(false); - service.unsubscribeAll(); + service.closeAll(); expect(sub1.closed).toBe(true); expect(sub2.closed).toBe(true); @@ -62,8 +62,8 @@ describe('SubscriptionService', () => { describe('#reset', () => { it('should close all subscriptions but not the parent subscription', () => { - const sub1 = service.subscribe(timer(1000), () => {}); - const sub2 = service.subscribe(timer(1000), () => {}); + const sub1 = service.addOne(timer(1000), () => {}); + const sub2 = service.addOne(timer(1000), () => {}); expect(sub1.closed).toBe(false); expect(sub2.closed).toBe(false); @@ -77,17 +77,17 @@ describe('SubscriptionService', () => { }); }); - describe('#unsubscribeOne', () => { + describe('#closeOne', () => { it('should unsubscribe from given subscription only', () => { - const sub1 = service.subscribe(timer(1000), () => {}); - const sub2 = service.subscribe(timer(1000), () => {}); + const sub1 = service.addOne(timer(1000), () => {}); + const sub2 = service.addOne(timer(1000), () => {}); expect(service.isClosed).toBe(false); - service.unsubscribeOne(sub1); + service.closeOne(sub1); expect(sub1.closed).toBe(true); expect(service.isClosed).toBe(false); - service.unsubscribeOne(sub2); + service.closeOne(sub2); expect(sub2.closed).toBe(true); expect(service.isClosed).toBe(false); }); @@ -95,8 +95,8 @@ describe('SubscriptionService', () => { describe('#removeOne', () => { it('should remove given subscription from list of subscriptions', () => { - const sub1 = service.subscribe(timer(1000), () => {}); - const sub2 = service.subscribe(timer(1000), () => {}); + const sub1 = service.addOne(timer(1000), () => {}); + const sub2 = service.addOne(timer(1000), () => {}); expect(service.isClosed).toBe(false); service.removeOne(sub1); From 03539decc414093183d4e0e543d312e28d88bbcb Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 22 Jul 2020 14:06:49 +0300 Subject: [PATCH 06/11] docs: update subscription service method names --- docs/en/UI/Angular/Subscription-Service.md | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/en/UI/Angular/Subscription-Service.md b/docs/en/UI/Angular/Subscription-Service.md index b92cd26c59..5d920df96e 100644 --- a/docs/en/UI/Angular/Subscription-Service.md +++ b/docs/en/UI/Angular/Subscription-Service.md @@ -17,12 +17,12 @@ class DemoComponent { count$ = interval(1000); constructor(private subscription: SubscriptionService) { - this.subscription.subscribe(this.count$, console.log); + this.subscription.addOne(this.count$, console.log); } } ``` -The values emitted by the `count$` will be logged until the component is destroyed. You will not have to `unsubscribe` manually. +The values emitted by the `count$` will be logged until the component is destroyed. You will not have to unsubscribe manually. > Please do not try to use a singleton `SubscriptionService`. It simply will not work. @@ -48,7 +48,7 @@ class DemoComponent implements OnInit { return of(null); }; - this.subscription.subscribe(source$, nextFn, errorFn); + this.subscription.addOne(source$, nextFn, errorFn); } } ``` @@ -70,16 +70,16 @@ class DemoComponent implements OnInit { complete: () => console.log('DONE'), }; - this.subscription.subscribe(source$, observer); + this.subscription.addOne(source$, observer); } } ``` -Ths `subscribe` method returns the individual subscription, so that you may use it later on. Please see topics below for details. +The `addOne` method returns the individual subscription, so that you may use it later on. Please see topics below for details. ### How to Unsubscribe Before Instance Destruction -There are two ways to do that. If you are not going to subscribe again, you may use the `unsubscribeAll` method. +There are two ways to do that. If you are not going to subscribe again, you may use the `closeAll` method. ```js @Component({ @@ -90,11 +90,11 @@ class DemoComponent implements OnInit { constructor(private subscription: SubscriptionService) {} ngOnInit() { - this.subscription.subscribe(interval(1000), console.log); + this.subscription.addOne(interval(1000), console.log); } onSomeEvent() { - this.subscription.unsubscribeAll(); + this.subscription.closeAll(); } } ``` @@ -110,19 +110,19 @@ class DemoComponent implements OnInit { constructor(private subscription: SubscriptionService) {} ngOnInit() { - this.subscription.subscribe(interval(1000), console.log); + this.subscription.addOne(interval(1000), console.log); } onSomeEvent() { this.subscription.reset(); - this.subscription.subscribe(interval(1000), console.warn); + this.subscription.addOne(interval(1000), console.warn); } } ``` ### How to Unsubscribe From a Single Subscription -Sometimes, you may need to unsubscribe from a particular subscription but leave others alive. In such a case, you may use the `unsubscribeOne` method. +Sometimes, you may need to unsubscribe from a particular subscription but leave others alive. In such a case, you may use the `closeOne` method. ```js @Component({ @@ -135,14 +135,14 @@ class DemoComponent implements OnInit { constructor(private subscription: SubscriptionService) {} ngOnInit() { - this.countSubscription = this.subscription.subscribe( + this.countSubscription = this.subscription.addOne( interval(1000), console.log ); } onSomeEvent() { - this.subscription.unsubscribeOne(this.countSubscription); + this.subscription.closeOne(this.countSubscription); console.log(this.countSubscription.closed); // true } } @@ -163,7 +163,7 @@ class DemoComponent implements OnInit { constructor(private subscription: SubscriptionService) {} ngOnInit() { - this.countSubscription = this.subscription.subscribe( + this.countSubscription = this.subscription.addOne( interval(1000), console.log ); @@ -178,7 +178,7 @@ class DemoComponent implements OnInit { ### How to Check If Unsubscribed From All -Please use `isClosed` getter to check if `unsubscribeAll` was called before. +Please use `isClosed` getter to check if `closeAll` was called before. ```js @Component({ @@ -189,7 +189,7 @@ class DemoComponent implements OnInit { constructor(private subscription: SubscriptionService) {} ngOnInit() { - this.subscription.subscribe(interval(1000), console.log); + this.subscription.addOne(interval(1000), console.log); } onSomeEvent() { From 8c50cdb7d9e63463affe2f13d9db144ed85a5b83 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 22 Jul 2020 22:54:58 +0300 Subject: [PATCH 07/11] chore(core): deprecate the takeUntilDestroy #4818 --- npm/ng-packs/packages/core/src/lib/utils/rxjs-utils.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/utils/rxjs-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/rxjs-utils.ts index ef6a75fe65..7e77bddba5 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/rxjs-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/rxjs-utils.ts @@ -1,3 +1,4 @@ +// tslint:disable: max-line-length import { Observable, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -5,13 +6,16 @@ function isFunction(value) { return typeof value === 'function'; } +/** + * @deprecated no longer working, please use SubscriptionService (https://docs.abp.io/en/abp/latest/UI/Angular/Subscription-Service) instead. + */ export const takeUntilDestroy = (componentInstance, destroyMethodName = 'ngOnDestroy') => ( - source: Observable + source: Observable, ) => { const originalDestroy = componentInstance[destroyMethodName]; if (isFunction(originalDestroy) === false) { throw new Error( - `${componentInstance.constructor.name} is using untilDestroyed but doesn't implement ${destroyMethodName}` + `${componentInstance.constructor.name} is using untilDestroyed but doesn't implement ${destroyMethodName}`, ); } if (!componentInstance['__takeUntilDestroy']) { From 5ea30d4e66644e08cd2ee9f3fc226127c3758c6d Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 22 Jul 2020 22:55:52 +0300 Subject: [PATCH 08/11] feat: implement SubscriptionService instead of takeUntilDestroy resolves #4818 --- .../auth-wrapper/auth-wrapper.component.ts | 27 ++++++-------- .../components/dynamic-layout.component.ts | 14 ++++---- .../replaceable-route-container.component.ts | 28 +++++++++------ .../src/lib/directives/debounce.directive.ts | 29 ++++++--------- .../lib/directives/form-submit.directive.ts | 36 +++++++++---------- .../lib/directives/permission.directive.ts | 21 ++++++----- .../replaceable-template.directive.ts | 27 +++++++------- .../directives/stop-propagation.directive.ts | 18 +++++----- .../core/src/lib/services/list.service.ts | 21 ++++++++--- .../core/src/lib/services/routes.service.ts | 12 ++++--- .../application-layout.component.ts | 12 +++---- .../breadcrumb/breadcrumb.component.ts | 28 ++++++--------- .../http-error-wrapper.component.ts | 19 +++++----- .../loader-bar/loader-bar.component.ts | 35 +++++++++--------- .../lib/components/modal/modal.component.ts | 17 ++++----- 15 files changed, 175 insertions(+), 169 deletions(-) diff --git a/npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts b/npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts index 23130318b8..be35672862 100644 --- a/npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts @@ -1,21 +1,18 @@ -import { ConfigState, takeUntilDestroy } from '@abp/ng.core'; -import { Component, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; +import { ConfigState, SubscriptionService } from '@abp/ng.core'; +import { Component, Input, OnInit, TemplateRef } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; -import { Account } from '../../models/account'; import { eAccountComponents } from '../../enums/components'; +import { Account } from '../../models/account'; @Component({ selector: 'abp-auth-wrapper', templateUrl: './auth-wrapper.component.html', exportAs: 'abpAuthWrapper', + providers: [SubscriptionService], }) export class AuthWrapperComponent - implements - Account.AuthWrapperComponentInputs, - Account.AuthWrapperComponentOutputs, - OnInit, - OnDestroy { + implements Account.AuthWrapperComponentInputs, Account.AuthWrapperComponentOutputs, OnInit { @Input() readonly mainContentRef: TemplateRef; @@ -29,18 +26,16 @@ export class AuthWrapperComponent tenantBoxKey = eAccountComponents.TenantBox; - constructor(private store: Store) {} + constructor(private store: Store, private subscription: SubscriptionService) {} ngOnInit() { - this.store - .select(ConfigState.getSetting('Abp.Account.EnableLocalLogin')) - .pipe(takeUntilDestroy(this)) - .subscribe(value => { + this.subscription.addOne( + this.store.select(ConfigState.getSetting('Abp.Account.EnableLocalLogin')), + value => { if (value) { this.enableLocalLogin = value.toLowerCase() !== 'false'; } - }); + }, + ); } - - ngOnDestroy() {} } diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts index 7b1395223c..c47b3c02fd 100644 --- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts @@ -1,4 +1,4 @@ -import { Component, Injector, OnDestroy, Optional, SkipSelf, Type } from '@angular/core'; +import { Component, Injector, Optional, SkipSelf, Type } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Store } from '@ngxs/store'; import { eLayoutType } from '../enums/common'; @@ -6,9 +6,9 @@ import { ABP } from '../models'; import { ReplaceableComponents } from '../models/replaceable-components'; import { LocalizationService } from '../services/localization.service'; import { RoutesService } from '../services/routes.service'; +import { SubscriptionService } from '../services/subscription.service'; import { ReplaceableComponentsState } from '../states/replaceable-components.state'; import { findRoute, getRoutePath } from '../utils/route-utils'; -import { takeUntilDestroy } from '../utils/rxjs-utils'; import { TreeNode } from '../utils/tree-utils'; @Component({ @@ -20,8 +20,9 @@ import { TreeNode } from '../utils/tree-utils'; > `, + providers: [SubscriptionService], }) -export class DynamicLayoutComponent implements OnDestroy { +export class DynamicLayoutComponent { layout: Type; // TODO: Consider a shared enum (eThemeSharedComponents) for known layouts @@ -37,6 +38,7 @@ export class DynamicLayoutComponent implements OnDestroy { injector: Injector, private localizationService: LocalizationService, private store: Store, + private subscription: SubscriptionService, @Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent, ) { if (dynamicLayoutComponent) return; @@ -44,7 +46,7 @@ export class DynamicLayoutComponent implements OnDestroy { const router = injector.get(Router); const routes = injector.get(RoutesService); - router.events.pipe(takeUntilDestroy(this)).subscribe(event => { + this.subscription.addOne(router.events, event => { if (event instanceof NavigationEnd) { let expectedLayout = (route.snapshot.data || {}).layout; @@ -73,7 +75,7 @@ export class DynamicLayoutComponent implements OnDestroy { } private listenToLanguageChange() { - this.localizationService.languageChange.pipe(takeUntilDestroy(this)).subscribe(() => { + this.subscription.addOne(this.localizationService.languageChange, () => { this.isLayoutVisible = false; setTimeout(() => (this.isLayoutVisible = true), 0); }); @@ -82,6 +84,4 @@ export class DynamicLayoutComponent implements OnDestroy { private getComponent(key: string): ReplaceableComponents.ReplaceableComponent { return this.store.selectSnapshot(ReplaceableComponentsState.getComponent(key)); } - - ngOnDestroy() {} } diff --git a/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts b/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts index 32d2dadb76..04c596d398 100644 --- a/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts @@ -1,39 +1,45 @@ -import { Component, OnDestroy, OnInit, Type } from '@angular/core'; +import { Component, OnInit, Type } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngxs/store'; import { distinctUntilChanged } from 'rxjs/operators'; -import { ABP } from '../models/common'; import { ReplaceableComponents } from '../models/replaceable-components'; +import { SubscriptionService } from '../services/subscription.service'; import { ReplaceableComponentsState } from '../states/replaceable-components.state'; -import { takeUntilDestroy } from '../utils/rxjs-utils'; @Component({ selector: 'abp-replaceable-route-container', template: ` `, + providers: [SubscriptionService], }) -export class ReplaceableRouteContainerComponent implements OnInit, OnDestroy { +export class ReplaceableRouteContainerComponent implements OnInit { defaultComponent: Type; componentKey: string; externalComponent: Type; - constructor(private route: ActivatedRoute, private store: Store) {} + constructor( + private route: ActivatedRoute, + private store: Store, + private subscription: SubscriptionService, + ) {} ngOnInit() { this.defaultComponent = this.route.snapshot.data.replaceableComponent.defaultComponent; this.componentKey = (this.route.snapshot.data .replaceableComponent as ReplaceableComponents.RouteData).key; - this.store + const component$ = this.store .select(ReplaceableComponentsState.getComponent(this.componentKey)) - .pipe(takeUntilDestroy(this), distinctUntilChanged()) - .subscribe((res = {} as ReplaceableComponents.ReplaceableComponent) => { + .pipe(distinctUntilChanged()); + + this.subscription.addOne( + component$, + (res = {} as ReplaceableComponents.ReplaceableComponent) => { this.externalComponent = res.component; - }); + }, + ); } - - ngOnDestroy() {} } diff --git a/npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts index add5545d06..f35b0c6768 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts @@ -1,34 +1,25 @@ -import { - Directive, - ElementRef, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output, -} from '@angular/core'; -import { takeUntilDestroy } from '../utils/rxjs-utils'; +import { Directive, ElementRef, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { fromEvent } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; +import { SubscriptionService } from '../services/subscription.service'; @Directive({ // tslint:disable-next-line: directive-selector selector: '[input.debounce]', + providers: [SubscriptionService], }) -export class InputEventDebounceDirective implements OnInit, OnDestroy { +export class InputEventDebounceDirective implements OnInit { @Input() debounce = 300; @Output('input.debounce') readonly debounceEvent = new EventEmitter(); - constructor(private el: ElementRef) {} + constructor(private el: ElementRef, private subscription: SubscriptionService) {} ngOnInit(): void { - fromEvent(this.el.nativeElement, 'input') - .pipe(debounceTime(this.debounce), takeUntilDestroy(this)) - .subscribe((event: Event) => { - this.debounceEvent.emit(event); - }); - } + const input$ = fromEvent(this.el.nativeElement, 'input').pipe(debounceTime(this.debounce)); - ngOnDestroy(): void {} + this.subscription.addOne(input$, (event: Event) => { + this.debounceEvent.emit(event); + }); + } } diff --git a/npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts index 4d6c1014db..1d5955dca2 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts @@ -4,7 +4,6 @@ import { ElementRef, EventEmitter, Input, - OnDestroy, OnInit, Output, Self, @@ -12,15 +11,16 @@ import { import { FormControl, FormGroup, FormGroupDirective } from '@angular/forms'; import { fromEvent } from 'rxjs'; import { debounceTime, filter } from 'rxjs/operators'; -import { takeUntilDestroy } from '../utils'; +import { SubscriptionService } from '../services/subscription.service'; type Controls = { [key: string]: FormControl } | FormGroup[]; @Directive({ // tslint:disable-next-line: directive-selector selector: 'form[ngSubmit][formGroup]', + providers: [SubscriptionService], }) -export class FormSubmitDirective implements OnInit, OnDestroy { +export class FormSubmitDirective implements OnInit { @Input() debounce = 200; @@ -35,30 +35,30 @@ export class FormSubmitDirective implements OnInit, OnDestroy { @Self() private formGroupDirective: FormGroupDirective, private host: ElementRef, private cdRef: ChangeDetectorRef, + private subscription: SubscriptionService, ) {} ngOnInit() { - this.formGroupDirective.ngSubmit.pipe(takeUntilDestroy(this)).subscribe(() => { + this.subscription.addOne(this.formGroupDirective.ngSubmit, () => { this.markAsDirty(); this.executedNgSubmit = true; }); - fromEvent(this.host.nativeElement as HTMLElement, 'keyup') - .pipe( - debounceTime(this.debounce), - filter((key: KeyboardEvent) => key && key.key === 'Enter'), - takeUntilDestroy(this), - ) - .subscribe(() => { - if (!this.executedNgSubmit) { - this.host.nativeElement.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); - } + const keyup$ = fromEvent(this.host.nativeElement as HTMLElement, 'keyup').pipe( + debounceTime(this.debounce), + filter((key: KeyboardEvent) => key && key.key === 'Enter'), + ); - this.executedNgSubmit = false; - }); - } + this.subscription.addOne(keyup$, () => { + if (!this.executedNgSubmit) { + this.host.nativeElement.dispatchEvent( + new Event('submit', { bubbles: true, cancelable: true }), + ); + } - ngOnDestroy(): void {} + this.executedNgSubmit = false; + }); + } markAsDirty() { const { form } = this.formGroupDirective; diff --git a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts index 870e4ec144..1d83d1275e 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts @@ -2,19 +2,18 @@ import { Directive, ElementRef, Input, + OnChanges, OnDestroy, OnInit, - Renderer2, - ViewContainerRef, - TemplateRef, Optional, + Renderer2, SimpleChanges, - OnChanges, + TemplateRef, + ViewContainerRef, } from '@angular/core'; import { Store } from '@ngxs/store'; -import { ConfigState } from '../states'; -import { takeUntilDestroy } from '../utils'; import { Subscription } from 'rxjs'; +import { ConfigState } from '../states'; @Directive({ selector: '[abpPermission]', @@ -39,7 +38,6 @@ export class PermissionDirective implements OnInit, OnDestroy, OnChanges { this.subscription = this.store .select(ConfigState.getGrantedPolicy(this.condition)) - .pipe(takeUntilDestroy(this)) .subscribe(isGranted => { if (this.templateRef && isGranted) { this.vcRef.clear(); @@ -47,7 +45,10 @@ export class PermissionDirective implements OnInit, OnDestroy, OnChanges { } else if (this.templateRef && !isGranted) { this.vcRef.clear(); } else if (!isGranted && !this.templateRef) { - this.renderer.removeChild((this.elRef.nativeElement as HTMLElement).parentElement, this.elRef.nativeElement); + this.renderer.removeChild( + (this.elRef.nativeElement as HTMLElement).parentElement, + this.elRef.nativeElement, + ); } }); } @@ -58,7 +59,9 @@ export class PermissionDirective implements OnInit, OnDestroy, OnChanges { } } - ngOnDestroy(): void {} + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } ngOnChanges({ condition }: SimpleChanges) { if ((condition || { currentValue: null }).currentValue) { diff --git a/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts index 91f666137f..fd9051c64e 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts @@ -4,7 +4,6 @@ import { Injector, Input, OnChanges, - OnDestroy, OnInit, SimpleChanges, TemplateRef, @@ -12,17 +11,17 @@ import { ViewContainerRef, } from '@angular/core'; import { Store } from '@ngxs/store'; +import compare from 'just-compare'; import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; +import snq from 'snq'; import { ABP } from '../models/common'; import { ReplaceableComponents } from '../models/replaceable-components'; +import { SubscriptionService } from '../services/subscription.service'; import { ReplaceableComponentsState } from '../states/replaceable-components.state'; -import { takeUntilDestroy } from '../utils/rxjs-utils'; -import compare from 'just-compare'; -import snq from 'snq'; -@Directive({ selector: '[abpReplaceableTemplate]' }) -export class ReplaceableTemplateDirective implements OnInit, OnDestroy, OnChanges { +@Directive({ selector: '[abpReplaceableTemplate]', providers: [SubscriptionService] }) +export class ReplaceableTemplateDirective implements OnInit, OnChanges { @Input('abpReplaceableTemplate') data: ReplaceableComponents.ReplaceableTemplateDirectiveInput; @@ -47,6 +46,7 @@ export class ReplaceableTemplateDirective implements OnInit, OnDestroy, OnChange private cfRes: ComponentFactoryResolver, private vcRef: ViewContainerRef, private store: Store, + private subscription: SubscriptionService, ) { this.context = { initTemplate: ref => { @@ -58,16 +58,18 @@ export class ReplaceableTemplateDirective implements OnInit, OnDestroy, OnChange } ngOnInit() { - this.store + const component$ = this.store .select(ReplaceableComponentsState.getComponent(this.data.componentKey)) .pipe( filter( (res = {} as ReplaceableComponents.ReplaceableComponent) => !this.initialized || !compare(res.component, this.externalComponent), ), - takeUntilDestroy(this), - ) - .subscribe((res = {} as ReplaceableComponents.ReplaceableComponent) => { + ); + + this.subscription.addOne( + component$, + (res = {} as ReplaceableComponents.ReplaceableComponent) => { this.vcRef.clear(); this.externalComponent = res.component; if (this.defaultComponentRef) { @@ -90,7 +92,8 @@ export class ReplaceableTemplateDirective implements OnInit, OnDestroy, OnChange } this.initialized = true; - }); + }, + ); } ngOnChanges(changes: SimpleChanges) { @@ -99,8 +102,6 @@ export class ReplaceableTemplateDirective implements OnInit, OnDestroy, OnChange } } - ngOnDestroy() {} - setDefaultComponentInputs() { if (!this.defaultComponentRef || (!this.data.inputs && !this.data.outputs)) return; diff --git a/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts index ced0ae4159..4faf900232 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts @@ -1,23 +1,25 @@ -import { Directive, ElementRef, EventEmitter, OnInit, Output, OnDestroy } from '@angular/core'; +import { Directive, ElementRef, EventEmitter, OnInit, Output } from '@angular/core'; import { fromEvent } from 'rxjs'; -import { takeUntilDestroy } from '../utils/rxjs-utils'; +import { SubscriptionService } from '../services/subscription.service'; @Directive({ // tslint:disable-next-line: directive-selector selector: '[click.stop]', + providers: [SubscriptionService], }) -export class StopPropagationDirective implements OnInit, OnDestroy { +export class StopPropagationDirective implements OnInit { @Output('click.stop') readonly stopPropEvent = new EventEmitter(); - constructor(private el: ElementRef) {} + constructor(private el: ElementRef, private subscription: SubscriptionService) {} ngOnInit(): void { - fromEvent(this.el.nativeElement, 'click') - .pipe(takeUntilDestroy(this)) - .subscribe((event: MouseEvent) => { + const click$ = this.subscription.addOne( + fromEvent(this.el.nativeElement, 'click'), + (event: MouseEvent) => { event.stopPropagation(); this.stopPropEvent.emit(event); - }); + }, + ); } ngOnDestroy(): void {} diff --git a/npm/ng-packs/packages/core/src/lib/services/list.service.ts b/npm/ng-packs/packages/core/src/lib/services/list.service.ts index d3da082eeb..3ebb6dc193 100644 --- a/npm/ng-packs/packages/core/src/lib/services/list.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/list.service.ts @@ -1,10 +1,17 @@ import { Inject, Injectable, OnDestroy, Optional } from '@angular/core'; -import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; -import { catchError, debounceTime, filter, shareReplay, switchMap, tap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { + catchError, + debounceTime, + filter, + shareReplay, + switchMap, + takeUntil, + tap, +} from 'rxjs/operators'; import { ABP } from '../models/common'; import { PagedResultDto } from '../models/dtos'; import { LIST_QUERY_DEBOUNCE_TIME } from '../tokens/list.token'; -import { takeUntilDestroy } from '../utils/rxjs-utils'; @Injectable() export class ListService implements OnDestroy { @@ -65,6 +72,8 @@ export class ListService implements OnDes private _isLoading$ = new BehaviorSubject(false); + private destroy$ = new Subject(); + get isLoading$(): Observable { return this._isLoading$.asObservable(); } @@ -92,11 +101,13 @@ export class ListService implements OnDes filter(Boolean), tap(() => this._isLoading$.next(false)), shareReplay({ bufferSize: 1, refCount: true }), - takeUntilDestroy(this), + takeUntil(this.destroy$), ); } - ngOnDestroy() {} + ngOnDestroy() { + this.destroy$.next(); + } } export type QueryStreamCreatorCallback = ( diff --git a/npm/ng-packs/packages/core/src/lib/services/routes.service.ts b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts index 2f80d6b44d..c2b90a91c7 100644 --- a/npm/ng-packs/packages/core/src/lib/services/routes.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts @@ -1,10 +1,9 @@ import { Injectable, OnDestroy } from '@angular/core'; import { Actions, ofActionSuccessful, Store } from '@ngxs/store'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { GetAppConfiguration } from '../actions/config.actions'; import { ABP } from '../models/common'; import { ConfigState } from '../states/config.state'; -import { takeUntilDestroy } from '../utils/rxjs-utils'; import { pushValueTo } from '../utils/array-utils'; import { BaseTreeNode, createTreeFromList, TreeNode } from '../utils/tree-utils'; @@ -129,6 +128,7 @@ export abstract class AbstractTreeService { @Injectable() export abstract class AbstractNavTreeService extends AbstractTreeService implements OnDestroy { + private subscription: Subscription; readonly id = 'name'; readonly parentId = 'parentName'; readonly hide = (item: T) => item.invisible || !this.isGranted(item); @@ -142,8 +142,8 @@ export abstract class AbstractNavTreeService extends Abstract constructor(protected actions: Actions, protected store: Store) { super(); - this.actions - .pipe(takeUntilDestroy(this), ofActionSuccessful(GetAppConfiguration)) + this.subscription = this.actions + .pipe(ofActionSuccessful(GetAppConfiguration)) .subscribe(() => this.refresh()); } @@ -162,7 +162,9 @@ export abstract class AbstractNavTreeService extends Abstract } /* istanbul ignore next */ - ngOnDestroy() {} + ngOnDestroy() { + this.subscription.unsubscribe(); + } } @Injectable({ providedIn: 'root' }) diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.ts index d3192057bb..b37516600e 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.ts @@ -1,4 +1,4 @@ -import { eLayoutType, takeUntilDestroy } from '@abp/ng.core'; +import { eLayoutType, SubscriptionService } from '@abp/ng.core'; import { collapseWithMargin, slideFromBottom } from '@abp/ng.theme.shared'; import { AfterViewInit, Component, OnDestroy } from '@angular/core'; import { fromEvent } from 'rxjs'; @@ -9,6 +9,7 @@ import { eThemeBasicComponents } from '../../enums/components'; selector: 'abp-layout-application', templateUrl: './application-layout.component.html', animations: [slideFromBottom, collapseWithMargin], + providers: [SubscriptionService], }) export class ApplicationLayoutComponent implements AfterViewInit, OnDestroy { // required for dynamic component @@ -24,6 +25,8 @@ export class ApplicationLayoutComponent implements AfterViewInit, OnDestroy { navItemsComponentKey = eThemeBasicComponents.NavItems; + constructor(private subscription: SubscriptionService) {} + private checkWindowWidth() { setTimeout(() => { if (window.innerWidth < 992) { @@ -43,11 +46,8 @@ export class ApplicationLayoutComponent implements AfterViewInit, OnDestroy { ngAfterViewInit() { this.checkWindowWidth(); - fromEvent(window, 'resize') - .pipe(takeUntilDestroy(this), debounceTime(150)) - .subscribe(() => { - this.checkWindowWidth(); - }); + const resize$ = fromEvent(window, 'resize').pipe(debounceTime(150)); + this.subscription.addOne(resize$, () => this.checkWindowWidth()); } ngOnDestroy() {} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts index c556776b63..11270c6dc1 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts @@ -1,11 +1,5 @@ -import { ABP, getRoutePath, RoutesService, takeUntilDestroy, TreeNode } from '@abp/ng.core'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - OnDestroy, - OnInit, -} from '@angular/core'; +import { ABP, getRoutePath, RoutesService, TreeNode, SubscriptionService } from '@abp/ng.core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { filter, map, startWith } from 'rxjs/operators'; import { eThemeSharedRouteNames } from '../../enums'; @@ -14,28 +8,27 @@ import { eThemeSharedRouteNames } from '../../enums'; selector: 'abp-breadcrumb', templateUrl: './breadcrumb.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SubscriptionService], }) -export class BreadcrumbComponent implements OnDestroy, OnInit { +export class BreadcrumbComponent implements OnInit { segments: Partial[] = []; constructor( public readonly cdRef: ChangeDetectorRef, private router: Router, private routes: RoutesService, + private subscription: SubscriptionService, ) {} - ngOnDestroy() {} - ngOnInit(): void { - this.router.events - .pipe( - takeUntilDestroy(this), + this.subscription.addOne( + this.router.events.pipe( filter(event => event instanceof NavigationEnd), // tslint:disable-next-line:deprecation startWith(null), map(() => this.routes.search({ path: getRoutePath(this.router) })), - ) - .subscribe(route => { + ), + route => { this.segments = []; if (route) { let node = { parent: route } as TreeNode; @@ -48,7 +41,8 @@ export class BreadcrumbComponent implements OnDestroy, OnInit { this.cdRef.detectChanges(); } - }); + }, + ); } } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts index 0ada431f4f..fb1b2d4a6c 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts @@ -1,4 +1,4 @@ -import { Config, takeUntilDestroy } from '@abp/ng.core'; +import { Config, SubscriptionService } from '@abp/ng.core'; import { AfterViewInit, ApplicationRef, @@ -20,6 +20,7 @@ import snq from 'snq'; selector: 'abp-http-error-wrapper', templateUrl: './http-error-wrapper.component.html', styleUrls: ['http-error-wrapper.component.scss'], + providers: [SubscriptionService], }) export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnInit { appRef: ApplicationRef; @@ -51,6 +52,8 @@ export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnIn return this.status ? `[${this.status}]` : ''; } + constructor(private subscription: SubscriptionService) {} + ngOnInit() { this.backgroundColor = snq(() => window.getComputedStyle(document.body).getPropertyValue('background-color')) || @@ -71,15 +74,11 @@ export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnIn customComponentRef.changeDetectorRef.detectChanges(); } - fromEvent(document, 'keyup') - .pipe( - takeUntilDestroy(this), - debounceTime(150), - filter((key: KeyboardEvent) => key && key.key === 'Escape'), - ) - .subscribe(() => { - this.destroy(); - }); + const keyup$ = fromEvent(document, 'keyup').pipe( + debounceTime(150), + filter((key: KeyboardEvent) => key && key.key === 'Escape'), + ); + this.subscription.addOne(keyup$, () => this.destroy()); } ngOnDestroy() {} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts index 47ff4d057d..8ed30f2cbb 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts @@ -1,7 +1,6 @@ -import { StartLoader, StopLoader } from '@abp/ng.core'; +import { StartLoader, StopLoader, SubscriptionService } from '@abp/ng.core'; import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router'; -import { takeUntilDestroy } from '@ngx-validate/core'; import { Actions, ofActionSuccessful } from '@ngxs/store'; import { Subscription, timer } from 'rxjs'; import { filter } from 'rxjs/operators'; @@ -77,36 +76,38 @@ export class LoaderBarComponent implements OnDestroy, OnInit { return `0 0 10px rgba(${this.color}, 0.5)`; } - constructor(private actions: Actions, private router: Router, private cdRef: ChangeDetectorRef) {} + constructor( + private actions: Actions, + private router: Router, + private cdRef: ChangeDetectorRef, + private subscription: SubscriptionService, + ) {} private subscribeToLoadActions() { - this.actions - .pipe( - ofActionSuccessful(StartLoader, StopLoader), - filter(this.filter), - takeUntilDestroy(this), - ) - .subscribe(action => { + this.subscription.addOne( + this.actions.pipe(ofActionSuccessful(StartLoader, StopLoader), filter(this.filter)), + action => { if (action instanceof StartLoader) this.startLoading(); else this.stopLoading(); - }); + }, + ); } private subscribeToRouterEvents() { - this.router.events - .pipe( + this.subscription.addOne( + this.router.events.pipe( filter( event => event instanceof NavigationStart || event instanceof NavigationEnd || event instanceof NavigationError, ), - takeUntilDestroy(this), - ) - .subscribe(event => { + ), + event => { if (event instanceof NavigationStart) this.startLoading(); else this.stopLoading(); - }); + }, + ); } ngOnInit() { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts index 83d62f0b5a..b16b2bb14c 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts @@ -1,4 +1,4 @@ -import { takeUntilDestroy } from '@abp/ng.core'; +import { SubscriptionService } from '@abp/ng.core'; import { Component, ContentChild, @@ -27,7 +27,7 @@ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl'; templateUrl: './modal.component.html', animations: [fadeAnimation], styleUrls: ['./modal.component.scss'], - providers: [ModalService], + providers: [ModalService, SubscriptionService], }) export class ModalComponent implements OnDestroy { @Input() @@ -60,11 +60,11 @@ export class ModalComponent implements OnDestroy { @ContentChild(ButtonComponent, { static: false, read: ButtonComponent }) abpSubmit: ButtonComponent; - @ContentChild('abpHeader', {static: false}) abpHeader: TemplateRef; + @ContentChild('abpHeader', { static: false }) abpHeader: TemplateRef; - @ContentChild('abpBody', {static: false}) abpBody: TemplateRef; + @ContentChild('abpBody', { static: false }) abpBody: TemplateRef; - @ContentChild('abpFooter', {static: false}) abpFooter: TemplateRef; + @ContentChild('abpFooter', { static: false }) abpFooter: TemplateRef; @ContentChild('abpClose', { static: false, read: ElementRef }) abpClose: ElementRef; @@ -103,14 +103,15 @@ export class ModalComponent implements OnDestroy { private renderer: Renderer2, private confirmationService: ConfirmationService, private modalService: ModalService, + private subscription: SubscriptionService, ) { this.initToggleStream(); } private initToggleStream() { - this.toggle$ - .pipe(takeUntilDestroy(this), debounceTime(0), distinctUntilChanged()) - .subscribe(value => this.toggle(value)); + this.subscription.addOne(this.toggle$.pipe(debounceTime(0), distinctUntilChanged()), value => + this.toggle(value), + ); } private toggle(value: boolean) { From 5cc0de966da2200a6e9f4f904ef832b422bcea18 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 22 Jul 2020 23:14:40 +0300 Subject: [PATCH 09/11] fix: avoid a lint error and fix some testing errors --- .../core/src/lib/directives/permission.directive.ts | 2 +- .../lib/directives/stop-propagation.directive.ts | 13 ++++--------- .../components/loader-bar/loader-bar.component.ts | 1 + .../src/lib/tests/loader-bar.component.spec.ts | 11 +++++++++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts index 1d83d1275e..fda8b7d315 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts @@ -60,7 +60,7 @@ export class PermissionDirective implements OnInit, OnDestroy, OnChanges { } ngOnDestroy(): void { - this.subscription.unsubscribe(); + if (this.subscription) this.subscription.unsubscribe(); } ngOnChanges({ condition }: SimpleChanges) { diff --git a/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts index 4faf900232..12c035cc19 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts @@ -13,14 +13,9 @@ export class StopPropagationDirective implements OnInit { constructor(private el: ElementRef, private subscription: SubscriptionService) {} ngOnInit(): void { - const click$ = this.subscription.addOne( - fromEvent(this.el.nativeElement, 'click'), - (event: MouseEvent) => { - event.stopPropagation(); - this.stopPropEvent.emit(event); - }, - ); + this.subscription.addOne(fromEvent(this.el.nativeElement, 'click'), (event: MouseEvent) => { + event.stopPropagation(); + this.stopPropEvent.emit(event); + }); } - - ngOnDestroy(): void {} } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts index 8ed30f2cbb..f94b6b3656 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts @@ -21,6 +21,7 @@ import { filter } from 'rxjs/operators'; `, styleUrls: ['./loader-bar.component.scss'], + providers: [SubscriptionService], }) export class LoaderBarComponent implements OnDestroy, OnInit { protected _isLoading: boolean; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts index e363dacfd1..a369c97b37 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts @@ -1,9 +1,15 @@ -import { Router, RouteReuseStrategy, NavigationStart, NavigationEnd, NavigationError } from '@angular/router'; +import { + Router, + RouteReuseStrategy, + NavigationStart, + NavigationEnd, + NavigationError, +} from '@angular/router'; import { createHostFactory, SpectatorHost, SpyObject } from '@ngneat/spectator/jest'; import { Actions, NgxsModule, Store } from '@ngxs/store'; import { Subject, Subscription, Observable, Subscriber, timer } from 'rxjs'; import { LoaderBarComponent } from '../components/loader-bar/loader-bar.component'; -import { StartLoader, StopLoader } from '@abp/ng.core'; +import { StartLoader, StopLoader, SubscriptionService } from '@abp/ng.core'; import { HttpRequest } from '@angular/common/http'; describe('LoaderBarComponent', () => { @@ -16,6 +22,7 @@ describe('LoaderBarComponent', () => { mocks: [Router], imports: [NgxsModule.forRoot()], detectChanges: false, + providers: [SubscriptionService], }); beforeEach(() => { From ca08dd77e5bfb5eace07d50c963114e8d577afc8 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 23 Jul 2020 12:32:34 +0300 Subject: [PATCH 10/11] docs: remove booksType from tutorial part 2 --- docs/en/Tutorials/Part-2.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 690394264c..8eae95648c 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -474,7 +474,7 @@ Open the `/src/app/book/book.component.ts` file and replace the content as below ```js import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from './models'; +import { BookDto } from './models'; import { BookService } from './services'; @Component({ @@ -486,8 +486,6 @@ import { BookService } from './services'; export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; - constructor(public readonly list: ListService, private bookService: BookService) {} ngOnInit() { From df34cea1d63aac118d0349c8b60f0032bc7a0a58 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Thu, 23 Jul 2020 12:37:27 +0300 Subject: [PATCH 11/11] docs: place bookType in tutorial part 3 --- docs/en/Tutorials/Part-3.md | 38 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md index 3b31ec1c88..b8593a5ac5 100644 --- a/docs/en/Tutorials/Part-3.md +++ b/docs/en/Tutorials/Part-3.md @@ -643,7 +643,7 @@ Open `/src/app/book/book.component.ts` and replace the content as below: ```js import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from './models'; +import { BookDto } from './models'; import { BookService } from './services'; @Component({ @@ -655,8 +655,6 @@ import { BookService } from './services'; export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; - isModalOpen = false; // add this line constructor(public readonly list: ListService, private bookService: BookService) {} @@ -738,7 +736,7 @@ Open `/src/app/book/book.component.ts` and replace the content as below: ```js import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from './models'; +import { BookDto, BookType } from './models'; // add BookType import { BookService } from './services'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this @@ -751,13 +749,13 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; - form: FormGroup; // add this line - // add bookTypes as a list of enum members - bookTypes = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' + bookType = BookType; // add this line + + // add bookTypes as a list of BookType enum members + bookTypes = Object.keys(this.bookType).filter( + (key) => typeof this.bookType[key] === 'number' ); isModalOpen = false; @@ -808,7 +806,8 @@ export class BookComponent implements OnInit { * Imported `FormGroup`, `FormBuilder` and `Validators` from `@angular/forms`. * Added `form: FormGroup` property. -* Add `bookTypes` as a list of `BookType` enum members. +* Added `bookType` property so that you can reach `BookType` enum members from template. +* Added `bookTypes` property as a list of `BookType` enum members. That will be used in form options. * Injected `FormBuilder` into the constructor. [FormBuilder](https://angular.io/api/forms/FormBuilder) provides convenient methods for generating form controls. It reduces the amount of boilerplate needed to build complex forms. * Added `buildForm` method to the end of the file and executed the `buildForm()` in the `createBook` method. * Added `save` method. @@ -832,7 +831,7 @@ Open `/src/app/book/book.component.html` and replace ` Type * @@ -917,13 +916,12 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; - form: FormGroup; - // <== added bookTypes array ==> - bookTypes = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' + bookType = BookType; + + bookTypes = Object.keys(this.bookType).filter( + (key) => typeof this.bookType[key] === 'number' ); isModalOpen = false; @@ -998,14 +996,14 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; + selectedBook = new BookDto(); // declare selectedBook form: FormGroup; - selectedBook = new BookDto(); // declare selectedBook + bookType = BookType; - bookTypes = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' + bookTypes = Object.keys(this.bookType).filter( + (key) => typeof this.bookType[key] === 'number' ); isModalOpen = false;