Merge branch 'master' into tutorial-2

pull/4864/head
Halil İbrahim Kalkan 5 years ago
commit 3d2393b182

@ -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.
See the [ABP CLI documentation](./CLI.md) for more information.

@ -476,7 +476,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({
@ -488,8 +488,6 @@ import { BookService } from './services';
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
constructor(public readonly list: ListService, private bookService: BookService) {}
ngOnInit() {

@ -648,7 +648,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({
@ -660,8 +660,6 @@ import { BookService } from './services';
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
isModalOpen = false; // add this line
constructor(public readonly list: ListService, private bookService: BookService) {}
@ -743,7 +741,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
@ -756,13 +754,13 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
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;
@ -813,7 +811,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.
@ -837,7 +836,7 @@ Open `/src/app/book/book.component.html` and replace `<ng-template #abpBody> </n
<label for="book-type">Type</label><span> * </span>
<select class="form-control" id="book-type" formControlName="type">
<option [ngValue]="null">Select a book type</option>
<option [ngValue]="booksType[type]" *ngFor="let type of bookTypes"> {%{{{ type }}}%}</option>
<option [ngValue]="bookType[type]" *ngFor="let type of bookTypes"> {%{{{ type }}}%}</option>
</select>
</div>
@ -922,13 +921,12 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
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;
@ -1003,14 +1001,14 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
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;

@ -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)

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

@ -454,6 +454,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"

@ -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<any>;
@ -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() {}
}

@ -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';
><ng-container *ngIf="isLayoutVisible" [ngComponentOutlet]="layout"></ng-container
></ng-template>
`,
providers: [SubscriptionService],
})
export class DynamicLayoutComponent implements OnDestroy {
export class DynamicLayoutComponent {
layout: Type<any>;
// 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() {}
}

@ -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: `
<ng-container *ngComponentOutlet="externalComponent || defaultComponent"></ng-container>
`,
providers: [SubscriptionService],
})
export class ReplaceableRouteContainerComponent implements OnInit, OnDestroy {
export class ReplaceableRouteContainerComponent implements OnInit {
defaultComponent: Type<any>;
componentKey: string;
externalComponent: Type<any>;
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() {}
}

@ -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<Event>();
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);
});
}
}

@ -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<HTMLFormElement>,
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;

@ -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 {
if (this.subscription) this.subscription.unsubscribe();
}
ngOnChanges({ condition }: SimpleChanges) {
if ((condition || { currentValue: null }).currentValue) {

@ -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<any, any>;
@ -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;

@ -1,24 +1,21 @@
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<MouseEvent>();
constructor(private el: ElementRef) {}
constructor(private el: ElementRef, private subscription: SubscriptionService) {}
ngOnInit(): void {
fromEvent(this.el.nativeElement, 'click')
.pipe(takeUntilDestroy(this))
.subscribe((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 {}
}

@ -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';

@ -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<QueryParamsType = ABP.PageQueryParams> implements OnDestroy {
@ -65,6 +72,8 @@ export class ListService<QueryParamsType = ABP.PageQueryParams> implements OnDes
private _isLoading$ = new BehaviorSubject(false);
private destroy$ = new Subject();
get isLoading$(): Observable<boolean> {
return this._isLoading$.asObservable();
}
@ -92,11 +101,13 @@ export class ListService<QueryParamsType = ABP.PageQueryParams> 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<T, QueryParamsType = ABP.PageQueryParams> = (

@ -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<T extends object> {
@Injectable()
export abstract class AbstractNavTreeService<T extends ABP.Nav> extends AbstractTreeService<T>
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<T extends ABP.Nav> 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<T extends ABP.Nav> extends Abstract
}
/* istanbul ignore next */
ngOnDestroy() {}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
@Injectable({ providedIn: 'root' })

@ -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;

@ -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();
});
});
});

@ -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') => <T>(
source: Observable<T>
source: Observable<T>,
) => {
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']) {

@ -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() {}

@ -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<ABP.Route>[] = [];
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<NavigationEnd>(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<ABP.Route>;
@ -48,7 +41,8 @@ export class BreadcrumbComponent implements OnDestroy, OnInit {
this.cdRef.detectChanges();
}
});
},
);
}
}

@ -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() {}

@ -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';
@ -22,6 +21,7 @@ import { filter } from 'rxjs/operators';
</div>
`,
styleUrls: ['./loader-bar.component.scss'],
providers: [SubscriptionService],
})
export class LoaderBarComponent implements OnDestroy, OnInit {
protected _isLoading: boolean;
@ -77,36 +77,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() {

@ -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<any>;
@ContentChild('abpHeader', { static: false }) abpHeader: TemplateRef<any>;
@ContentChild('abpBody', {static: false}) abpBody: TemplateRef<any>;
@ContentChild('abpBody', { static: false }) abpBody: TemplateRef<any>;
@ContentChild('abpFooter', {static: false}) abpFooter: TemplateRef<any>;
@ContentChild('abpFooter', { static: false }) abpFooter: TemplateRef<any>;
@ContentChild('abpClose', { static: false, read: ElementRef })
abpClose: ElementRef<any>;
@ -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) {

@ -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(() => {

Loading…
Cancel
Save