Browse Source

Merge pull request #4838 from abpframework/feat/4837

Created a Utility Service For Subscription Management
pull/4849/head
Erol Arkat 6 years ago
committed by GitHub
parent
commit
436ad0ff91
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 203
      docs/en/UI/Angular/Subscription-Service.md
  2. 2
      docs/en/UI/Angular/Track-By-Service.md
  3. 4
      docs/en/docs-nav.json
  4. 1
      npm/ng-packs/packages/core/src/lib/services/index.ts
  5. 54
      npm/ng-packs/packages/core/src/lib/services/subscription.service.ts
  6. 109
      npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts

203
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.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.
> 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.addOne(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.addOne(source$, observer);
}
}
```
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 `closeAll` method.
```js
@Component({
/* class metadata here */
providers: [SubscriptionService],
})
class DemoComponent implements OnInit {
constructor(private subscription: SubscriptionService) {}
ngOnInit() {
this.subscription.addOne(interval(1000), console.log);
}
onSomeEvent() {
this.subscription.closeAll();
}
}
```
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.addOne(interval(1000), console.log);
}
onSomeEvent() {
this.subscription.reset();
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 `closeOne` method.
```js
@Component({
/* class metadata here */
providers: [SubscriptionService],
})
class DemoComponent implements OnInit {
countSubscription: Subscription;
constructor(private subscription: SubscriptionService) {}
ngOnInit() {
this.countSubscription = this.subscription.addOne(
interval(1000),
console.log
);
}
onSomeEvent() {
this.subscription.closeOne(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.addOne(
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 `closeAll` was called before.
```js
@Component({
/* class metadata here */
providers: [SubscriptionService],
})
class DemoComponent implements OnInit {
constructor(private subscription: SubscriptionService) {}
ngOnInit() {
this.subscription.addOne(interval(1000), console.log);
}
onSomeEvent() {
console.log(this.subscription.isClosed); // false
}
}
```
## What's Next?
- [ListService](./List-Service.md)

2
docs/en/UI/Angular/Track-By-Service.md

@ -116,4 +116,4 @@ class DemoComponent {
## What's Next? ## What's Next?
- [ListService](./List-Service.md) - [SubscriptionService](./Subscription-Service.md)

4
docs/en/docs-nav.json

@ -434,6 +434,10 @@
"text": "TrackByService", "text": "TrackByService",
"path": "UI/Angular/Track-By-Service.md" "path": "UI/Angular/Track-By-Service.md"
}, },
{
"text": "SubscriptionService",
"path": "UI/Angular/Subscription-Service.md"
},
{ {
"text": "ListService", "text": "ListService",
"path": "UI/Angular/List-Service.md" "path": "UI/Angular/List-Service.md"

1
npm/ng-packs/packages/core/src/lib/services/index.ts

@ -11,4 +11,5 @@ export * from './profile.service';
export * from './rest.service'; export * from './rest.service';
export * from './routes.service'; export * from './routes.service';
export * from './session-state.service'; export * from './session-state.service';
export * from './subscription.service';
export * from './track-by.service'; export * from './track-by.service';

54
npm/ng-packs/packages/core/src/lib/services/subscription.service.ts

@ -0,0 +1,54 @@
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;
}
addOne<T extends unknown>(
source$: Observable<T>,
next?: (value: T) => void,
error?: (error: any) => void,
): Subscription;
addOne<T extends unknown>(source$: Observable<T>, observer?: PartialObserver<T>): Subscription;
addOne<T extends unknown>(
source$: Observable<T>,
nextOrObserver?: PartialObserver<T> | Next<T>,
error?: (error: any) => void,
): Subscription {
const subscription = source$.subscribe(nextOrObserver as Next<T>, error);
this.subscription.add(subscription);
return subscription;
}
closeAll() {
this.subscription.unsubscribe();
}
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<T> = (value: T) => void;

109
npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts

@ -0,0 +1,109 @@
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('#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.addOne(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.addOne(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.addOne(timer(1000), () => {});
expect(service.isClosed).toBe(false);
service['subscription'].unsubscribe();
expect(service.isClosed).toBe(true);
});
});
describe('#closeAll', () => {
it('should close all subscriptions and the parent subscription', () => {
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.closeAll();
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.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.reset();
expect(sub1.closed).toBe(true);
expect(sub2.closed).toBe(true);
expect(service.isClosed).toBe(false);
});
});
describe('#closeOne', () => {
it('should unsubscribe from given subscription only', () => {
const sub1 = service.addOne(timer(1000), () => {});
const sub2 = service.addOne(timer(1000), () => {});
expect(service.isClosed).toBe(false);
service.closeOne(sub1);
expect(sub1.closed).toBe(true);
expect(service.isClosed).toBe(false);
service.closeOne(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.addOne(timer(1000), () => {});
const sub2 = service.addOne(timer(1000), () => {});
expect(service.isClosed).toBe(false);
service.removeOne(sub1);
expect(sub1.closed).toBe(false);
expect(service.isClosed).toBe(false);
sub1.unsubscribe();
});
});
});
Loading…
Cancel
Save