From 5f154e14852caa1fb28695ddaeb9b7188fbbd45c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 30 Jan 2019 16:16:21 +0100 Subject: [PATCH 01/16] Started with stateful component. --- .../pages/users/user-page.component.ts | 2 +- .../pages/users/users-page.component.html | 2 +- .../state/event-consumers.state.ts | 2 +- .../administration/state/users.state.ts | 2 +- .../contents/contents-page.component.html | 2 +- .../shared/contents-selector.component.html | 2 +- .../events/rule-events-page.component.html | 2 +- .../app/framework/angular/code.component.ts | 9 +- .../forms/checkbox-group.component.html | 6 +- .../angular/forms/checkbox-group.component.ts | 52 +++++---- .../angular/forms/code-editor.component.ts | 14 ++- .../forms/control-errors.component.html | 4 +- .../angular/forms/control-errors.component.ts | 55 +++++----- .../angular/forms/json-editor.component.ts | 5 +- .../angular/forms/progress-bar.component.ts | 5 +- .../angular/forms/stars.component.html | 4 +- .../angular/forms/stars.component.ts | 71 ++++++------ .../angular/forms/toggle.component.html | 6 +- .../angular/forms/toggle.component.ts | 53 +++++---- .../angular/image-source.directive.ts | 2 +- .../modals/dialog-renderer.component.html | 6 +- .../modals/dialog-renderer.component.ts | 103 ++++++++---------- .../angular/modals/modal-dialog.component.ts | 4 +- .../modals/onboarding-tooltip.component.ts | 56 ++++------ .../angular/modals/root-view.component.ts | 10 +- .../angular/modals/tooltip.component.ts | 37 ++----- .../framework/angular/pager.component.html | 4 +- .../app/framework/angular/pager.component.ts | 20 ++-- .../app/framework/angular/panel.component.ts | 9 +- .../angular/pipes/date-time.pipes.ts | 3 +- .../app/framework/angular/pipes/money.pipe.ts | 2 +- .../app/framework/angular/pipes/name.pipe.ts | 2 +- .../angular/shortcut.component.spec.ts | 12 +- .../framework/angular/shortcut.component.ts | 21 ++-- .../framework/angular/stateful.component.ts | 58 ++++++++++ .../app/framework/angular/title.component.ts | 2 +- .../angular/user-report.component.ts | 38 +++---- src/Squidex/app/framework/internal.ts | 2 + src/Squidex/app/framework/state.ts | 11 +- .../components/assets-list.component.html | 2 +- src/Squidex/app/shared/state/apps.state.ts | 4 +- src/Squidex/app/shared/state/assets.state.ts | 2 +- .../app/shared/state/contents.forms.ts | 2 +- src/Squidex/app/shared/state/schemas.state.ts | 2 +- 44 files changed, 395 insertions(+), 317 deletions(-) create mode 100644 src/Squidex/app/framework/angular/stateful.component.ts diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.ts b/src/Squidex/app/features/administration/pages/users/user-page.component.ts index 3be7e69a6..27d10c5ce 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.ts +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.ts @@ -42,7 +42,7 @@ export class UserPageComponent implements OnDestroy, OnInit { this.selectedUserSubscription = this.usersState.selectedUser .subscribe(selectedUser => { - this.user = selectedUser; + this.user = selectedUser!; if (selectedUser) { this.userForm.load(selectedUser.user); diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/src/Squidex/app/features/administration/pages/users/users-page.component.html index 4a8963e8f..3270262c4 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -81,7 +81,7 @@ diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.ts b/src/Squidex/app/features/administration/state/event-consumers.state.ts index e98fd0cf8..ae16e1f21 100644 --- a/src/Squidex/app/features/administration/state/event-consumers.state.ts +++ b/src/Squidex/app/features/administration/state/event-consumers.state.ts @@ -21,7 +21,7 @@ import { EventConsumerDto, EventConsumersService } from './../services/event-con interface Snapshot { eventConsumers: ImmutableArray; - isLoaded?: false; + isLoaded?: boolean; } @Injectable() diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/src/Squidex/app/features/administration/state/users.state.ts index 85266dcb9..05482ff9a 100644 --- a/src/Squidex/app/features/administration/state/users.state.ts +++ b/src/Squidex/app/features/administration/state/users.state.ts @@ -98,7 +98,7 @@ interface Snapshot { isLoaded?: boolean; - selectedUser?: SnapshotUser; + selectedUser?: SnapshotUser | null; } @Injectable() diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index dc771f257..7d7e8067d 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -120,7 +120,7 @@ diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.html b/src/Squidex/app/features/content/shared/contents-selector.component.html index 263b77df6..d73ff6b2c 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.html +++ b/src/Squidex/app/features/content/shared/contents-selector.component.html @@ -63,7 +63,7 @@ diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html index c39cde203..b02c39f7b 100644 --- a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html @@ -90,7 +90,7 @@ - + diff --git a/src/Squidex/app/framework/angular/code.component.ts b/src/Squidex/app/framework/angular/code.component.ts index ec02060f5..7ae539d76 100644 --- a/src/Squidex/app/framework/angular/code.component.ts +++ b/src/Squidex/app/framework/angular/code.component.ts @@ -5,7 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; + +import { PureComponent } from '@app/framework/internal'; @Component({ selector: 'sqx-code', @@ -13,5 +15,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; templateUrl: './code.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class CodeComponent { +export class CodeComponent extends PureComponent { + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector); + } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.html b/src/Squidex/app/framework/angular/forms/checkbox-group.component.html index 0f2a18bc1..de5b90311 100644 --- a/src/Squidex/app/framework/angular/forms/checkbox-group.component.html +++ b/src/Squidex/app/framework/angular/forms/checkbox-group.component.html @@ -1,9 +1,9 @@ - + [disabled]="snapshot.isDisabled"> - + \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts b/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts index 34c311056..90dc09607 100644 --- a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts +++ b/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts @@ -8,14 +8,22 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Types } from '@app/framework/internal'; - -import { MathHelper } from '../../utils/math-helper'; +import { + MathHelper, + StatefulComponent, + Types +} from '@app/framework/internal'; export const SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckboxGroupComponent), multi: true }; +interface State { + checkedValues: string[]; + controlId: string; + isDisabled: boolean; +} + @Component({ selector: 'sqx-checkbox-group', styleUrls: ['./checkbox-group.component.scss'], @@ -23,33 +31,27 @@ export const SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class CheckboxGroupComponent implements ControlValueAccessor { +export class CheckboxGroupComponent extends StatefulComponent implements ControlValueAccessor { private callChange = (v: any) => { /* NOOP */ }; private callTouched = () => { /* NOOP */ }; - private checkedValues: string[] = []; @Input() public values: string[] = []; - public isDisabled = false; - - public control = MathHelper.guid(); - - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + controlId: MathHelper.guid(), + checkedValues: [], + isDisabled: false + }); } public writeValue(obj: any) { - this.checkedValues = Types.isArrayOfString(obj) ? obj.filter(x => this.values.indexOf(x) >= 0) : []; - - this.changeDetector.markForCheck(); + this.next({ checkedValues: Types.isArrayOfString(obj) ? obj.filter(x => this.values.indexOf(x) >= 0) : [] }); } public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); + this.next({ isDisabled }); } public registerOnChange(fn: any) { @@ -65,16 +67,20 @@ export class CheckboxGroupComponent implements ControlValueAccessor { } public check(isChecked: boolean, value: string) { + let checkedValues = this.snapshot.checkedValues; + if (isChecked) { - this.checkedValues = [value, ...this.checkedValues]; + checkedValues = [value, ...checkedValues]; } else { - this.checkedValues = this.checkedValues.filter(x => x !== value); + checkedValues = checkedValues.filter(x => x !== value); } - this.callChange(this.checkedValues); - } + this.next({ checkedValues }); + + this.callChange(checkedValues); + } public isChecked(value: string) { - return this.checkedValues.indexOf(value) >= 0; + return this.snapshot.checkedValues.indexOf(value) >= 0; } } diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.ts b/src/Squidex/app/framework/angular/forms/code-editor.component.ts index fd7cf17ec..7af142c50 100644 --- a/src/Squidex/app/framework/angular/forms/code-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/code-editor.component.ts @@ -5,12 +5,16 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; -import { ResourceLoaderService, Types } from '@app/framework/internal'; +import { + PureComponent, + ResourceLoaderService, + Types +} from '@app/framework/internal'; declare var ace: any; @@ -25,7 +29,7 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class CodeEditorComponent implements ControlValueAccessor, AfterViewInit { +export class CodeEditorComponent extends PureComponent implements ControlValueAccessor, AfterViewInit { private callChange = (v: any) => { /* NOOP */ }; private callTouched = () => { /* NOOP */ }; private valueChanged = new Subject(); @@ -40,8 +44,12 @@ export class CodeEditorComponent implements ControlValueAccessor, AfterViewInit public mode = 'ace/mode/javascript'; constructor( + changeDetector: ChangeDetectorRef, private readonly resourceLoader: ResourceLoaderService ) { + super(changeDetector); + + changeDetector.detach(); } public writeValue(obj: any) { diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.html b/src/Squidex/app/framework/angular/forms/control-errors.component.html index 5cab06ca2..3626cf842 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.html +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.html @@ -1,6 +1,6 @@ -
+
- + {{message}}
diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/src/Squidex/app/framework/angular/forms/control-errors.component.ts index 673975dfc..1e2215f0b 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.ts @@ -7,12 +7,20 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnChanges, OnDestroy, Optional } from '@angular/core'; import { AbstractControl, FormGroupDirective } from '@angular/forms'; -import { merge, Subscription } from 'rxjs'; +import { merge } from 'rxjs'; -import { fadeAnimation, Types } from '@app/framework/internal'; +import { + fadeAnimation, + StatefulComponent, + Types +} from '@app/framework/internal'; import { formatError } from './error-formatting'; +interface State { + errorMessages: string[]; +} + @Component({ selector: 'sqx-control-errors', styleUrls: ['./control-errors.component.scss'], @@ -22,10 +30,9 @@ import { formatError } from './error-formatting'; ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ControlErrorsComponent implements OnChanges, OnDestroy { +export class ControlErrorsComponent extends StatefulComponent implements OnChanges, OnDestroy { private displayFieldName: string; private control: AbstractControl; - private controlSubscription: Subscription | null = null; private originalMarkAsTouched: any; @Input() @@ -43,16 +50,20 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { @Input() public submitOnly = false; - public errorMessages: string[] = []; - - constructor( - @Optional() @Host() private readonly formGroupDirective: FormGroupDirective, - private readonly changeDetector: ChangeDetectorRef + constructor(changeDetector: ChangeDetectorRef, + @Optional() @Host() private readonly formGroupDirective: FormGroupDirective ) { + super(changeDetector, { + errorMessages: [] + }); } public ngOnDestroy() { - this.unsubscribe(); + super.ngOnDestroy(); + + if (this.control && this.originalMarkAsTouched) { + this.control['markAsTouched'] = this.originalMarkAsTouched; + } } public ngOnChanges() { @@ -75,16 +86,16 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { } if (this.control !== control) { - this.unsubscribe(); + this.ngOnDestroy(); this.control = control; if (control) { - this.controlSubscription = + this.observe( merge(control.valueChanges, control.statusChanges) .subscribe(() => { this.createMessages(); - }); + })); this.originalMarkAsTouched = this.control.markAsTouched; @@ -101,18 +112,8 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { this.createMessages(); } - private unsubscribe() { - if (this.controlSubscription) { - this.controlSubscription.unsubscribe(); - } - - if (this.control && this.originalMarkAsTouched) { - this.control['markAsTouched'] = this.originalMarkAsTouched; - } - } - private createMessages() { - const errors: string[] = []; + const errorMessages: string[] = []; if (this.control && this.control.invalid && ((this.control.touched && !this.submitOnly) || this.submitted) && this.control.errors) { for (let key in this.control.errors) { @@ -120,14 +121,12 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { const message = formatError(this.displayFieldName, key, this.control.errors[key], this.control.value, this.errors); if (message) { - errors.push(message); + errorMessages.push(message); } } } } - this.errorMessages = errors; - - this.changeDetector.markForCheck(); + this.next({ errorMessages }); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.ts b/src/Squidex/app/framework/angular/forms/json-editor.component.ts index 35f84d0b5..77e3adf1d 100644 --- a/src/Squidex/app/framework/angular/forms/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/json-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; @@ -37,9 +37,10 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit @ViewChild('editor') public editor: ElementRef; - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly resourceLoader: ResourceLoaderService ) { + changeDetector.detach(); } public writeValue(obj: any) { diff --git a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts b/src/Squidex/app/framework/angular/forms/progress-bar.component.ts index a5f300c0f..4a4d99649 100644 --- a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts +++ b/src/Squidex/app/framework/angular/forms/progress-bar.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; import * as ProgressBar from 'progressbar.js'; @@ -38,10 +38,11 @@ export class ProgressBarComponent implements OnChanges, OnInit { @Input() public value = 0; - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly element: ElementRef, private readonly renderer: Renderer2 ) { + changeDetector.detach(); } public ngOnInit() { diff --git a/src/Squidex/app/framework/angular/forms/stars.component.html b/src/Squidex/app/framework/angular/forms/stars.component.html index 4f325a931..4cf2ca64f 100644 --- a/src/Squidex/app/framework/angular/forms/stars.component.html +++ b/src/Squidex/app/framework/angular/forms/stars.component.html @@ -4,8 +4,8 @@
- - + + diff --git a/src/Squidex/app/framework/angular/forms/stars.component.ts b/src/Squidex/app/framework/angular/forms/stars.component.ts index 26f45ef20..3a9772f9b 100644 --- a/src/Squidex/app/framework/angular/forms/stars.component.ts +++ b/src/Squidex/app/framework/angular/forms/stars.component.ts @@ -8,12 +8,21 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Types } from '@app/framework/internal'; +import { StatefulComponent, Types } from '@app/framework/internal'; export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarsComponent), multi: true }; +interface State { + isDisabled: boolean; + + stars: number; + starsArray: number[]; + + value: number | null; +} + @Component({ selector: 'sqx-stars', styleUrls: ['./stars.component.scss'], @@ -21,7 +30,7 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class StarsComponent implements ControlValueAccessor { +export class StarsComponent extends StatefulComponent implements ControlValueAccessor { private callChange = (v: any) => { /* NOOP */ }; private callTouched = () => { /* NOOP */ }; private maximumStarsValue = 5; @@ -33,11 +42,13 @@ export class StarsComponent implements ControlValueAccessor { if (this.maximumStarsValue !== maxStars) { this.maximumStarsValue = value; - this.starsArray = []; + const starsArray = []; for (let i = 1; i <= value; i++) { - this.starsArray.push(i); + starsArray.push(i); } + + this.next({ starsArray }); } } @@ -45,28 +56,23 @@ export class StarsComponent implements ControlValueAccessor { return this.maximumStarsValue; } - public isDisabled = false; - - public stars: number; - public starsArray: number[] = [1, 2, 3, 4, 5]; - - public value: number | null = 1; - - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + isDisabled: false, + stars: -1, + starsArray: [1, 2, 3, 4, 5], + value: 1 + }); } public writeValue(obj: any) { - this.value = this.stars = Types.isNumber(obj) ? obj : 0; + const value = Types.isNumber(obj) ? obj : 0; - this.changeDetector.markForCheck(); + this.next({ stars: value, value }); } public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); + this.next({ isDisabled }); } public registerOnChange(fn: any) { @@ -77,32 +83,31 @@ export class StarsComponent implements ControlValueAccessor { this.callTouched = fn; } - public setPreview(value: number) { - if (this.isDisabled) { + public setPreview(stars: number) { + if (this.snapshot.isDisabled) { return; } - this.stars = value; + this.next({ stars }); } public stopPreview() { - if (this.isDisabled) { + if (this.snapshot.isDisabled) { return; } - this.stars = this.value || 0; + this.next(s => { s.stars = s.value || 0; }); } public reset() { - if (this.isDisabled) { + if (this.snapshot.isDisabled) { return false; } - if (this.value) { - this.value = null; - this.stars = 0; + if (this.snapshot.value) { + this.next({ stars: -1, value: null }); - this.callChange(this.value); + this.callChange(null); this.callTouched(); } @@ -110,14 +115,14 @@ export class StarsComponent implements ControlValueAccessor { } public setValue(value: number) { - if (this.isDisabled) { + if (this.snapshot.isDisabled) { return false; } - if (this.value !== value) { - this.value = this.stars = value; + if (this.snapshot.value !== value) { + this.next({ stars: value, value }); - this.callChange(this.value); + this.callChange(value); this.callTouched(); } diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.html b/src/Squidex/app/framework/angular/forms/toggle.component.html index 8c774acff..ecdde36e5 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.html +++ b/src/Squidex/app/framework/angular/forms/toggle.component.html @@ -1,6 +1,6 @@
+ [class.disabled]="snapshot.isDisabled" + [class.checked]="snapshot.isChecked === true" + [class.unchecked]="snapshot.isChecked === false">
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/src/Squidex/app/framework/angular/forms/toggle.component.ts index 1524d908b..94052ac3c 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.ts +++ b/src/Squidex/app/framework/angular/forms/toggle.component.ts @@ -5,47 +5,48 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; +import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Types } from '@app/framework/internal'; +import { StatefulComponent } from '../stateful.component'; + export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToggleComponent), multi: true }; +interface State { + isDisabled: boolean; + isChecked: boolean | null; +} + @Component({ selector: 'sqx-toggle', styleUrls: ['./toggle.component.scss'], templateUrl: './toggle.component.html', - providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR], - changeDetection: ChangeDetectionStrategy.OnPush + providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] }) -export class ToggleComponent implements ControlValueAccessor { +export class ToggleComponent extends StatefulComponent implements ControlValueAccessor { private callChange = (v: any) => { /* NOOP */ }; private callTouched = () => { /* NOOP */ }; @Input() public threeStates = false; - public isChecked: boolean | null = null; - public isDisabled = false; - - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + isChecked: null, + isDisabled: false + }); } public writeValue(obj: any) { - this.isChecked = Types.isBoolean(obj) ? obj : null; - - this.changeDetector.markForCheck(); + this.next({ isChecked: Types.isBoolean(obj) ? obj : null }); } public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); + this.next({ isDisabled }); } public registerOnChange(fn: any) { @@ -57,23 +58,27 @@ export class ToggleComponent implements ControlValueAccessor { } public changeState(event: MouseEvent) { - if (this.isDisabled) { + let { isDisabled, isChecked } = this.snapshot; + + if (isDisabled) { return; } if (this.threeStates && (event.ctrlKey || event.shiftKey)) { - if (this.isChecked) { - this.isChecked = null; - } else if (this.isChecked === null) { - this.isChecked = false; + if (isChecked) { + isChecked = null; + } else if (isChecked === null) { + isChecked = false; } else { - this.isChecked = true; + isChecked = true; } } else { - this.isChecked = !(this.isChecked === true); + isChecked = !(isChecked === true); } - this.callChange(this.isChecked); + this.next({ isChecked }); + + this.callChange(isChecked); this.callTouched(); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/image-source.directive.ts b/src/Squidex/app/framework/angular/image-source.directive.ts index c9406b524..cdfb16fd7 100644 --- a/src/Squidex/app/framework/angular/image-source.directive.ts +++ b/src/Squidex/app/framework/angular/image-source.directive.ts @@ -7,7 +7,7 @@ import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, Renderer2 } from '@angular/core'; -import { MathHelper } from './../utils/math-helper'; +import { MathHelper } from '@app/framework/internal'; const LAYOUT_CACHE: { [key: string]: { width: number, height: number } } = {}; diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html index f39ac71d5..0dcb911a0 100644 --- a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html +++ b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html @@ -2,11 +2,11 @@ - {{dialogRequest?.title}} + {{snapshot.dialogRequest?.title}} - {{dialogRequest?.text}} + {{snapshot.dialogRequest?.text}} @@ -16,7 +16,7 @@
-
+
diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts index 17c184f0c..335a19da0 100644 --- a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts +++ b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts @@ -5,8 +5,8 @@ * Copyright (c) Sebastian Stehle. All rights r vbeserved */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { timer } from 'rxjs'; import { DialogModel, @@ -16,6 +16,14 @@ import { Notification } from '@app/framework/internal'; +import { StatefulComponent } from '../stateful.component'; + +interface State { + dialogRequest?: DialogRequest | null; + + notifications: Notification[]; +} + @Component({ selector: 'sqx-dialog-renderer', styleUrls: ['./dialog-renderer.component.scss'], @@ -25,89 +33,74 @@ import { ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DialogRendererComponent implements OnDestroy, OnInit { - private dialogSubscription: Subscription; - private dialogsSubscription: Subscription; - private notificationsSubscription: Subscription; - - public dialogView = new DialogModel(); - public dialogRequest: DialogRequest | null = null; - - public notifications: Notification[] = []; - +export class DialogRendererComponent extends StatefulComponent implements OnInit { @Input() public position = 'bottomright'; - constructor( - private readonly changeDetector: ChangeDetectorRef, + public dialogView = new DialogModel(); + + constructor(changeDetector: ChangeDetectorRef, private readonly dialogs: DialogService ) { - } - - public ngOnDestroy() { - this.notificationsSubscription.unsubscribe(); - this.dialogSubscription.unsubscribe(); - this.dialogsSubscription.unsubscribe(); + super(changeDetector, { notifications: [] }); } public ngOnInit() { - this.dialogSubscription = + this.observe( this.dialogView.isOpen.subscribe(isOpen => { if (!isOpen) { - this.cancel(); - - this.changeDetector.detectChanges(); + this.finishRequest(false); } - }); + })); - this.notificationsSubscription = + this.observe( this.dialogs.notifications.subscribe(notification => { - this.notifications.push(notification); + this.next(state => { + state.notifications = [...state.notifications, notification]; + }); if (notification.displayTime > 0) { - setTimeout(() => { + this.observe(timer(notification.displayTime).subscribe(() => { this.close(notification); - }, notification.displayTime); + })); } + })); - this.changeDetector.detectChanges(); - }); - - this.dialogsSubscription = + this.observe( this.dialogs.dialogs .subscribe(request => { this.cancel(); - this.dialogRequest = request; - this.dialogView.show(); - - this.changeDetector.detectChanges(); - }); + this.next(state => { + state.dialogRequest = request; + }); + })); } public cancel() { - if (this.dialogRequest) { - this.dialogRequest.complete(false); - this.dialogRequest = null; - this.dialogView.hide(); - } + this.finishRequest(false); + + this.dialogView.hide(); } public confirm() { - if (this.dialogRequest) { - this.dialogRequest.complete(true); - this.dialogRequest = null; - this.dialogView.hide(); - } - } + this.finishRequest(true); - public close(notification: Notification) { - const index = this.notifications.indexOf(notification); + this.dialogView.hide(); + } - if (index >= 0) { - this.notifications.splice(index, 1); + private finishRequest(value: boolean) { + this.next(state => { + if (state.dialogRequest) { + state.dialogRequest.complete(value); + state.dialogRequest = null; + } + }); + } - this.changeDetector.detectChanges(); - } + public close(notification: Notification) { + this.next(state => { + state.notifications = state.notifications.filter(n => notification !== n); + }); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/modal-dialog.component.ts b/src/Squidex/app/framework/angular/modals/modal-dialog.component.ts index 20c685ff0..1282a4cb5 100644 --- a/src/Squidex/app/framework/angular/modals/modal-dialog.component.ts +++ b/src/Squidex/app/framework/angular/modals/modal-dialog.component.ts @@ -7,7 +7,7 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { fadeAnimation } from './../animations'; +import { fadeAnimation } from '@app/framework/internal'; @Component({ selector: 'sqx-modal-dialog', @@ -16,7 +16,7 @@ import { fadeAnimation } from './../animations'; animations: [ fadeAnimation ], - changeDetection: ChangeDetectionStrategy.Default + changeDetection: ChangeDetectionStrategy.OnPush }) export class ModalDialogComponent implements AfterViewInit { @Input() diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts b/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts index 14a8fc598..70b6c1cb3 100644 --- a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts @@ -6,11 +6,13 @@ */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { timer } from 'rxjs'; import { fadeAnimation, ModalModel, OnboardingService, + PureComponent, Types } from '@app/framework/internal'; @@ -23,11 +25,7 @@ import { ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class OnboardingTooltipComponent implements OnDestroy, OnInit { - private showTimer: any; - private closeTimer: any; - private forMouseDownListener: Function | null; - +export class OnboardingTooltipComponent extends PureComponent implements OnDestroy, OnInit { public tooltipModal = new ModalModel(); @Input() @@ -42,56 +40,50 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit { @Input() public position = 'left'; - constructor( - private readonly changeDetector: ChangeDetectorRef, + constructor(changeDetector: ChangeDetectorRef, private readonly onboardingService: OnboardingService, private readonly renderer: Renderer2 ) { + super(changeDetector); } public ngOnDestroy() { - clearTimeout(this.showTimer); - clearTimeout(this.closeTimer); + super.ngOnDestroy(); this.tooltipModal.hide(); - - if (this.forMouseDownListener) { - this.forMouseDownListener(); - this.forMouseDownListener = null; - } } public ngOnInit() { if (this.for && this.helpId && Types.isFunction(this.for.addEventListener)) { - this.showTimer = setTimeout(() => { - if (this.onboardingService.shouldShow(this.helpId)) { - const forRect = this.for.getBoundingClientRect(); - - const x = forRect.left + 0.5 * forRect.width; - const y = forRect.top + 0.5 * forRect.height; + this.observe( + timer(this.after).subscribe(() => { + if (this.onboardingService.shouldShow(this.helpId)) { + const forRect = this.for.getBoundingClientRect(); - const fromPoint = document.elementFromPoint(x, y); + const x = forRect.left + 0.5 * forRect.width; + const y = forRect.top + 0.5 * forRect.height; - if (this.isSameOrParent(fromPoint)) { - this.tooltipModal.show(); + const fromPoint = document.elementFromPoint(x, y); - this.changeDetector.markForCheck(); + if (this.isSameOrParent(fromPoint)) { + this.tooltipModal.show(); - this.closeTimer = setTimeout(() => { - this.hideThis(); - }, 10000); + this.observe( + timer(10000).subscribe(() => { + this.hideThis(); + })); - this.onboardingService.disable(this.helpId); + this.onboardingService.disable(this.helpId); + } } - } - }, this.after); + })); - this.forMouseDownListener = + this.observe( this.renderer.listen(this.for, 'mousedown', () => { this.onboardingService.disable(this.helpId); this.hideThis(); - }); + })); } } diff --git a/src/Squidex/app/framework/angular/modals/root-view.component.ts b/src/Squidex/app/framework/angular/modals/root-view.component.ts index 2f3ec887c..2bd9862dd 100644 --- a/src/Squidex/app/framework/angular/modals/root-view.component.ts +++ b/src/Squidex/app/framework/angular/modals/root-view.component.ts @@ -5,7 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, ViewChild, ViewContainerRef } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewChild, ViewContainerRef } from '@angular/core'; + +import { PureComponent } from '@app/framework/internal'; @Component({ selector: 'sqx-root-view', @@ -13,7 +15,11 @@ import { ChangeDetectionStrategy, Component, ViewChild, ViewContainerRef } from templateUrl: './root-view.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class RootViewComponent { +export class RootViewComponent extends PureComponent { @ViewChild('element', { read: ViewContainerRef }) public viewContainer: ViewContainerRef; + + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector); + } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/tooltip.component.ts b/src/Squidex/app/framework/angular/modals/tooltip.component.ts index e58c03243..8a0e558a8 100644 --- a/src/Squidex/app/framework/angular/modals/tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/tooltip.component.ts @@ -7,9 +7,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; -import { ModalModel } from './../../utils/modal-view'; - -import { fadeAnimation } from './../animations'; +import { + fadeAnimation, + ModalModel, + PureComponent +} from '@app/framework/internal'; @Component({ selector: 'sqx-tooltip', @@ -20,10 +22,7 @@ import { fadeAnimation } from './../animations'; ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TooltipComponent implements OnDestroy, OnInit { - private targetMouseEnterListener: any; - private targetMouseLeaveListener: any; - +export class TooltipComponent extends PureComponent implements OnDestroy, OnInit { @Input() public target: any; @@ -32,35 +31,23 @@ export class TooltipComponent implements OnDestroy, OnInit { public modal = new ModalModel(); - constructor( - private readonly changeDetector: ChangeDetectorRef, + constructor(changeDetector: ChangeDetectorRef, private readonly renderer: Renderer2 ) { - } - - public ngOnDestroy() { - if (this.targetMouseEnterListener) { - this.targetMouseEnterListener(); - } - - if (this.targetMouseLeaveListener) { - this.targetMouseLeaveListener(); - } + super(changeDetector); } public ngOnInit() { if (this.target) { - this.targetMouseEnterListener = + this.observe( this.renderer.listen(this.target, 'mouseenter', () => { this.modal.show(); + })); - this.changeDetector.markForCheck(); - }); - - this.targetMouseLeaveListener = + this.observe( this.renderer.listen(this.target, 'mouseleave', () => { this.modal.hide(); - }); + })); } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/pager.component.html b/src/Squidex/app/framework/angular/pager.component.html index cd328ec9d..47899d0c8 100644 --- a/src/Squidex/app/framework/angular/pager.component.html +++ b/src/Squidex/app/framework/angular/pager.component.html @@ -2,10 +2,10 @@ diff --git a/src/Squidex/app/framework/angular/pager.component.ts b/src/Squidex/app/framework/angular/pager.component.ts index ce88403d7..dce6f8f8b 100644 --- a/src/Squidex/app/framework/angular/pager.component.ts +++ b/src/Squidex/app/framework/angular/pager.component.ts @@ -5,9 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; -import { Pager } from './../internal'; +import { Pager, PureComponent } from '@app/framework/internal'; @Component({ selector: 'sqx-pager', @@ -15,16 +15,20 @@ import { Pager } from './../internal'; templateUrl: './pager.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class PagerComponent { +export class PagerComponent extends PureComponent { + @Output() + public nextPage = new EventEmitter(); + + @Output() + public prevPage = new EventEmitter(); + @Input() public pager: Pager; @Input() public hideWhenButtonsDisabled = false; - @Output() - public next = new EventEmitter(); - - @Output() - public prev = new EventEmitter(); + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector); + } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/src/Squidex/app/framework/angular/panel.component.ts index 4556ed9b7..68bdb01d4 100644 --- a/src/Squidex/app/framework/angular/panel.component.ts +++ b/src/Squidex/app/framework/angular/panel.component.ts @@ -5,9 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; -import { slideRightAnimation } from './animations'; +import { PureComponent, slideRightAnimation } from '@app/framework/internal'; import { PanelContainerDirective } from './panel-container.directive'; @@ -20,7 +20,7 @@ import { PanelContainerDirective } from './panel-container.directive'; ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { +export class PanelComponent extends PureComponent implements AfterViewInit, OnDestroy, OnInit { private styleWidth: string; public renderWidth = 0; @@ -61,10 +61,11 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { @ViewChild('panel') public panel: ElementRef; - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly container: PanelContainerDirective, private readonly renderer: Renderer2 ) { + super(changeDetector); } public ngOnDestroy() { diff --git a/src/Squidex/app/framework/angular/pipes/date-time.pipes.ts b/src/Squidex/app/framework/angular/pipes/date-time.pipes.ts index 145014c81..1da8095fc 100644 --- a/src/Squidex/app/framework/angular/pipes/date-time.pipes.ts +++ b/src/Squidex/app/framework/angular/pipes/date-time.pipes.ts @@ -7,8 +7,7 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { DateTime } from './../../utils/date-time'; -import { Duration } from './../../utils/duration'; +import { DateTime, Duration } from '@app/framework/internal'; @Pipe({ name: 'sqxShortDate', diff --git a/src/Squidex/app/framework/angular/pipes/money.pipe.ts b/src/Squidex/app/framework/angular/pipes/money.pipe.ts index 8508ae21b..0c4cc4f71 100644 --- a/src/Squidex/app/framework/angular/pipes/money.pipe.ts +++ b/src/Squidex/app/framework/angular/pipes/money.pipe.ts @@ -7,7 +7,7 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { CurrencyConfig, DecimalSeparatorConfig } from './../../configurations'; +import { CurrencyConfig, DecimalSeparatorConfig } from '@app/framework/internal'; @Pipe({ name: 'sqxMoney', diff --git a/src/Squidex/app/framework/angular/pipes/name.pipe.ts b/src/Squidex/app/framework/angular/pipes/name.pipe.ts index 49e12ec4c..c82448a55 100644 --- a/src/Squidex/app/framework/angular/pipes/name.pipe.ts +++ b/src/Squidex/app/framework/angular/pipes/name.pipe.ts @@ -7,7 +7,7 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { StringHelper } from './../../utils/string-helper'; +import { StringHelper } from '@app/framework/internal'; @Pipe({ name: 'sqxDisplayName', diff --git a/src/Squidex/app/framework/angular/shortcut.component.spec.ts b/src/Squidex/app/framework/angular/shortcut.component.spec.ts index 146285756..ff725a20f 100644 --- a/src/Squidex/app/framework/angular/shortcut.component.spec.ts +++ b/src/Squidex/app/framework/angular/shortcut.component.spec.ts @@ -18,13 +18,13 @@ describe('ShortcutComponent', () => { }); it('should instantiate', () => { - const shortcutComponent = new ShortcutComponent(shortcutService, new NgZone({})); + const shortcutComponent = new ShortcutComponent({}, shortcutService, new NgZone({})); expect(shortcutComponent).toBeDefined(); }); it('should init without keys', () => { - const shortcutComponent = new ShortcutComponent(shortcutService, new NgZone({})); + const shortcutComponent = new ShortcutComponent({}, shortcutService, new NgZone({})); shortcutComponent.keys = null!; shortcutComponent.ngOnInit(); @@ -33,7 +33,7 @@ describe('ShortcutComponent', () => { }); it('should destroy without keys', () => { - const shortcutComponent = new ShortcutComponent(shortcutService, new NgZone({})); + const shortcutComponent = new ShortcutComponent({}, shortcutService, new NgZone({})); shortcutComponent.keys = null!; shortcutComponent.ngOnDestroy(); @@ -42,7 +42,7 @@ describe('ShortcutComponent', () => { }); it('should raise event when triggered', () => { - const shortcutComponent = new ShortcutComponent(shortcutService, new NgZone({})); + const shortcutComponent = new ShortcutComponent({}, shortcutService, new NgZone({})); let isTriggered = false; @@ -56,7 +56,7 @@ describe('ShortcutComponent', () => { }); it('should not raise event when triggered but disabled', () => { - const shortcutComponent = new ShortcutComponent(shortcutService, new NgZone({})); + const shortcutComponent = new ShortcutComponent({}, shortcutService, new NgZone({})); let isTriggered = false; @@ -71,7 +71,7 @@ describe('ShortcutComponent', () => { }); it('should not raise event when triggered but destroyed', () => { - const shortcutComponent = new ShortcutComponent(shortcutService, new NgZone({})); + const shortcutComponent = new ShortcutComponent({}, shortcutService, new NgZone({})); let isTriggered = false; diff --git a/src/Squidex/app/framework/angular/shortcut.component.ts b/src/Squidex/app/framework/angular/shortcut.component.ts index 26191b2eb..c264be77d 100644 --- a/src/Squidex/app/framework/angular/shortcut.component.ts +++ b/src/Squidex/app/framework/angular/shortcut.component.ts @@ -5,16 +5,17 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; -import { ShortcutService } from './../services/shortcut.service'; +import { PureComponent, ShortcutService } from '@app/framework/internal'; @Component({ selector: 'sqx-shortcut', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush + template: '' }) -export class ShortcutComponent implements OnDestroy, OnInit { +export class ShortcutComponent extends PureComponent implements OnDestroy, OnInit { + private lastKeys: string; + @Input() public keys: string; @@ -24,12 +25,14 @@ export class ShortcutComponent implements OnDestroy, OnInit { @Output() public trigger = new EventEmitter(); - private lastKeys: string; - constructor( + changeDetector: ChangeDetectorRef, private readonly shortcutService: ShortcutService, private readonly zone: NgZone ) { + super(changeDetector); + + changeDetector.detach(); } public ngOnDestroy() { @@ -42,10 +45,10 @@ export class ShortcutComponent implements OnDestroy, OnInit { this.lastKeys = this.keys; if (this.lastKeys) { - this.shortcutService.on(this.lastKeys, e => { + this.shortcutService.on(this.lastKeys, () => { if (!this.disabled) { this.zone.run(() => { - this.trigger.next(e); + this.trigger.next(); }); } diff --git a/src/Squidex/app/framework/angular/stateful.component.ts b/src/Squidex/app/framework/angular/stateful.component.ts new file mode 100644 index 000000000..330ecaa34 --- /dev/null +++ b/src/Squidex/app/framework/angular/stateful.component.ts @@ -0,0 +1,58 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectorRef, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; + +import { Types } from './../utils/types'; + +import { State } from '../state'; + +declare type UnsubscribeFunction = () => void; + +export abstract class StatefulComponent extends State implements OnDestroy, OnInit { + private subscriptions: (Subscription | UnsubscribeFunction)[] = []; + + constructor( + private readonly changeDetector: ChangeDetectorRef, + state: T + ) { + super(state); + } + + protected observe(subscription: Subscription | UnsubscribeFunction) { + if (subscription) { + this.subscriptions.push(subscription); + } + } + + public ngOnInit() { + this.changes.subscribe(() => { + this.changeDetector.detectChanges(); + }); + } + + public ngOnDestroy() { + try { + for (let subscription of this.subscriptions) { + if (Types.isFunction(subscription)) { + subscription(); + } else { + subscription.unsubscribe(); + } + } + } finally { + this.subscriptions = []; + } + } +} + +export abstract class PureComponent extends StatefulComponent { + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, {}); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/title.component.ts b/src/Squidex/app/framework/angular/title.component.ts index 20161b09a..63510e7dc 100644 --- a/src/Squidex/app/framework/angular/title.component.ts +++ b/src/Squidex/app/framework/angular/title.component.ts @@ -7,7 +7,7 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; -import { TitleService } from './../services/title.service'; +import { TitleService } from '@app/framework/internal'; @Component({ selector: 'sqx-title', diff --git a/src/Squidex/app/framework/angular/user-report.component.ts b/src/Squidex/app/framework/angular/user-report.component.ts index 0cb33edb7..ed197e543 100644 --- a/src/Squidex/app/framework/angular/user-report.component.ts +++ b/src/Squidex/app/framework/angular/user-report.component.ts @@ -5,36 +5,36 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { timer } from 'rxjs'; -import { UserReportConfig } from './../configurations'; -import { ResourceLoaderService } from './../services/resource-loader.service'; +import { + PureComponent, + ResourceLoaderService, + UserReportConfig +} from '@app/framework/internal'; @Component({ selector: 'sqx-user-report', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush + template: '' }) -export class UserReportComponent implements OnDestroy, OnInit { - private loadingTimer: any; - - constructor(config: UserReportConfig, changeDetector: ChangeDetectorRef, +export class UserReportComponent extends PureComponent implements OnDestroy, OnInit { + constructor(changeDetector: ChangeDetectorRef, + private readonly config: UserReportConfig, private readonly resourceLoader: ResourceLoaderService ) { - changeDetector.detach(); - - window['_urq'] = window['_urq'] || []; - window['_urq'].push(['initSite', config.siteId]); - } + super(changeDetector); - public ngOnDestroy() { - clearTimeout(this.loadingTimer); + changeDetector.detach(); } public ngOnInit() { - this.loadingTimer = - setTimeout(() => { + window['_urq'] = window['_urq'] || []; + window['_urq'].push(['initSite', this.config.siteId]); + + this.observe( + timer(4000).subscribe(() => { this.resourceLoader.loadScript('https://cdn.userreport.com/userreport.js'); - }, 4000); + })); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/internal.ts b/src/Squidex/app/framework/internal.ts index e738bbd3d..0f1556ebe 100644 --- a/src/Squidex/app/framework/internal.ts +++ b/src/Squidex/app/framework/internal.ts @@ -35,4 +35,6 @@ export * from './utils/string-helper'; export * from './utils/types'; export * from './utils/version'; +export * from './angular/stateful.component'; + export * from './configurations'; \ No newline at end of file diff --git a/src/Squidex/app/framework/state.ts b/src/Squidex/app/framework/state.ts index 03ed7e181..d7690801d 100644 --- a/src/Squidex/app/framework/state.ts +++ b/src/Squidex/app/framework/state.ts @@ -15,7 +15,7 @@ import { fullValue} from './angular/forms/forms-helper'; export interface FormState { submitted: boolean; - error?: string; + error?: string | null; } export class Form { @@ -136,11 +136,14 @@ export class State { this.next(this.initialState); } - public next(update: ((v: T) => T) | object) { + public next(update: ((v: T) => T | void) | Partial) { if (Types.isFunction(update)) { - this.state.next(update(this.state.value)); + const stateNew = { ...this.snapshot }; + const stateUpdated = update(stateNew); + + this.state.next(stateUpdated || stateNew); } else { - this.state.next(Object.assign({}, this.snapshot, update)); + this.state.next({ ...this.snapshot, ...update }); } } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/assets-list.component.html b/src/Squidex/app/shared/components/assets-list.component.html index d2f849a28..9133d6d70 100644 --- a/src/Squidex/app/shared/components/assets-list.component.html +++ b/src/Squidex/app/shared/components/assets-list.component.html @@ -35,4 +35,4 @@
- \ No newline at end of file + \ No newline at end of file diff --git a/src/Squidex/app/shared/state/apps.state.ts b/src/Squidex/app/shared/state/apps.state.ts index 1eff17ed3..ec4a13c2d 100644 --- a/src/Squidex/app/shared/state/apps.state.ts +++ b/src/Squidex/app/shared/state/apps.state.ts @@ -67,9 +67,9 @@ export class AppsState extends State { public load(): Observable { return this.appsService.getApps().pipe( - tap(dtos => { + tap((dto: AppDto[]) => { this.next(s => { - const apps = ImmutableArray.of(dtos); + const apps = ImmutableArray.of(dto); return { ...s, apps }; }); diff --git a/src/Squidex/app/shared/state/assets.state.ts b/src/Squidex/app/shared/state/assets.state.ts index 39f08819d..8563f4ee6 100644 --- a/src/Squidex/app/shared/state/assets.state.ts +++ b/src/Squidex/app/shared/state/assets.state.ts @@ -28,7 +28,7 @@ interface Snapshot { assetsPager: Pager; assetsQuery?: string; - isLoaded?: false; + isLoaded?: boolean; } @Injectable() diff --git a/src/Squidex/app/shared/state/contents.forms.ts b/src/Squidex/app/shared/state/contents.forms.ts index 02f96ce36..956d5e255 100644 --- a/src/Squidex/app/shared/state/contents.forms.ts +++ b/src/Squidex/app/shared/state/contents.forms.ts @@ -147,7 +147,7 @@ export class FieldValidatorsFactory implements FieldPropertiesVisitor 0) { diff --git a/src/Squidex/app/shared/state/schemas.state.ts b/src/Squidex/app/shared/state/schemas.state.ts index a4f7aa4de..0ae83627d 100644 --- a/src/Squidex/app/shared/state/schemas.state.ts +++ b/src/Squidex/app/shared/state/schemas.state.ts @@ -325,7 +325,7 @@ export class SchemasState extends State { private replaceSchema(schema: SchemaDto) { return this.next(s => { const schemas = s.schemas.replaceBy('id', schema).sortByStringAsc(x => x.displayName); - const selectedSchema = s.selectedSchema && s.selectedSchema.id === schema.id ? schema : s.selectedSchema; + const selectedSchema = Types.is(schema, SchemaDetailsDto) && s.selectedSchema && s.selectedSchema.id === schema.id ? schema : s.selectedSchema; const categories = buildCategories(s.categories, schemas); From c6dcce7269fffd9a500c38cfbff1ce456f37746b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 30 Jan 2019 17:40:32 +0100 Subject: [PATCH 02/16] Test --- .../forms/checkbox-group.component.html | 6 +- .../angular/forms/checkbox-group.component.ts | 35 +--- .../angular/forms/code-editor.component.ts | 18 +- .../forms/date-time-editor.component.html | 10 +- .../forms/date-time-editor.component.ts | 47 ++--- .../angular/forms/iframe-editor.component.ts | 30 +-- .../angular/forms/json-editor.component.ts | 20 +- .../angular/forms/slider.component.html | 3 - .../angular/forms/slider.component.scss | 51 ----- .../angular/forms/slider.component.ts | 196 ------------------ .../angular/forms/stars.component.ts | 21 +- .../angular/forms/tag-editor.component.ts | 63 +++--- .../angular/forms/toggle.component.ts | 23 +- .../framework/angular/stateful.component.ts | 67 ++++++ src/Squidex/app/framework/declarations.ts | 1 - src/Squidex/app/framework/module.ts | 3 - 16 files changed, 144 insertions(+), 450 deletions(-) delete mode 100644 src/Squidex/app/framework/angular/forms/slider.component.html delete mode 100644 src/Squidex/app/framework/angular/forms/slider.component.scss delete mode 100644 src/Squidex/app/framework/angular/forms/slider.component.ts diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.html b/src/Squidex/app/framework/angular/forms/checkbox-group.component.html index de5b90311..f3245e6f2 100644 --- a/src/Squidex/app/framework/angular/forms/checkbox-group.component.html +++ b/src/Squidex/app/framework/angular/forms/checkbox-group.component.html @@ -1,9 +1,9 @@ - - + \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts b/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts index 90dc09607..a426f93cc 100644 --- a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts +++ b/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts @@ -6,11 +6,11 @@ */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { MathHelper, - StatefulComponent, + StatefulControlComponent, Types } from '@app/framework/internal'; @@ -20,8 +20,6 @@ export const SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR: any = { interface State { checkedValues: string[]; - controlId: string; - isDisabled: boolean; } @Component({ @@ -31,39 +29,22 @@ interface State { providers: [SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class CheckboxGroupComponent extends StatefulComponent implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class CheckboxGroupComponent extends StatefulControlComponent { + public readonly controlId = MathHelper.guid(); @Input() public values: string[] = []; constructor(changeDetector: ChangeDetectorRef) { super(changeDetector, { - controlId: MathHelper.guid(), - checkedValues: [], - isDisabled: false + checkedValues: [] }); } public writeValue(obj: any) { - this.next({ checkedValues: Types.isArrayOfString(obj) ? obj.filter(x => this.values.indexOf(x) >= 0) : [] }); - } - - public setDisabledState(isDisabled: boolean): void { - this.next({ isDisabled }); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } + const checkedValues = Types.isArrayOfString(obj) ? obj.filter(x => this.values.indexOf(x) >= 0) : []; - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - - public blur() { - this.callTouched(); + this.next({ checkedValues }); } public check(isChecked: boolean, value: string) { @@ -78,7 +59,7 @@ export class CheckboxGroupComponent extends StatefulComponent implements this.next({ checkedValues }); this.callChange(checkedValues); - } + } public isChecked(value: string) { return this.snapshot.checkedValues.indexOf(value) >= 0; diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.ts b/src/Squidex/app/framework/angular/forms/code-editor.component.ts index 7af142c50..056ebfe55 100644 --- a/src/Squidex/app/framework/angular/forms/code-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/code-editor.component.ts @@ -6,12 +6,12 @@ */ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; import { - PureComponent, + ExternalControlComponent, ResourceLoaderService, Types } from '@app/framework/internal'; @@ -29,9 +29,7 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class CodeEditorComponent extends PureComponent implements ControlValueAccessor, AfterViewInit { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class CodeEditorComponent extends ExternalControlComponent implements AfterViewInit { private valueChanged = new Subject(); private aceEditor: any; private value: string; @@ -48,8 +46,6 @@ export class CodeEditorComponent extends PureComponent implements ControlValueAc private readonly resourceLoader: ResourceLoaderService ) { super(changeDetector); - - changeDetector.detach(); } public writeValue(obj: any) { @@ -68,14 +64,6 @@ export class CodeEditorComponent extends PureComponent implements ControlValueAc } } - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public ngAfterViewInit() { this.valueChanged.pipe( debounceTime(500)) diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.html b/src/Squidex/app/framework/angular/forms/date-time-editor.component.html index 730e2f679..0bca5eb55 100644 --- a/src/Squidex/app/framework/angular/forms/date-time-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/date-time-editor.component.html @@ -1,19 +1,19 @@
- +
- +
- +
- +
- +
diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts index eee5c1890..cca58a42b 100644 --- a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts @@ -5,12 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import * as moment from 'moment'; -import { Subscription } from 'rxjs'; -import { Types } from '@app/framework/internal'; +import { StatefulControlComponent, Types } from '@app/framework/internal'; declare module 'pikaday/pikaday'; @@ -27,15 +26,11 @@ export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, OnInit, AfterViewInit { - private timeSubscription: Subscription; - private dateSubscription: Subscription; +export class DateTimeEditorComponent extends StatefulControlComponent<{}, string | null> implements OnInit, AfterViewInit { private picker: any; private timeValue: any | null = null; private dateValue: any | null = null; private suppressEvents = false; - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; @Input() public mode: string; @@ -49,8 +44,6 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, @ViewChild('dateInput') public dateInput: ElementRef; - public isDisabled = false; - public timeControl = new FormControl(); public dateControl = new FormControl(); @@ -62,18 +55,12 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, return !!this.dateValue; } - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { - } - - public ngOnDestroy() { - this.dateSubscription.unsubscribe(); - this.timeSubscription.unsubscribe(); + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, {}); } public ngOnInit() { - this.timeSubscription = + this.observe( this.timeControl.valueChanges.subscribe(value => { if (!value || value.length === 0) { this.timeValue = null; @@ -82,9 +69,9 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } this.updateValue(); - }); + })); - this.dateSubscription = + this.observe( this.dateControl.valueChanges.subscribe(value => { if (!value || value.length === 0) { this.dateValue = null; @@ -93,7 +80,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } this.updateValue(); - }); + })); } public writeValue(obj: any) { @@ -114,7 +101,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; + super.setDisabledState(isDisabled); if (isDisabled) { this.dateControl.disable({ emitEvent: false }); @@ -123,8 +110,6 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, this.dateControl.enable({ emitEvent: false }); this.timeControl.enable({ emitEvent: false }); } - - this.changeDetector.markForCheck(); } public registerOnChange(fn: any) { @@ -144,25 +129,19 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, this.dateValue = this.picker.getMoment(); this.updateValue(); - this.touched(); - - this.changeDetector.markForCheck(); + this.callTouched(); } }); this.updateControls(); } - public touched() { - this.callTouched(); - } - public writeNow() { this.writeValue(new Date().toUTCString()); this.updateControls(); this.updateValue(); - this.touched(); + this.callTouched(); return false; } diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts index 528eb663b..11146a73c 100644 --- a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts @@ -5,11 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; -import { Types } from '@app/framework/internal'; +import { ExternalControlComponent, Types } from '@app/framework/internal'; export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IFrameEditorComponent), multi: true @@ -22,10 +22,7 @@ export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class IFrameEditorComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { - private windowMessageListener: Function; - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class IFrameEditorComponent extends ExternalControlComponent implements AfterViewInit, OnInit { private value: any; private isDisabled = false; private isInitialized = false; @@ -41,14 +38,11 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni public sanitizedUrl: SafeResourceUrl; - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly sanitizer: DomSanitizer, private readonly renderer: Renderer2 ) { - } - - public ngOnDestroy() { - this.windowMessageListener(); + super(changeDetector); } public ngAfterViewInit() { @@ -56,7 +50,7 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni } public ngOnInit(): void { - this.windowMessageListener = + this.observe( this.renderer.listen('window', 'message', (event: MessageEvent) => { if (event.source === this.plugin.contentWindow) { const { type } = event.data; @@ -84,7 +78,7 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni this.callTouched(); } } - }); + })); } public writeValue(obj: any) { @@ -102,12 +96,4 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni this.plugin.contentWindow.postMessage({ type: 'disabled', isDisabled: this.isDisabled }, '*'); } } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.ts b/src/Squidex/app/framework/angular/forms/json-editor.component.ts index 77e3adf1d..143fd095a 100644 --- a/src/Squidex/app/framework/angular/forms/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/json-editor.component.ts @@ -6,11 +6,11 @@ */ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; -import { ResourceLoaderService } from '@app/framework/internal'; +import { ExternalControlComponent, ResourceLoaderService } from '@app/framework/internal'; declare var ace: any; @@ -25,9 +25,7 @@ export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class JsonEditorComponent extends ExternalControlComponent implements AfterViewInit { private valueChanged = new Subject(); private aceEditor: any; private value: any; @@ -35,12 +33,12 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit private isDisabled = false; @ViewChild('editor') - public editor: ElementRef; + public editor: ElementRef; constructor(changeDetector: ChangeDetectorRef, private readonly resourceLoader: ResourceLoaderService ) { - changeDetector.detach(); + super(changeDetector); } public writeValue(obj: any) { @@ -65,14 +63,6 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit } } - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public ngAfterViewInit() { this.valueChanged.pipe( debounceTime(500)) diff --git a/src/Squidex/app/framework/angular/forms/slider.component.html b/src/Squidex/app/framework/angular/forms/slider.component.html deleted file mode 100644 index ab37c52ff..000000000 --- a/src/Squidex/app/framework/angular/forms/slider.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/slider.component.scss b/src/Squidex/app/framework/angular/forms/slider.component.scss deleted file mode 100644 index da277a647..000000000 --- a/src/Squidex/app/framework/angular/forms/slider.component.scss +++ /dev/null @@ -1,51 +0,0 @@ -@import '_mixins'; -@import '_vars'; - -$bar-height: .8rem; - -$thumb-size: 1.25rem; -$thumb-margin: ($thumb-size - $bar-height) * .5; - -.slider { - &-bar { - & { - @include border-radius($bar-height * .5); - position: relative; - border: 1px solid $color-input-border; - margin-bottom: 1.25rem; - margin-top: .25rem; - margin-right: $thumb-size * .5; - background: $color-dark-foreground; - height: $bar-height; - } - - &.disabled { - background: lighten($color-border, 5%); - } - } - - &-thumb { - & { - @include border-radius($thumb-size * .5); - position: absolute; - width: $thumb-size; - height: $thumb-size; - border: 1px solid $color-input-border; - background: $color-dark-foreground; - margin-top: -$thumb-margin; - margin-left: -$thumb-size * .5; - } - - &.disabled { - background: lighten($color-border, 5%); - } - - &.focused { - border-color: $color-theme-blue; - } - } -} - -.disabled { - pointer-events: none; -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/slider.component.ts b/src/Squidex/app/framework/angular/forms/slider.component.ts deleted file mode 100644 index 2dcb73854..000000000 --- a/src/Squidex/app/framework/angular/forms/slider.component.ts +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { Types } from '@app/framework/internal'; - -export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SliderComponent), multi: true -}; - -@Component({ - selector: 'sqx-slider', - styleUrls: ['./slider.component.scss'], - templateUrl: './slider.component.html', - providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class SliderComponent implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - private windowMouseMoveListener: Function | null = null; - private windowMouseUpListener: Function | null = null; - private centerStartOffset = 0; - private lastValue: number; - private value: number; - private isDragging = false; - - @ViewChild('bar') - public bar: ElementRef; - - @ViewChild('thumb') - public thumb: ElementRef; - - @Input() - public min = 0; - - @Input() - public max = 100; - - @Input() - public step = 1; - - public isDisabled = false; - - constructor( - private readonly changeDetector: ChangeDetectorRef, - private readonly renderer: Renderer2 - ) { - } - - public writeValue(obj: any) { - this.lastValue = this.value = Types.isNumber(obj) ? obj : 0; - - this.updateThumbPosition(); - - this.changeDetector.markForCheck(); - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - - public onBarMouseClick(event: MouseEvent): boolean { - if (this.windowMouseMoveListener) { - return true; - } - - const relativeValue = this.getRelativeX(event); - - this.value = Math.round((relativeValue * (this.max - this.min) + this.min) / this.step) * this.step; - - this.updateThumbPosition(); - this.updateTouched(); - this.updateValue(); - - return false; - } - - public onThumbMouseDown(event: MouseEvent): boolean { - this.centerStartOffset = event.offsetX - this.thumb.nativeElement.clientWidth * 0.5; - - this.windowMouseMoveListener = - this.renderer.listen('window', 'mousemove', (e: MouseEvent) => { - this.onMouseMove(e); - }); - - this.windowMouseUpListener = - this.renderer.listen('window', 'mouseup', () => { - this.onMouseUp(); - }); - - this.renderer.addClass(this.thumb.nativeElement, 'focused'); - - this.isDragging = true; - - return false; - } - - private onMouseMove(event: MouseEvent): boolean { - if (!this.isDragging) { - return true; - } - - const relativeValue = this.getRelativeX(event); - - this.value = Math.round((relativeValue * (this.max - this.min) + this.min) / this.step) * this.step; - - this.updateThumbPosition(); - this.updateTouched(); - - return false; - } - - private onMouseUp(): boolean { - this.updateValue(); - - setTimeout(() => { - this.releaseMouseHandlers(); - this.renderer.removeClass(this.thumb.nativeElement, 'focused'); - }, 10); - - this.centerStartOffset = 0; - - this.isDragging = false; - - return false; - } - - private getRelativeX(event: MouseEvent): number { - const parentOffsetX = this.getParentX(event, this.bar.nativeElement) - this.centerStartOffset; - const parentWidth = this.bar.nativeElement.clientWidth; - - const relativeValue = Math.min(1, Math.max(0, (parentOffsetX - this.centerStartOffset) / parentWidth)); - - return relativeValue; - } - - private getParentX(e: any, container: any): number { - const rect = container.getBoundingClientRect(); - - const x = - !!e.touches ? - e.touches[0].pageX : - e.pageX; - - return x - rect.left; - } - - private updateTouched() { - this.callTouched(); - } - - private updateValue() { - if (this.lastValue !== this.value) { - this.lastValue = this.value; - - this.callChange(this.value); - } - } - - private updateThumbPosition() { - const relativeValue = Math.min(1, Math.max(0, (this.value - this.min) / (this.max - this.min))); - - this.renderer.setStyle(this.thumb.nativeElement, 'left', relativeValue * 100 + '%'); - } - - private releaseMouseHandlers() { - if (this.windowMouseMoveListener) { - this.windowMouseMoveListener(); - this.windowMouseMoveListener = null; - } - - if (this.windowMouseUpListener) { - this.windowMouseUpListener(); - this.windowMouseUpListener = null; - } - - this.isDragging = false; - } -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/stars.component.ts b/src/Squidex/app/framework/angular/forms/stars.component.ts index 3a9772f9b..75761a388 100644 --- a/src/Squidex/app/framework/angular/forms/stars.component.ts +++ b/src/Squidex/app/framework/angular/forms/stars.component.ts @@ -8,15 +8,13 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { StatefulComponent, Types } from '@app/framework/internal'; +import { StatefulControlComponent, Types } from '@app/framework/internal'; export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarsComponent), multi: true }; interface State { - isDisabled: boolean; - stars: number; starsArray: number[]; @@ -30,9 +28,7 @@ interface State { providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class StarsComponent extends StatefulComponent implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class StarsComponent extends StatefulControlComponent implements ControlValueAccessor { private maximumStarsValue = 5; @Input() @@ -58,7 +54,6 @@ export class StarsComponent extends StatefulComponent implements ControlV constructor(changeDetector: ChangeDetectorRef) { super(changeDetector, { - isDisabled: false, stars: -1, starsArray: [1, 2, 3, 4, 5], value: 1 @@ -71,18 +66,6 @@ export class StarsComponent extends StatefulComponent implements ControlV this.next({ stars: value, value }); } - public setDisabledState(isDisabled: boolean): void { - this.next({ isDisabled }); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public setPreview(stars: number) { if (this.snapshot.isDisabled) { return; diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index 0fa053881..32e0b96c4 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -5,12 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Subscription } from 'rxjs'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; -import { Types } from '@app/framework/internal'; +import { StatefulControlComponent, Types } from '@app/framework/internal'; const KEY_COMMA = 188; const KEY_DELETE = 8; @@ -75,6 +74,15 @@ const CACHED_SIZES: { [key: string]: number } = {}; let CACHED_FONT: string; +interface State { + hasFocus: boolean; + + suggestedItems: string[]; + suggestedIndex: number; + + items: any[]; +} + @Component({ selector: 'sqx-tag-editor', styleUrls: ['./tag-editor.component.scss'], @@ -82,11 +90,7 @@ let CACHED_FONT: string; providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, OnDestroy, OnInit { - private subscription: Subscription; - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - +export class TagEditorComponent extends StatefulControlComponent implements AfterViewInit, ControlValueAccessor, OnInit { @Input() public converter: Converter = new StringConverter(); @@ -115,27 +119,20 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, public inputName = 'tag-editor'; @ViewChild('form') - public formElement: ElementRef; + public formElement: ElementRef; @ViewChild('input') public inputElement: ElementRef; - public hasFocus = false; - - public suggestedItems: string[] = []; - public suggestedIndex = 0; - - public items: any[] = []; - public addInput = new FormControl(); - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { - } - - public ngOnDestroy() { - this.subscription.unsubscribe(); + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + hasFocus: false, + suggestedItems: [], + suggestedIndex: 0, + items: [] + }); } public ngAfterViewInit() { @@ -149,7 +146,7 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } public ngOnInit() { - this.subscription = + this.observe( this.addInput.valueChanges.pipe( tap(() => { this.resetSize(); @@ -164,7 +161,7 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, distinctUntilChanged(), map(query => { if (Types.isArray(this.suggestions) && query && query.length > 0) { - return this.suggestions.filter(s => s.indexOf(query) >= 0 && this.items.indexOf(s) < 0); + return this.suggestions.filter(s => s.indexOf(query) >= 0 && this.snapshot.items.indexOf(s) < 0); } else { return []; } @@ -172,7 +169,7 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, .subscribe(items => { this.suggestedIndex = -1; this.suggestedItems = items || []; - }); + })); } public writeValue(obj: any) { @@ -189,6 +186,8 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } public setDisabledState(isDisabled: boolean): void { + super.setDisabledState(isDisabled); + if (isDisabled) { this.addInput.disable(); } else { @@ -196,17 +195,9 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } } - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public focus() { if (this.addInput.enabled) { - this.hasFocus = true; + this.next({ hasFocus: true }); } } @@ -396,7 +387,7 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } private updateItems(items: any[]) { - this.items = items; + const items = items; if (items.length === 0 && this.undefinedWhenEmpty) { this.callChange(undefined); diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/src/Squidex/app/framework/angular/forms/toggle.component.ts index 94052ac3c..2fb6142dd 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.ts +++ b/src/Squidex/app/framework/angular/forms/toggle.component.ts @@ -10,14 +10,13 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Types } from '@app/framework/internal'; -import { StatefulComponent } from '../stateful.component'; +import { StatefulControlComponent } from '../stateful.component'; export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToggleComponent), multi: true }; interface State { - isDisabled: boolean; isChecked: boolean | null; } @@ -27,17 +26,13 @@ interface State { templateUrl: './toggle.component.html', providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] }) -export class ToggleComponent extends StatefulComponent implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - +export class ToggleComponent extends StatefulControlComponent implements ControlValueAccessor { @Input() public threeStates = false; constructor(changeDetector: ChangeDetectorRef) { super(changeDetector, { - isChecked: null, - isDisabled: false + isChecked: null }); } @@ -45,18 +40,6 @@ export class ToggleComponent extends StatefulComponent implements Control this.next({ isChecked: Types.isBoolean(obj) ? obj : null }); } - public setDisabledState(isDisabled: boolean): void { - this.next({ isDisabled }); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public changeState(event: MouseEvent) { let { isDisabled, isChecked } = this.snapshot; diff --git a/src/Squidex/app/framework/angular/stateful.component.ts b/src/Squidex/app/framework/angular/stateful.component.ts index 330ecaa34..4ce595620 100644 --- a/src/Squidex/app/framework/angular/stateful.component.ts +++ b/src/Squidex/app/framework/angular/stateful.component.ts @@ -6,6 +6,7 @@ */ import { ChangeDetectorRef, OnDestroy, OnInit } from '@angular/core'; +import { ControlValueAccessor } from '@angular/forms'; import { Subscription } from 'rxjs'; import { Types } from './../utils/types'; @@ -51,8 +52,74 @@ export abstract class StatefulComponent extends State implements OnDestroy } } +export interface FormControlState { + isDisabled: boolean; +} + +export abstract class StatefulControlComponent extends StatefulComponent implements ControlValueAccessor { + private fnChanged = (v: any) => { /* NOOP */ }; + private fnTouched = () => { /* NOOP */ }; + + constructor(changeDetector: ChangeDetectorRef, state: T) { + super(changeDetector, { ...state, isDisabled: false }); + } + + public registerOnChange(fn: any) { + this.fnChanged = fn; + } + + public registerOnTouched(fn: any) { + this.fnTouched = fn; + } + + public callTouched() { + this.fnTouched(); + } + + public callChange(value: TValue | null | undefined) { + this.fnChanged(value); + } + + public setDisabledState(isDisabled: boolean): void { + this.next(state => { state.isDisabled = isDisabled; }); + } + + public abstract writeValue(obj: any): void; +} + export abstract class PureComponent extends StatefulComponent { constructor(changeDetector: ChangeDetectorRef) { super(changeDetector, {}); } +} + +export abstract class ExternalControlComponent extends PureComponent implements ControlValueAccessor { + private fnChanged = (v: any) => { /* NOOP */ }; + private fnTouched = () => { /* NOOP */ }; + + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector); + + changeDetector.detach(); + } + + public registerOnChange(fn: any) { + this.fnChanged = fn; + } + + public registerOnTouched(fn: any) { + this.fnTouched = fn; + } + + protected callTouched() { + this.fnTouched(); + } + + protected callChange(value: TValue) { + this.fnChanged(value); + } + + public abstract setDisabledState(isDisabled: boolean): void; + + public abstract writeValue(obj: any): void; } \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index f8ebdfc58..443df2412 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -20,7 +20,6 @@ export * from './angular/forms/iframe-editor.component'; export * from './angular/forms/indeterminate-value.directive'; export * from './angular/forms/json-editor.component'; export * from './angular/forms/progress-bar.component'; -export * from './angular/forms/slider.component'; export * from './angular/forms/stars.component'; export * from './angular/forms/tag-editor.component'; export * from './angular/forms/toggle.component'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 823abd164..0e65813a3 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -73,7 +73,6 @@ import { ShortcutService, ShortDatePipe, ShortTimePipe, - SliderComponent, SortedDirective, StarsComponent, TagEditorComponent, @@ -143,7 +142,6 @@ import { ShortcutComponent, ShortDatePipe, ShortTimePipe, - SliderComponent, SortedDirective, StarsComponent, TagEditorComponent, @@ -208,7 +206,6 @@ import { ShortcutComponent, ShortDatePipe, ShortTimePipe, - SliderComponent, SortedDirective, StarsComponent, TagEditorComponent, From 3eb4bca9f63703d2ed08a7151ffacc8aabb647ed Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 30 Jan 2019 18:52:29 +0100 Subject: [PATCH 03/16] Refactoring finished, awaiting more tests. --- .../shared/assets-editor.component.html | 16 +-- .../content/shared/assets-editor.component.ts | 102 +++++++----------- .../shared/preview-button.component.ts | 4 +- .../shared/references-editor.component.html | 16 +-- .../shared/references-editor.component.ts | 75 +++++-------- .../app/framework/angular/code.component.ts | 10 +- .../angular/forms/autocomplete.component.html | 8 +- .../angular/forms/autocomplete.component.ts | 71 ++++++------ .../angular/forms/checkbox-group.component.ts | 4 +- .../angular/forms/control-errors.component.ts | 2 +- .../angular/forms/stars.component.ts | 20 ++-- .../angular/forms/tag-editor.component.html | 14 +-- .../angular/forms/tag-editor.component.ts | 64 +++++------ .../angular/forms/toggle.component.ts | 10 +- .../modals/dialog-renderer.component.ts | 26 +++-- .../modals/onboarding-tooltip.component.ts | 6 +- .../angular/modals/root-view.component.ts | 10 +- .../angular/modals/tooltip.component.ts | 6 +- .../app/framework/angular/pager.component.ts | 10 +- .../app/framework/angular/panel.component.ts | 9 +- .../framework/angular/shortcut.component.ts | 6 +- .../framework/angular/stateful.component.ts | 20 +--- .../angular/user-report.component.ts | 6 +- src/Squidex/app/framework/state.ts | 21 ++-- .../geolocation-editor.component.html | 4 +- .../geolocation-editor.component.ts | 64 +++++------ .../components/markdown-editor.component.html | 2 +- .../components/markdown-editor.component.ts | 36 +++---- .../components/rich-editor.component.ts | 21 +--- src/Squidex/app/shared/state/ui.state.ts | 6 +- 30 files changed, 290 insertions(+), 379 deletions(-) diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.html b/src/Squidex/app/features/content/shared/assets-editor.component.html index 176b2dfb9..11661283c 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.html +++ b/src/Squidex/app/features/content/shared/assets-editor.component.html @@ -1,4 +1,4 @@ -
+
@@ -8,10 +8,10 @@
- -
@@ -22,10 +22,10 @@
- -
@@ -33,14 +33,14 @@
-
-
+
diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/src/Squidex/app/features/content/shared/assets-editor.component.ts index 5a59e5951..3eb4eff53 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.ts +++ b/src/Squidex/app/features/content/shared/assets-editor.component.ts @@ -7,9 +7,8 @@ // tslint:disable:prefer-for-of -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Subscription } from 'rxjs'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnInit } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AppsState, @@ -19,6 +18,7 @@ import { ImmutableArray, LocalStoreService, MessageBus, + StatefulControlComponent, Types } from '@app/shared'; @@ -34,6 +34,14 @@ class AssetUpdated { } } +interface State { + newAssets: ImmutableArray; + + oldAssets: ImmutableArray; + + isListView: boolean; +} + @Component({ selector: 'sqx-assets-editor', styleUrls: ['./assets-editor.component.scss'], @@ -41,39 +49,32 @@ class AssetUpdated { providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDestroy { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - private subscription: Subscription; - +export class AssetsEditorComponent extends StatefulControlComponent implements OnInit { public assetsDialog = new DialogModel(); - public newAssets = ImmutableArray.empty(); - public oldAssets = ImmutableArray.empty(); - - public isListView = false; - public isDisabled = false; - - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly appsState: AppsState, private readonly assetsService: AssetsService, - private readonly changeDetector: ChangeDetectorRef, private readonly localStore: LocalStoreService, private readonly messageBus: MessageBus ) { - this.isListView = this.localStore.getBoolean('squidex.assets.list-view'); + super(changeDetector, { + oldAssets: ImmutableArray.empty(), + newAssets: ImmutableArray.empty(), + isListView: localStore.getBoolean('squidex.assets.list-view') + }); } public writeValue(obj: any) { if (Types.isArrayOfString(obj)) { - if (!Types.isEquals(obj, this.oldAssets.map(x => x.id).values)) { + if (!Types.isEquals(obj, this.snapshot.oldAssets.map(x => x.id).values)) { const assetIds: string[] = obj; this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj) .subscribe(dtos => { this.setAssets(ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)!).filter(a => !!a))); - if (this.oldAssets.length !== assetIds.length) { + if (this.snapshot.oldAssets.length !== assetIds.length) { this.updateValue(); } }, () => { @@ -89,42 +90,18 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe this.messageBus.emit(new AssetUpdated(asset, this)); } - public ngOnDestroy() { - this.subscription.unsubscribe(); - } - public ngOnInit() { - this.subscription = + this.observe( this.messageBus.of(AssetUpdated) .subscribe(event => { if (event.source !== this) { - this.setAssets(this.oldAssets.replaceBy('id', event.asset)); + this.setAssets(this.snapshot.oldAssets.replaceBy('id', event.asset)); } - }); - } - - public setAssets(asset: ImmutableArray) { - this.oldAssets = asset; - - this.changeDetector.markForCheck(); - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); + })); } - public noop() { - return; - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; + public setAssets(oldAssets: ImmutableArray) { + this.next(s => ({ ...s, oldAssets })); } public pasteFiles(event: ClipboardEvent) { @@ -132,7 +109,7 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe const file = event.clipboardData.items[i].getAsFile(); if (file) { - this.newAssets = this.newAssets.pushFront(file); + this.next(s => ({ ...s, newAssets: s.newAssets.pushFront(file) })); } } } @@ -142,15 +119,13 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe const file = files[i]; if (file) { - this.newAssets = this.newAssets.pushFront(file); + this.next(s => ({ ...s, newAssets: s.newAssets.pushFront(file) })); } } } public selectAssets(assets: AssetDto[]) { - for (let asset of assets) { - this.oldAssets = this.oldAssets.push(asset); - } + this.setAssets(this.snapshot.oldAssets.push(...assets)); if (assets.length > 0) { this.updateValue(); @@ -161,8 +136,11 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe public addAsset(file: File, asset: AssetDto) { if (asset && file) { - this.newAssets = this.newAssets.remove(file); - this.oldAssets = this.oldAssets.pushFront(asset); + this.next(s => ({ + ...s, + newAssets: s.newAssets.remove(file), + oldAssets: s.oldAssets.pushFront(asset) + })); this.updateValue(); } @@ -170,7 +148,7 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe public sortAssets(assets: AssetDto[]) { if (assets) { - this.oldAssets = ImmutableArray.of(assets); + this.setAssets(ImmutableArray.of(assets)); this.updateValue(); } @@ -178,24 +156,24 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe public removeLoadedAsset(asset: AssetDto) { if (asset) { - this.oldAssets = this.oldAssets.remove(asset); + this.setAssets(this.snapshot.oldAssets.remove(asset)); this.updateValue(); } } public removeLoadingAsset(file: File) { - this.newAssets = this.newAssets.remove(file); + this.next(s => ({ ...s, newAssets: s.newAssets.remove(file) })); } public changeView(isListView: boolean) { - this.isListView = isListView; + this.next(s => ({ ...s, isListView })); this.localStore.setBoolean('squidex.assets.list-view', isListView); } private updateValue() { - let ids: string[] | null = this.oldAssets.values.map(x => x.id); + let ids: string[] | null = this.snapshot.oldAssets.values.map(x => x.id); if (ids.length === 0) { ids = null; @@ -203,11 +181,9 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe this.callTouched(); this.callChange(ids); - - this.changeDetector.markForCheck(); } - public trackByAsset(index: number, asset: AssetDto) { + public trackByAsset(asset: AssetDto) { return asset.id; } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/preview-button.component.ts b/src/Squidex/app/features/content/shared/preview-button.component.ts index 215500f7d..e47e393fd 100644 --- a/src/Squidex/app/features/content/shared/preview-button.component.ts +++ b/src/Squidex/app/features/content/shared/preview-button.component.ts @@ -20,10 +20,10 @@ import { selector: 'sqx-preview-button', styleUrls: ['./preview-button.component.scss'], templateUrl: './preview-button.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, animations: [ fadeAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PreviewButtonComponent implements OnInit { @Input() diff --git a/src/Squidex/app/features/content/shared/references-editor.component.html b/src/Squidex/app/features/content/shared/references-editor.component.html index 7fbb63a25..2bc315e36 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.html +++ b/src/Squidex/app/features/content/shared/references-editor.component.html @@ -1,27 +1,27 @@ -
- +
+
Click here to link content items.
- - +
-
+
Schema not found or not configured yet.
@@ -30,7 +30,7 @@ \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/references-editor.component.ts b/src/Squidex/app/features/content/shared/references-editor.component.ts index bbe28ba16..8485eb2f9 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.ts +++ b/src/Squidex/app/features/content/shared/references-editor.component.ts @@ -8,7 +8,7 @@ // tslint:disable:prefer-for-of import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AppLanguageDto, @@ -20,6 +20,7 @@ import { MathHelper, SchemaDetailsDto, SchemasService, + StatefulControlComponent, Types } from '@app/shared'; @@ -27,6 +28,13 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesEditorComponent), multi: true }; +interface State { + schema?: SchemaDetailsDto; + schemaInvalid: boolean; + + contentItems: ImmutableArray; +} + @Component({ selector: 'sqx-references-editor', styleUrls: ['./references-editor.component.scss'], @@ -34,10 +42,7 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - +export class ReferencesEditorComponent extends StatefulControlComponent implements OnInit { @Input() public schemaId: string; @@ -49,49 +54,41 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { public selectorDialog = new DialogModel(); - public schema: SchemaDetailsDto; - - public contentItems = ImmutableArray.empty(); - - public isDisabled = false; - public isInvalidSchema = false; - - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly appsState: AppsState, - private readonly changeDetector: ChangeDetectorRef, private readonly contentsService: ContentsService, private readonly schemasService: SchemasService ) { + super(changeDetector, { + schemaInvalid: false, + contentItems: ImmutableArray.empty() + }); } public ngOnInit() { if (this.schemaId === MathHelper.EMPTY_GUID) { - this.isInvalidSchema = true; + this.next(s => ({ ...s, schemaInvalid: true })); return; } this.schemasService.getSchema(this.appsState.appName, this.schemaId) - .subscribe(dto => { - this.schema = dto; - - this.changeDetector.markForCheck(); + .subscribe(schema => { + this.next(s => ({ ...s, schema })); }, () => { - this.isInvalidSchema = true; - - this.changeDetector.markForCheck(); + this.next(s => ({ ...s, schemaInvalid: true })); }); } public writeValue(obj: any) { if (Types.isArrayOfString(obj)) { - if (!Types.isEquals(obj, this.contentItems.map(x => x.id).values)) { + if (!Types.isEquals(obj, this.snapshot.contentItems.map(x => x.id).values)) { const contentIds: string[] = obj; this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds) .subscribe(dtos => { this.setContentItems(ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)!).filter(r => !!r))); - if (this.contentItems.length !== contentIds.length) { + if (this.snapshot.contentItems.length !== contentIds.length) { this.updateValue(); } }, () => { @@ -103,29 +100,13 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { } } - public setContentItems(contents: ImmutableArray) { - this.contentItems = contents; - - this.changeDetector.markForCheck(); - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; + public setContentItems(contentItems: ImmutableArray) { + this.next(s => ({ ...s, contentItems })); } public select(contents: ContentDto[]) { for (let content of contents) { - this.contentItems = this.contentItems.push(content); + this.setContentItems(this.snapshot.contentItems.push(content)); } if (contents.length > 0) { @@ -137,7 +118,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { public remove(content: ContentDto) { if (content) { - this.contentItems = this.contentItems.remove(content); + this.setContentItems(this.snapshot.contentItems.remove(content)); this.updateValue(); } @@ -145,14 +126,14 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { public sort(contents: ContentDto[]) { if (contents) { - this.contentItems = ImmutableArray.of(contents); + this.setContentItems(ImmutableArray.of(contents)); this.updateValue(); } } private updateValue() { - let ids: string[] | null = this.contentItems.values.map(x => x.id); + let ids: string[] | null = this.snapshot.contentItems.values.map(x => x.id); if (ids.length === 0) { ids = null; @@ -160,7 +141,5 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { this.callTouched(); this.callChange(ids); - - this.changeDetector.markForCheck(); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/code.component.ts b/src/Squidex/app/framework/angular/code.component.ts index 7ae539d76..f78d5b637 100644 --- a/src/Squidex/app/framework/angular/code.component.ts +++ b/src/Squidex/app/framework/angular/code.component.ts @@ -5,9 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; - -import { PureComponent } from '@app/framework/internal'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; @Component({ selector: 'sqx-code', @@ -15,8 +13,4 @@ import { PureComponent } from '@app/framework/internal'; templateUrl: './code.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class CodeComponent extends PureComponent { - constructor(changeDetector: ChangeDetectorRef) { - super(changeDetector); - } -} \ No newline at end of file +export class CodeComponent { } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.html b/src/Squidex/app/framework/angular/forms/autocomplete.component.html index 882956d72..9f4a662c3 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.html +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.html @@ -5,13 +5,13 @@ autocorrect="off" autocapitalize="off"> -
-
+
+ [sqxScrollActive]="i === snapshot.suggestedIndex"> {{item}} diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts index 164f74ec3..580593e34 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts @@ -5,11 +5,13 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, forwardRef, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Observable, of, Subscription } from 'rxjs'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators'; +import { StatefulControlComponent } from '@app/shared'; + export interface AutocompleteSource { find(query: string): Observable; } @@ -23,6 +25,11 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true }; +interface State { + suggestedItems: any[]; + suggestedIndex: number; +} + @Component({ selector: 'sqx-autocomplete', styleUrls: ['./autocomplete.component.scss'], @@ -30,11 +37,7 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit { - private subscription: Subscription; - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - +export class AutocompleteComponent extends StatefulControlComponent implements OnInit { @Input() public source: AutocompleteSource; @@ -53,17 +56,17 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O @ViewChild('input') public inputControl: ElementRef; - public suggestedItems: any[] = []; - public suggestedIndex = -1; - public queryInput = new FormControl(); - public ngOnDestroy() { - this.subscription.unsubscribe(); + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + suggestedItems: [], + suggestedIndex: -1 + }); } public ngOnInit() { - this.subscription = + this.observe( this.queryInput.valueChanges.pipe( tap(query => { this.callChange(query); @@ -80,9 +83,12 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O filter(query => !!query && !!this.source), switchMap(query => this.source.find(query)), catchError(() => of([]))) .subscribe(items => { - this.suggestedIndex = -1; - this.suggestedItems = items || []; - }); + this.next(s => ({ + ...s, + suggestedIndex: -1, + suggestedItems: items || [] + })); + })); } public onKeyDown(event: KeyboardEvent) { @@ -98,7 +104,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O this.reset(); return false; case KEY_ENTER: - if (this.suggestedItems.length > 0 && this.selectItem()) { + if (this.snapshot.suggestedItems.length > 0 && this.selectItem()) { return false; } break; @@ -149,11 +155,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O public selectItem(selection: any | null = null): boolean { if (!selection) { - selection = this.suggestedItems[this.suggestedIndex]; + selection = this.snapshot.suggestedItems[this.snapshot.suggestedIndex]; } - if (!selection && this.suggestedItems.length === 1) { - selection = this.suggestedItems[0]; + if (!selection && this.snapshot.suggestedItems.length === 1) { + selection = this.snapshot.suggestedItems[0]; } if (selection) { @@ -174,24 +180,24 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O return false; } - public selectIndex(selection: number) { - if (selection < 0) { - selection = 0; + public selectIndex(suggestedIndex: number) { + if (suggestedIndex < 0) { + suggestedIndex = 0; } - if (selection >= this.suggestedItems.length) { - selection = this.suggestedItems.length - 1; + if (suggestedIndex >= this.snapshot.suggestedItems.length) { + suggestedIndex = this.snapshot.suggestedItems.length - 1; } - this.suggestedIndex = selection; + this.next(s => ({ ...s, suggestedIndex })); } private up() { - this.selectIndex(this.suggestedIndex - 1); + this.selectIndex(this.snapshot.suggestedIndex - 1); } private down() { - this.selectIndex(this.suggestedIndex + 1); + this.selectIndex(this.snapshot.suggestedIndex + 1); } private resetForm() { @@ -199,7 +205,10 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O } private reset() { - this.suggestedItems = []; - this.suggestedIndex = -1; + this.next(s => ({ + ...s, + suggestedItems: [], + suggestedIndex: -1 + })); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts b/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts index a426f93cc..d34486b19 100644 --- a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts +++ b/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts @@ -44,7 +44,7 @@ export class CheckboxGroupComponent extends StatefulControlComponent this.values.indexOf(x) >= 0) : []; - this.next({ checkedValues }); + this.next(s => ({ ...s, checkedValues })); } public check(isChecked: boolean, value: string) { @@ -56,7 +56,7 @@ export class CheckboxGroupComponent extends StatefulControlComponent x !== value); } - this.next({ checkedValues }); + this.next(s => ({ ...s, checkedValues })); this.callChange(checkedValues); } diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/src/Squidex/app/framework/angular/forms/control-errors.component.ts index 1e2215f0b..26f8a0ce7 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.ts @@ -127,6 +127,6 @@ export class ControlErrorsComponent extends StatefulComponent implements } } - this.next({ errorMessages }); + this.next(() => ({ errorMessages })); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/stars.component.ts b/src/Squidex/app/framework/angular/forms/stars.component.ts index 75761a388..69a0e24ca 100644 --- a/src/Squidex/app/framework/angular/forms/stars.component.ts +++ b/src/Squidex/app/framework/angular/forms/stars.component.ts @@ -6,7 +6,7 @@ */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { StatefulControlComponent, Types } from '@app/framework/internal'; @@ -28,7 +28,7 @@ interface State { providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class StarsComponent extends StatefulControlComponent implements ControlValueAccessor { +export class StarsComponent extends StatefulControlComponent { private maximumStarsValue = 5; @Input() @@ -38,13 +38,13 @@ export class StarsComponent extends StatefulControlComponent ({ ...s, starsArray })); } } @@ -63,7 +63,7 @@ export class StarsComponent extends StatefulControlComponent ({ ...s, stars: value, value })); } public setPreview(stars: number) { @@ -71,7 +71,7 @@ export class StarsComponent extends StatefulControlComponent ({ ...s, stars })); } public stopPreview() { @@ -79,7 +79,7 @@ export class StarsComponent extends StatefulControlComponent { s.stars = s.value || 0; }); + this.next(s => ({ ...s, stars: s.value || 0 })); } public reset() { @@ -88,7 +88,7 @@ export class StarsComponent extends StatefulControlComponent ({ ...s, stars: -1, value: null })); this.callChange(null); this.callTouched(); @@ -103,7 +103,7 @@ export class StarsComponent extends StatefulControlComponent ({ ...s, stars: value, value })); this.callChange(value); this.callTouched(); diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/src/Squidex/app/framework/angular/forms/tag-editor.component.html index 5aa03539b..36d93bf22 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -1,9 +1,9 @@
- + [class.focus]="snapshot.hasFocus" + [class.disabled]snapshot.="addInput.disabled"> + {{item}} @@ -23,13 +23,13 @@ spellcheck="false">
-
-
+
+ [sqxScrollActive]="i === snapshot.suggestedIndex"> {{item}}
diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index 32e0b96c4..8927195c7 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -6,7 +6,7 @@ */ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { StatefulControlComponent, Types } from '@app/framework/internal'; @@ -90,7 +90,7 @@ interface State { providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TagEditorComponent extends StatefulControlComponent implements AfterViewInit, ControlValueAccessor, OnInit { +export class TagEditorComponent extends StatefulControlComponent implements AfterViewInit, OnInit { @Input() public converter: Converter = new StringConverter(); @@ -167,8 +167,11 @@ export class TagEditorComponent extends StatefulControlComponent i } })) .subscribe(items => { - this.suggestedIndex = -1; - this.suggestedItems = items || []; + this.next(s => ({ + ...s, + suggestedIndex: -1, + suggestedItems: items || [] + })); })); } @@ -177,12 +180,10 @@ export class TagEditorComponent extends StatefulControlComponent i this.resetSize(); if (this.converter && Types.isArrayOf(obj, v => this.converter.isValidValue(v))) { - this.items = obj; + this.next(s => ({ ...s, items: obj })); } else { - this.items = []; + this.next(s => ({ ...s, items: [] })); } - - this.changeDetector.markForCheck(); } public setDisabledState(isDisabled: boolean): void { @@ -197,7 +198,7 @@ export class TagEditorComponent extends StatefulControlComponent i public focus() { if (this.addInput.enabled) { - this.next({ hasFocus: true }); + this.next(s => ({ ...s, hasFocus: true })); } } @@ -211,7 +212,7 @@ export class TagEditorComponent extends StatefulControlComponent i } public remove(index: number) { - this.updateItems([...this.items.slice(0, index), ...this.items.splice(index + 1)]); + this.updateItems(this.snapshot.items.filter((_, i) => i !== index)); } public resetSize() { @@ -265,7 +266,7 @@ export class TagEditorComponent extends StatefulControlComponent i const value = this.addInput.value; if (!value || value.length === 0) { - this.updateItems(this.items.slice(0, this.items.length - 1)); + this.updateItems(this.snapshot.items.slice(0, this.snapshot.items.length - 1)); return false; } @@ -276,8 +277,8 @@ export class TagEditorComponent extends StatefulControlComponent i this.down(); return false; } else if (key === KEY_ENTER) { - if (this.suggestedIndex >= 0) { - if (this.selectValue(this.suggestedItems[this.suggestedIndex])) { + if (this.snapshot.suggestedIndex >= 0) { + if (this.selectValue(this.snapshot.suggestedItems[this.snapshot.suggestedIndex])) { return false; } } else if (this.acceptEnter) { @@ -298,8 +299,8 @@ export class TagEditorComponent extends StatefulControlComponent i if (value && this.converter.isValidInput(value)) { const converted = this.converter.convert(value); - if (this.allowDuplicates || this.items.indexOf(converted) < 0) { - this.updateItems([...this.items, converted]); + if (this.allowDuplicates || this.snapshot.items.indexOf(converted) < 0) { + this.updateItems([...this.snapshot.items, converted]); } this.resetForm(); @@ -309,24 +310,27 @@ export class TagEditorComponent extends StatefulControlComponent i } private resetAutocompletion() { - this.suggestedItems = []; - this.suggestedIndex = -1; + this.next(s => ({ + ...s, + suggestedItems: [], + suggestedIndex: -1 + })); } - public selectIndex(selection: number) { - if (selection < 0) { - selection = 0; + public selectIndex(suggestedIndex: number) { + if (suggestedIndex < 0) { + suggestedIndex = 0; } - if (selection >= this.suggestedItems.length) { - selection = this.suggestedItems.length - 1; + if (suggestedIndex >= this.snapshot.suggestedItems.length) { + suggestedIndex = this.snapshot.suggestedItems.length - 1; } - this.suggestedIndex = selection; + this.next(s => ({ ...s, suggestedIndex })); } public resetFocus(): any { - this.hasFocus = false; + this.next(s => ({ ...s, hasFocus: false })); } private resetForm() { @@ -334,11 +338,11 @@ export class TagEditorComponent extends StatefulControlComponent i } private up() { - this.selectIndex(this.suggestedIndex - 1); + this.selectIndex(this.snapshot.suggestedIndex - 1); } private down() { - this.selectIndex(this.suggestedIndex + 1); + this.selectIndex(this.snapshot.suggestedIndex + 1); } public onCut(event: ClipboardEvent) { @@ -351,7 +355,7 @@ export class TagEditorComponent extends StatefulControlComponent i public onCopy(event: ClipboardEvent) { if (!this.hasSelection()) { - event.clipboardData.setData('text/plain', this.items.filter(x => !!x).join(',')); + event.clipboardData.setData('text/plain', this.snapshot.items.filter(x => !!x).join(',')); event.preventDefault(); } @@ -363,7 +367,7 @@ export class TagEditorComponent extends StatefulControlComponent i if (value) { this.resetForm(); - const values = [...this.items]; + const values = [...this.snapshot.items]; for (let part of value.split(',')) { const converted = this.converter.convert(part); @@ -387,12 +391,12 @@ export class TagEditorComponent extends StatefulControlComponent i } private updateItems(items: any[]) { - const items = items; + this.next(s => ({ ...s, items })); if (items.length === 0 && this.undefinedWhenEmpty) { this.callChange(undefined); } else { - this.callChange(this.items); + this.callChange(items); } this.resetSize(); diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/src/Squidex/app/framework/angular/forms/toggle.component.ts index 2fb6142dd..5eb23cb82 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.ts +++ b/src/Squidex/app/framework/angular/forms/toggle.component.ts @@ -6,7 +6,7 @@ */ import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { Types } from '@app/framework/internal'; @@ -26,7 +26,7 @@ interface State { templateUrl: './toggle.component.html', providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] }) -export class ToggleComponent extends StatefulControlComponent implements ControlValueAccessor { +export class ToggleComponent extends StatefulControlComponent { @Input() public threeStates = false; @@ -37,7 +37,9 @@ export class ToggleComponent extends StatefulControlComponent ({ ...s, isChecked })); } public changeState(event: MouseEvent) { @@ -59,7 +61,7 @@ export class ToggleComponent extends StatefulControlComponent ({ ...s, isChecked })); this.callChange(isChecked); this.callTouched(); diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts index 335a19da0..666da8e15 100644 --- a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts +++ b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts @@ -55,9 +55,10 @@ export class DialogRendererComponent extends StatefulComponent implements this.observe( this.dialogs.notifications.subscribe(notification => { - this.next(state => { - state.notifications = [...state.notifications, notification]; - }); + this.next(s => ({ + ...s, + notifications: [...s.notifications, notification] + })); if (notification.displayTime > 0) { this.observe(timer(notification.displayTime).subscribe(() => { @@ -68,12 +69,10 @@ export class DialogRendererComponent extends StatefulComponent implements this.observe( this.dialogs.dialogs - .subscribe(request => { + .subscribe(dialogRequest => { this.cancel(); - this.next(state => { - state.dialogRequest = request; - }); + this.next(s => ({ ...s, dialogRequest })); })); } @@ -90,17 +89,16 @@ export class DialogRendererComponent extends StatefulComponent implements } private finishRequest(value: boolean) { - this.next(state => { - if (state.dialogRequest) { - state.dialogRequest.complete(value); - state.dialogRequest = null; + this.next(s => { + if (s.dialogRequest) { + s.dialogRequest.complete(value); } + + return { ...s, dialogRequest: null }; }); } public close(notification: Notification) { - this.next(state => { - state.notifications = state.notifications.filter(n => notification !== n); - }); + this.next(s => ({ ...s, notifications: s.notifications.filter(n => notification !== n) })); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts b/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts index 70b6c1cb3..b0de1483f 100644 --- a/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts @@ -12,7 +12,7 @@ import { fadeAnimation, ModalModel, OnboardingService, - PureComponent, + StatefulComponent, Types } from '@app/framework/internal'; @@ -25,7 +25,7 @@ import { ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class OnboardingTooltipComponent extends PureComponent implements OnDestroy, OnInit { +export class OnboardingTooltipComponent extends StatefulComponent implements OnDestroy, OnInit { public tooltipModal = new ModalModel(); @Input() @@ -44,7 +44,7 @@ export class OnboardingTooltipComponent extends PureComponent implements OnDestr private readonly onboardingService: OnboardingService, private readonly renderer: Renderer2 ) { - super(changeDetector); + super(changeDetector, {}); } public ngOnDestroy() { diff --git a/src/Squidex/app/framework/angular/modals/root-view.component.ts b/src/Squidex/app/framework/angular/modals/root-view.component.ts index 2bd9862dd..2f3ec887c 100644 --- a/src/Squidex/app/framework/angular/modals/root-view.component.ts +++ b/src/Squidex/app/framework/angular/modals/root-view.component.ts @@ -5,9 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewChild, ViewContainerRef } from '@angular/core'; - -import { PureComponent } from '@app/framework/internal'; +import { ChangeDetectionStrategy, Component, ViewChild, ViewContainerRef } from '@angular/core'; @Component({ selector: 'sqx-root-view', @@ -15,11 +13,7 @@ import { PureComponent } from '@app/framework/internal'; templateUrl: './root-view.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class RootViewComponent extends PureComponent { +export class RootViewComponent { @ViewChild('element', { read: ViewContainerRef }) public viewContainer: ViewContainerRef; - - constructor(changeDetector: ChangeDetectorRef) { - super(changeDetector); - } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/tooltip.component.ts b/src/Squidex/app/framework/angular/modals/tooltip.component.ts index 8a0e558a8..d4c921544 100644 --- a/src/Squidex/app/framework/angular/modals/tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/tooltip.component.ts @@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy import { fadeAnimation, ModalModel, - PureComponent + StatefulComponent } from '@app/framework/internal'; @Component({ @@ -22,7 +22,7 @@ import { ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TooltipComponent extends PureComponent implements OnDestroy, OnInit { +export class TooltipComponent extends StatefulComponent implements OnDestroy, OnInit { @Input() public target: any; @@ -34,7 +34,7 @@ export class TooltipComponent extends PureComponent implements OnDestroy, OnInit constructor(changeDetector: ChangeDetectorRef, private readonly renderer: Renderer2 ) { - super(changeDetector); + super(changeDetector, {}); } public ngOnInit() { diff --git a/src/Squidex/app/framework/angular/pager.component.ts b/src/Squidex/app/framework/angular/pager.component.ts index dce6f8f8b..c85eaf9cd 100644 --- a/src/Squidex/app/framework/angular/pager.component.ts +++ b/src/Squidex/app/framework/angular/pager.component.ts @@ -5,9 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { Pager, PureComponent } from '@app/framework/internal'; +import { Pager } from '@app/framework/internal'; @Component({ selector: 'sqx-pager', @@ -15,7 +15,7 @@ import { Pager, PureComponent } from '@app/framework/internal'; templateUrl: './pager.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class PagerComponent extends PureComponent { +export class PagerComponent { @Output() public nextPage = new EventEmitter(); @@ -27,8 +27,4 @@ export class PagerComponent extends PureComponent { @Input() public hideWhenButtonsDisabled = false; - - constructor(changeDetector: ChangeDetectorRef) { - super(changeDetector); - } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/src/Squidex/app/framework/angular/panel.component.ts index 68bdb01d4..e29d14327 100644 --- a/src/Squidex/app/framework/angular/panel.component.ts +++ b/src/Squidex/app/framework/angular/panel.component.ts @@ -5,9 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; -import { PureComponent, slideRightAnimation } from '@app/framework/internal'; +import { slideRightAnimation } from '@app/framework/internal'; import { PanelContainerDirective } from './panel-container.directive'; @@ -20,7 +20,7 @@ import { PanelContainerDirective } from './panel-container.directive'; ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class PanelComponent extends PureComponent implements AfterViewInit, OnDestroy, OnInit { +export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { private styleWidth: string; public renderWidth = 0; @@ -61,11 +61,10 @@ export class PanelComponent extends PureComponent implements AfterViewInit, OnDe @ViewChild('panel') public panel: ElementRef; - constructor(changeDetector: ChangeDetectorRef, + constructor( private readonly container: PanelContainerDirective, private readonly renderer: Renderer2 ) { - super(changeDetector); } public ngOnDestroy() { diff --git a/src/Squidex/app/framework/angular/shortcut.component.ts b/src/Squidex/app/framework/angular/shortcut.component.ts index c264be77d..d1b2b993f 100644 --- a/src/Squidex/app/framework/angular/shortcut.component.ts +++ b/src/Squidex/app/framework/angular/shortcut.component.ts @@ -7,13 +7,13 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; -import { PureComponent, ShortcutService } from '@app/framework/internal'; +import { ShortcutService, StatefulComponent } from '@app/framework/internal'; @Component({ selector: 'sqx-shortcut', template: '' }) -export class ShortcutComponent extends PureComponent implements OnDestroy, OnInit { +export class ShortcutComponent extends StatefulComponent implements OnDestroy, OnInit { private lastKeys: string; @Input() @@ -30,7 +30,7 @@ export class ShortcutComponent extends PureComponent implements OnDestroy, OnIni private readonly shortcutService: ShortcutService, private readonly zone: NgZone ) { - super(changeDetector); + super(changeDetector, {}); changeDetector.detach(); } diff --git a/src/Squidex/app/framework/angular/stateful.component.ts b/src/Squidex/app/framework/angular/stateful.component.ts index 4ce595620..fad4ab6df 100644 --- a/src/Squidex/app/framework/angular/stateful.component.ts +++ b/src/Squidex/app/framework/angular/stateful.component.ts @@ -15,7 +15,7 @@ import { State } from '../state'; declare type UnsubscribeFunction = () => void; -export abstract class StatefulComponent extends State implements OnDestroy, OnInit { +export abstract class StatefulComponent extends State implements OnDestroy, OnInit { private subscriptions: (Subscription | UnsubscribeFunction)[] = []; constructor( @@ -52,11 +52,7 @@ export abstract class StatefulComponent extends State implements OnDestroy } } -export interface FormControlState { - isDisabled: boolean; -} - -export abstract class StatefulControlComponent extends StatefulComponent implements ControlValueAccessor { +export abstract class StatefulControlComponent extends StatefulComponent implements ControlValueAccessor { private fnChanged = (v: any) => { /* NOOP */ }; private fnTouched = () => { /* NOOP */ }; @@ -81,24 +77,18 @@ export abstract class StatefulControlComponent extends StatefulCompon } public setDisabledState(isDisabled: boolean): void { - this.next(state => { state.isDisabled = isDisabled; }); + this.next(s => ({ ...s, isDisabled })); } public abstract writeValue(obj: any): void; } -export abstract class PureComponent extends StatefulComponent { - constructor(changeDetector: ChangeDetectorRef) { - super(changeDetector, {}); - } -} - -export abstract class ExternalControlComponent extends PureComponent implements ControlValueAccessor { +export abstract class ExternalControlComponent extends StatefulComponent implements ControlValueAccessor { private fnChanged = (v: any) => { /* NOOP */ }; private fnTouched = () => { /* NOOP */ }; constructor(changeDetector: ChangeDetectorRef) { - super(changeDetector); + super(changeDetector, {}); changeDetector.detach(); } diff --git a/src/Squidex/app/framework/angular/user-report.component.ts b/src/Squidex/app/framework/angular/user-report.component.ts index ed197e543..9d2b32ba7 100644 --- a/src/Squidex/app/framework/angular/user-report.component.ts +++ b/src/Squidex/app/framework/angular/user-report.component.ts @@ -9,8 +9,8 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { timer } from 'rxjs'; import { - PureComponent, ResourceLoaderService, + StatefulComponent, UserReportConfig } from '@app/framework/internal'; @@ -18,12 +18,12 @@ import { selector: 'sqx-user-report', template: '' }) -export class UserReportComponent extends PureComponent implements OnDestroy, OnInit { +export class UserReportComponent extends StatefulComponent implements OnDestroy, OnInit { constructor(changeDetector: ChangeDetectorRef, private readonly config: UserReportConfig, private readonly resourceLoader: ResourceLoaderService ) { - super(changeDetector); + super(changeDetector, {}); changeDetector.detach(); } diff --git a/src/Squidex/app/framework/state.ts b/src/Squidex/app/framework/state.ts index d7690801d..357acb2ed 100644 --- a/src/Squidex/app/framework/state.ts +++ b/src/Squidex/app/framework/state.ts @@ -49,13 +49,13 @@ export class Form { } public load(value: any) { - this.state.next({ submitted: false, error: null }); + this.state.next(_ => ({ submitted: false, error: null })); this.setValue(value); } public submit(): any | null { - this.state.next({ submitted: true }); + this.state.next(_ => ({ submitted: true })); if (this.form.valid) { const value = fullValue(this.form); @@ -69,7 +69,7 @@ export class Form { } public submitCompleted(newValue?: any) { - this.state.next({ submitted: false, error: null }); + this.state.next(_ => ({ submitted: false, error: null })); this.enable(); @@ -81,7 +81,7 @@ export class Form { } public submitFailed(error?: string | ErrorDto) { - this.state.next({ submitted: false, error: this.getError(error) }); + this.state.next(_ => ({ submitted: false, error: this.getError(error) })); this.enable(); } @@ -133,17 +133,10 @@ export class State { } public resetState() { - this.next(this.initialState); + this.next(_ => this.initialState); } - public next(update: ((v: T) => T | void) | Partial) { - if (Types.isFunction(update)) { - const stateNew = { ...this.snapshot }; - const stateUpdated = update(stateNew); - - this.state.next(stateUpdated || stateNew); - } else { - this.state.next({ ...this.snapshot, ...update }); - } + public next(update: (v: T) => T) { + this.state.next(update(this.snapshot)); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.html b/src/Squidex/app/shared/components/geolocation-editor.component.html index fdf03b724..768fe3b9e 100644 --- a/src/Squidex/app/shared/components/geolocation-editor.component.html +++ b/src/Squidex/app/shared/components/geolocation-editor.component.html @@ -1,7 +1,7 @@
- +
@@ -24,7 +24,7 @@
- +
diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.ts b/src/Squidex/app/shared/components/geolocation-editor.component.ts index 8d1ec494a..00de85805 100644 --- a/src/Squidex/app/shared/components/geolocation-editor.component.ts +++ b/src/Squidex/app/shared/components/geolocation-editor.component.ts @@ -6,10 +6,11 @@ */ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ResourceLoaderService, + StatefulControlComponent, Types, UIState, ValidatorsEx @@ -27,6 +28,10 @@ interface Geolocation { longitude: number; } +interface State { + isGoogleMaps: boolean; +} + @Component({ selector: 'sqx-geolocation-editor', styleUrls: ['./geolocation-editor.component.scss'], @@ -34,9 +39,7 @@ interface Geolocation { providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class GeolocationEditorComponent extends StatefulControlComponent implements AfterViewInit { private marker: any; private map: any; private value: Geolocation | null = null; @@ -62,20 +65,19 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi }); @ViewChild('editor') - public editor: ElementRef; + public editor: ElementRef; @ViewChild('searchBox') - public searchBoxInput: ElementRef; - - public isGoogleMaps = false; - public isDisabled = false; + public searchBoxInput: ElementRef; - constructor( - private readonly changeDetector: ChangeDetectorRef, + constructor(changeDetector: ChangeDetectorRef, private readonly resourceLoader: ResourceLoaderService, private readonly formBuilder: FormBuilder, private readonly uiState: UIState ) { + super(changeDetector, { + isGoogleMaps: false + }); } public writeValue(obj: any) { @@ -91,9 +93,9 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; + super.setDisabledState(isDisabled); - if (!this.isGoogleMaps) { + if (!this.snapshot.isGoogleMaps) { this.setDisabledStateOSM(isDisabled); } else { this.setDisabledStateGoogle(isDisabled); @@ -104,8 +106,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } else { this.geolocationForm.enable(); } - - this.changeDetector.markForCheck(); } private setDisabledStateOSM(isDisabled: boolean): void { @@ -139,14 +139,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } } - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public updateValueByInput() { const lat = this.geolocationForm.controls['latitude'].value; const lng = this.geolocationForm.controls['longitude'].value; @@ -164,9 +156,11 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi public ngAfterViewInit() { this.uiState.settings .subscribe(settings => { - this.isGoogleMaps = settings.mapType === 'GoogleMaps'; + const isGoogleMaps = settings.mapType === 'GoogleMaps'; + + this.next(s => ({ ...s, isGoogleMaps })); - if (!this.isGoogleMaps) { + if (!this.snapshot.isGoogleMaps) { this.ngAfterViewInitOSM(); } else { this.ngAfterViewInitGoogle(settings.mapKey); @@ -189,7 +183,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.map.on('click', (event: any) => { - if (!this.marker && !this.isDisabled) { + if (!this.marker && !this.snapshot.isDisabled) { const latlng = event.latlng.wrap(); this.updateValue(latlng.lat, latlng.lng); @@ -199,7 +193,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.updateMarker(true, false); - if (this.isDisabled) { + if (this.snapshot.isDisabled) { this.map.zoomControl.disable(); this.map._handlers.forEach((handler: any) => { @@ -224,7 +218,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.map.addListener('click', (event: any) => { - if (!this.isDisabled) { + if (!this.snapshot.isDisabled) { this.updateValue(event.latLng.lat(), event.latLng.lng()); this.updateMarker(false, true); } @@ -244,7 +238,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi return; } - if (!this.isDisabled) { + if (!this.snapshot.isDisabled) { let lat = place.geometry.location.lat(); let lng = place.geometry.location.lng(); @@ -256,7 +250,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.updateMarker(true, false); - if (this.isDisabled) { + if (this.snapshot.isDisabled) { this.map.setOptions({ draggable: false, zoomControl: false }); } }); @@ -264,7 +258,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi public reset() { this.value = null; - this.searchBoxInput.nativeElement.value = null; + this.searchBoxInput.nativeElement.value = ''; this.updateMarker(true, true); } @@ -274,7 +268,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } private updateMarker(zoom: boolean, fireEvent: boolean) { - if (!this.isGoogleMaps) { + if (!this.snapshot.isGoogleMaps) { this.updateMarkerOSM(zoom); } else { this.updateMarkerGoogle(zoom); @@ -307,7 +301,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.updateMarker(false, true); }); - if (this.isDisabled) { + if (this.snapshot.isDisabled) { this.marker.dragging.disable(); } } @@ -344,12 +338,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi }); this.marker.addListener('drag', (event: any) => { - if (!this.isDisabled) { + if (!this.snapshot.isDisabled) { this.updateValue(event.latLng.lat(), event.LatLng.lng()); } }); this.marker.addListener('dragend', (event: any) => { - if (!this.isDisabled) { + if (!this.snapshot.isDisabled) { this.updateValue(event.latLng.lat(), event.LatLng.lng()); this.updateMarker(false, true); } diff --git a/src/Squidex/app/shared/components/markdown-editor.component.html b/src/Squidex/app/shared/components/markdown-editor.component.html index 7480c92da..79380fdc6 100644 --- a/src/Squidex/app/shared/components/markdown-editor.component.html +++ b/src/Squidex/app/shared/components/markdown-editor.component.html @@ -1,5 +1,5 @@
-
+
diff --git a/src/Squidex/app/shared/components/markdown-editor.component.ts b/src/Squidex/app/shared/components/markdown-editor.component.ts index 3d673176a..4b1b23514 100644 --- a/src/Squidex/app/shared/components/markdown-editor.component.ts +++ b/src/Squidex/app/shared/components/markdown-editor.component.ts @@ -6,12 +6,13 @@ */ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Renderer2, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AssetDto, DialogModel, ResourceLoaderService, + StatefulControlComponent, Types } from '@app/shared/internal'; @@ -21,6 +22,10 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MarkdownEditorComponent), multi: true }; +interface State { + isFullscreen: false; +} + @Component({ selector: 'sqx-markdown-editor', styleUrls: ['./markdown-editor.component.scss'], @@ -28,9 +33,7 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class MarkdownEditorComponent extends StatefulControlComponent implements AfterViewInit { private simplemde: any; private value: string; private isDisabled = false; @@ -46,13 +49,14 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI @ViewChild('inner') public inner: ElementRef; - public isFullscreen = false; - - constructor( - private readonly changeDetector: ChangeDetectorRef, + constructor(changeDetector: ChangeDetectorRef, private readonly renderer: Renderer2, private readonly resourceLoader: ResourceLoaderService ) { + super(changeDetector, { + isFullscreen: false + }); + this.resourceLoader.loadStyle('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css'); } @@ -72,18 +76,8 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI } } - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - private showSelector = () => { this.assetsDialog.show(); - - this.changeDetector.detectChanges(); } public ngAfterViewInit() { @@ -182,17 +176,17 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI }); this.simplemde.codemirror.on('refresh', () => { - this.isFullscreen = this.simplemde.isFullscreenActive(); + const isFullscreen = this.simplemde.isFullscreenActive(); let target = this.container.nativeElement; - if (this.isFullscreen) { + if (isFullscreen) { target = document.body; } this.renderer.appendChild(target, this.inner.nativeElement); - this.changeDetector.detectChanges(); + this.next(s => ({ ...s, isFullscreen })); }); this.simplemde.codemirror.on('blur', () => { diff --git a/src/Squidex/app/shared/components/rich-editor.component.ts b/src/Squidex/app/shared/components/rich-editor.component.ts index 5eee435e5..8abe54b0e 100644 --- a/src/Squidex/app/shared/components/rich-editor.component.ts +++ b/src/Squidex/app/shared/components/rich-editor.component.ts @@ -6,11 +6,12 @@ */ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AssetDto, DialogModel, + ExternalControlComponent, ResourceLoaderService, Types } from '@app/shared/internal'; @@ -28,9 +29,7 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class RichEditorComponent extends ExternalControlComponent implements AfterViewInit, OnDestroy { private tinyEditor: any; private tinyInitTimer: any; private value: string; @@ -44,10 +43,10 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, @Output() public assetPluginClicked = new EventEmitter(); - constructor( - private readonly changeDetector: ChangeDetectorRef, + constructor(changeDetector: ChangeDetectorRef, private readonly resourceLoader: ResourceLoaderService ) { + super(changeDetector); } public ngOnDestroy() { @@ -66,8 +65,6 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, private showSelector = () => { this.assetsDialog.show(); - - this.changeDetector.detectChanges(); } private getEditorOptions() { @@ -132,14 +129,6 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, } } - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public insertAssets(assets: AssetDto[]) { let content = ''; diff --git a/src/Squidex/app/shared/state/ui.state.ts b/src/Squidex/app/shared/state/ui.state.ts index 84a633a81..f71f500c2 100644 --- a/src/Squidex/app/shared/state/ui.state.ts +++ b/src/Squidex/app/shared/state/ui.state.ts @@ -53,7 +53,7 @@ export class UIState extends State { this.uiService.getSettings(this.appName) .subscribe(dtos => { - return this.next({ settings: dtos }); + return this.next(s => ({ ...s, settings: dtos })); }); } @@ -65,7 +65,7 @@ export class UIState extends State { current[key] = value; - this.next({ settings: root }); + this.next(s => ({ ...s, settings: root })); } } @@ -77,7 +77,7 @@ export class UIState extends State { delete current[key]; - this.next({ settings: root }); + this.next(s => ({ ...s, settings: root })); } } From 0372d6b6a674eb374db0c203a2977ea8ef20f8d1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 30 Jan 2019 18:56:36 +0100 Subject: [PATCH 04/16] Fix. --- .../app/features/content/shared/assets-editor.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.html b/src/Squidex/app/features/content/shared/assets-editor.component.html index 11661283c..7d2eb53e1 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.html +++ b/src/Squidex/app/features/content/shared/assets-editor.component.html @@ -20,7 +20,7 @@
- +
From a31ab53f217cb226facbe40521f3d068a7deaee3 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 31 Jan 2019 20:51:11 +0100 Subject: [PATCH 05/16] Base components. --- .../event-consumers-page.component.ts | 21 ++---- .../pages/restore/restore-page.component.ts | 28 ++++---- .../pages/users/user-page.component.ts | 18 ++--- .../pages/content/content-page.component.ts | 54 +++++++-------- .../contents-filters-page.component.ts | 17 ++--- .../pages/contents/contents-page.component.ts | 29 +++------ .../shared/array-editor.component.html | 2 +- .../content/shared/array-editor.component.ts | 21 ++++-- .../shared/assets-editor.component.html | 10 +-- .../content/shared/assets-editor.component.ts | 36 +++++----- .../shared/preview-button.component.html | 10 +-- .../shared/preview-button.component.ts | 39 +++++++---- .../shared/references-editor.component.ts | 4 +- .../pages/dashboard-page.component.ts | 27 +++----- .../pages/schema/schema-page.component.ts | 25 ++++--- .../pages/schema/types/number-ui.component.ts | 29 ++++----- .../pages/schema/types/string-ui.component.ts | 28 ++++---- .../types/string-validation.component.ts | 16 ++--- .../pages/schemas/schemas-page.component.ts | 32 ++++----- .../pages/backups/backups-page.component.ts | 24 +++---- .../contributors-page.component.ts | 4 +- .../languages/languages-page.component.ts | 21 +++--- .../angular/forms/autocomplete.component.ts | 5 +- .../angular/forms/code-editor.component.ts | 3 +- .../angular/forms/control-errors.component.ts | 4 +- .../forms/date-time-editor.component.ts | 4 +- .../angular/forms/iframe-editor.component.ts | 2 +- .../angular/forms/json-editor.component.ts | 2 + .../angular/forms/stars.component.ts | 1 + .../angular/forms/tag-editor.component.html | 2 +- .../angular/forms/tag-editor.component.ts | 3 +- .../angular/forms/toggle.component.ts | 4 +- .../angular/ignore-scrollbar.directive.ts | 25 +++---- .../angular/image-source.directive.ts | 19 +++--- .../modals/dialog-renderer.component.ts | 13 ++-- .../modals/modal-dialog.component.html | 4 +- .../angular/modals/modal-dialog.component.ts | 27 ++++---- .../angular/modals/modal-target.directive.ts | 36 ++++------ .../angular/modals/modal-view.directive.ts | 37 +++-------- .../modals/onboarding-tooltip.component.ts | 10 +-- .../angular/modals/tooltip.component.ts | 4 +- .../angular/routers/parent-link.directive.ts | 17 ++--- .../angular/shortcut.component.spec.ts | 18 +++-- .../app/framework/angular/sorted.directive.ts | 4 +- .../framework/angular/stateful.component.ts | 53 ++++++++++----- .../angular/user-report.component.ts | 8 +-- .../app/framework/services/loading.service.ts | 4 +- src/Squidex/app/framework/state.ts | 14 ++-- .../shared/components/app-form.component.ts | 3 +- .../shared/components/asset.component.html | 28 ++++---- .../app/shared/components/asset.component.ts | 50 +++++++------- .../components/assets-selector.component.html | 18 ++--- .../components/assets-selector.component.ts | 60 ++++++++++------- .../shared/components/comments.component.ts | 19 +++--- .../geolocation-editor.component.ts | 1 + .../components/markdown-editor.component.ts | 4 +- .../shared/components/permission.directive.ts | 44 +++++++++++-- .../components/rich-editor.component.ts | 8 ++- .../components/schema-category.component.html | 12 ++-- .../components/schema-category.component.scss | 8 ++- .../components/schema-category.component.ts | 65 ++++++++++++------- src/Squidex/app/shared/state/apps.state.ts | 2 +- src/Squidex/app/shared/state/schemas.state.ts | 17 +++-- .../pages/internal/internal-area.component.ts | 19 ++---- .../internal/profile-menu.component.html | 6 +- .../pages/internal/profile-menu.component.ts | 47 +++++++------- 66 files changed, 618 insertions(+), 611 deletions(-) diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index d7c4b44ce..ffde74647 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -5,11 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Subscription, timer } from 'rxjs'; +import { Component, OnInit } from '@angular/core'; +import { timer } from 'rxjs'; import { onErrorResumeNext, switchMap } from 'rxjs/operators'; -import { DialogModel } from '@app/shared'; +import { DialogModel, ResourceOwner } from '@app/shared'; import { EventConsumerDto } from './../../services/event-consumers.service'; import { EventConsumersState } from './../../state/event-consumers.state'; @@ -19,27 +19,20 @@ import { EventConsumersState } from './../../state/event-consumers.state'; styleUrls: ['./event-consumers-page.component.scss'], templateUrl: './event-consumers-page.component.html' }) -export class EventConsumersPageComponent implements OnDestroy, OnInit { - private timerSubscription: Subscription; - +export class EventConsumersPageComponent extends ResourceOwner implements OnInit { public eventConsumerErrorDialog = new DialogModel(); public eventConsumerError = ''; constructor( public readonly eventConsumersState: EventConsumersState ) { - } - - public ngOnDestroy() { - this.timerSubscription.unsubscribe(); + super(); } public ngOnInit() { this.eventConsumersState.load().pipe(onErrorResumeNext()).subscribe(); - this.timerSubscription = - timer(2000, 2000).pipe(switchMap(x => this.eventConsumersState.load(true, true).pipe(onErrorResumeNext()))) - .subscribe(); + this.takeOver(timer(2000, 2000).pipe(switchMap(() => this.eventConsumersState.load(true, true)))); } public reload() { @@ -58,7 +51,7 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit { this.eventConsumersState.reset(es).pipe(onErrorResumeNext()).subscribe(); } - public trackByEventConsumer(index: number, es: EventConsumerDto) { + public trackByEventConsumer(es: EventConsumerDto) { return es.name; } diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts b/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts index 026f0becb..27c25e171 100644 --- a/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts +++ b/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts @@ -5,15 +5,16 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; -import { Subscription, timer } from 'rxjs'; -import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators'; +import { timer } from 'rxjs'; +import { onErrorResumeNext, switchMap } from 'rxjs/operators'; import { AuthService, BackupsService, DialogService, + ResourceOwner, RestoreDto, RestoreForm } from '@app/shared'; @@ -23,9 +24,7 @@ import { styleUrls: ['./restore-page.component.scss'], templateUrl: './restore-page.component.html' }) -export class RestorePageComponent implements OnDestroy, OnInit { - private timerSubscription: Subscription; - +export class RestorePageComponent extends ResourceOwner implements OnInit { public restoreJob: RestoreDto | null; public restoreForm = new RestoreForm(this.formBuilder); @@ -35,18 +34,17 @@ export class RestorePageComponent implements OnDestroy, OnInit { private readonly dialogs: DialogService, private readonly formBuilder: FormBuilder ) { - } - - public ngOnDestroy() { - this.timerSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.timerSubscription = - timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore().pipe(onErrorResumeNext())), filter(x => !!x)) - .subscribe(dto => { - this.restoreJob = dto!; - }); + this.takeOver( + timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore().pipe(onErrorResumeNext()))) + .subscribe(job => { + if (job) { + this.restoreJob = job; + } + })); } public restore() { diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.ts b/src/Squidex/app/features/administration/pages/users/user-page.component.ts index 27d10c5ce..59750b200 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.ts +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.ts @@ -5,10 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; + +import { ResourceOwner } from '@app/shared'; import { UserDto } from './../../services/users.service'; import { UserForm, UsersState } from './../../state/users.state'; @@ -18,9 +19,7 @@ import { UserForm, UsersState } from './../../state/users.state'; styleUrls: ['./user-page.component.scss'], templateUrl: './user-page.component.html' }) -export class UserPageComponent implements OnDestroy, OnInit { - private selectedUserSubscription: Subscription; - +export class UserPageComponent extends ResourceOwner implements OnInit { public canUpdate = false; public user?: { user: UserDto, isCurrentUser: boolean }; @@ -32,14 +31,11 @@ export class UserPageComponent implements OnDestroy, OnInit { private readonly route: ActivatedRoute, private readonly router: Router ) { - } - - public ngOnDestroy() { - this.selectedUserSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.selectedUserSubscription = + this.takeOver( this.usersState.selectedUser .subscribe(selectedUser => { this.user = selectedUser!; @@ -47,7 +43,7 @@ export class UserPageComponent implements OnDestroy, OnInit { if (selectedUser) { this.userForm.load(selectedUser.user); } - }); + })); } public save() { diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index a880fa5c2..6508507e2 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -5,10 +5,10 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { Observable, of, Subscription } from 'rxjs'; -import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { onErrorResumeNext, switchMap } from 'rxjs/operators'; import { ContentVersionSelected } from './../messages'; @@ -25,6 +25,7 @@ import { LanguagesState, MessageBus, ModalModel, + ResourceOwner, SchemaDetailsDto, SchemasState, Version @@ -40,12 +41,7 @@ import { DueTimeSelectorComponent } from './../../shared/due-time-selector.compo fadeAnimation ] }) -export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit { - private languagesSubscription: Subscription; - private contentSubscription: Subscription; - private contentVersionSelectedSubscription: Subscription; - private selectedSchemaSubscription: Subscription; - +export class ContentPageComponent extends ResourceOwner implements CanComponentDeactivate, OnInit { public schema: SchemaDetailsDto; public content: ContentDto; @@ -70,44 +66,42 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, private readonly router: Router, private readonly schemasState: SchemasState ) { - } - - public ngOnDestroy() { - this.languagesSubscription.unsubscribe(); - this.contentSubscription.unsubscribe(); - this.contentVersionSelectedSubscription.unsubscribe(); - this.selectedSchemaSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.languagesSubscription = + this.takeOver( this.languagesState.languages .subscribe(languages => { this.languages = languages.map(x => x.language); this.language = this.languages.at(0); - }); + })); - this.selectedSchemaSubscription = - this.schemasState.selectedSchema.pipe(filter(s => !!s)) + this.takeOver( + this.schemasState.selectedSchema .subscribe(schema => { - this.schema = schema!; + if (schema) { + this.schema = schema!; - this.contentForm = new EditContentForm(this.schema, this.languages); - }); + this.contentForm = new EditContentForm(this.schema, this.languages); + } + })); - this.contentSubscription = - this.contentsState.selectedContent.pipe(filter(c => !!c)) + this.takeOver( + this.contentsState.selectedContent .subscribe(content => { - this.content = content!; + if (content) { + this.content = content; - this.loadContent(this.content.dataDraft); - }); + this.loadContent(this.content.dataDraft); + } + })); - this.contentVersionSelectedSubscription = + this.takeOver( this.messageBus.of(ContentVersionSelected) .subscribe(message => { this.loadVersion(message.version); - }); + })); } public canDeactivate(): Observable { diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts index 9701956cc..26f45801d 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts @@ -5,13 +5,13 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { Component, OnInit } from '@angular/core'; import { onErrorResumeNext } from 'rxjs/operators'; import { ContentsState, Queries, + ResourceOwner, SchemasState, UIState } from '@app/shared'; @@ -21,9 +21,7 @@ import { styleUrls: ['./contents-filters-page.component.scss'], templateUrl: './contents-filters-page.component.html' }) -export class ContentsFiltersPageComponent implements OnDestroy, OnInit { - private selectedSchemaSubscription: Subscription; - +export class ContentsFiltersPageComponent extends ResourceOwner implements OnInit { public schemaQueries: Queries; constructor( @@ -31,20 +29,17 @@ export class ContentsFiltersPageComponent implements OnDestroy, OnInit { private readonly schemasState: SchemasState, private readonly uiState: UIState ) { - } - - public ngOnDestroy() { - this.selectedSchemaSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.selectedSchemaSubscription = + this.takeOver( this.schemasState.selectedSchema .subscribe(schema => { if (schema) { this.schemaQueries = new Queries(this.uiState, `schemas.${schema.name}`); } - }); + })); } public search(query: string) { diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index d0f79bd98..3bbca2169 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -5,8 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators'; import { @@ -18,6 +17,7 @@ import { LanguagesState, ModalModel, Queries, + ResourceOwner, SchemaDetailsDto, SchemasState, UIState @@ -30,11 +30,7 @@ import { DueTimeSelectorComponent } from './../../shared/due-time-selector.compo styleUrls: ['./contents-page.component.scss'], templateUrl: './contents-page.component.html' }) -export class ContentsPageComponent implements OnDestroy, OnInit { - private contentsSubscription: Subscription; - private languagesSubscription: Subscription; - private selectedSchemaSubscription: Subscription; - +export class ContentsPageComponent extends ResourceOwner implements OnInit { public schema: SchemaDetailsDto; public schemaQueries: Queries; @@ -61,16 +57,11 @@ export class ContentsPageComponent implements OnDestroy, OnInit { private readonly schemasState: SchemasState, private readonly uiState: UIState ) { - } - - public ngOnDestroy() { - this.contentsSubscription.unsubscribe(); - this.languagesSubscription.unsubscribe(); - this.selectedSchemaSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.selectedSchemaSubscription = + this.takeOver( this.schemasState.selectedSchema .subscribe(schema => { this.resetSelection(); @@ -79,20 +70,20 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.schemaQueries = new Queries(this.uiState, `schemas.${this.schema.name}`); this.contentsState.init().pipe(onErrorResumeNext()).subscribe(); - }); + })); - this.contentsSubscription = + this.takeOver( this.contentsState.contents .subscribe(() => { this.updateSelectionSummary(); - }); + })); - this.languagesSubscription = + this.takeOver( this.languagesState.languages .subscribe(languages => { this.languages = languages.map(x => x.language); this.language = this.languages.at(0); - }); + })); } public reload() { diff --git a/src/Squidex/app/features/content/shared/array-editor.component.html b/src/Squidex/app/features/content/shared/array-editor.component.html index 81f476dfc..449225ce4 100644 --- a/src/Squidex/app/features/content/shared/array-editor.component.html +++ b/src/Squidex/app/features/content/shared/array-editor.component.html @@ -5,7 +5,7 @@ { @Input() public form: EditContentForm; @@ -37,10 +42,14 @@ export class ArrayEditorComponent { @Input() public arrayControl: FormArray; - public isHidden = false; + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + isHidden: false + }); + } - public hide(hide: boolean) { - this.isHidden = hide; + public hide(isHidden: boolean) { + this.next(s => ({ ...s, isHidden })); } public removeItem(index: number) { diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.html b/src/Squidex/app/features/content/shared/assets-editor.component.html index 7d2eb53e1..10412407d 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.html +++ b/src/Squidex/app/features/content/shared/assets-editor.component.html @@ -22,10 +22,10 @@
- -
@@ -33,14 +33,14 @@
-
-
+
diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/src/Squidex/app/features/content/shared/assets-editor.component.ts index 3eb4eff53..642805e90 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.ts +++ b/src/Squidex/app/features/content/shared/assets-editor.component.ts @@ -35,9 +35,9 @@ class AssetUpdated { } interface State { - newAssets: ImmutableArray; + assetFiles: ImmutableArray; - oldAssets: ImmutableArray; + assets: ImmutableArray; isListView: boolean; } @@ -59,22 +59,22 @@ export class AssetsEditorComponent extends StatefulControlComponent x.id).values)) { + if (!Types.isEquals(obj, this.snapshot.assets.map(x => x.id).values)) { const assetIds: string[] = obj; this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj) .subscribe(dtos => { this.setAssets(ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)!).filter(a => !!a))); - if (this.snapshot.oldAssets.length !== assetIds.length) { + if (this.snapshot.assets.length !== assetIds.length) { this.updateValue(); } }, () => { @@ -91,17 +91,17 @@ export class AssetsEditorComponent extends StatefulControlComponent { if (event.source !== this) { - this.setAssets(this.snapshot.oldAssets.replaceBy('id', event.asset)); + this.setAssets(this.snapshot.assets.replaceBy('id', event.asset)); } })); } - public setAssets(oldAssets: ImmutableArray) { - this.next(s => ({ ...s, oldAssets })); + public setAssets(assets: ImmutableArray) { + this.next(s => ({ ...s, assets })); } public pasteFiles(event: ClipboardEvent) { @@ -109,7 +109,7 @@ export class AssetsEditorComponent extends StatefulControlComponent ({ ...s, newAssets: s.newAssets.pushFront(file) })); + this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) })); } } } @@ -119,13 +119,13 @@ export class AssetsEditorComponent extends StatefulControlComponent ({ ...s, newAssets: s.newAssets.pushFront(file) })); + this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) })); } } } public selectAssets(assets: AssetDto[]) { - this.setAssets(this.snapshot.oldAssets.push(...assets)); + this.setAssets(this.snapshot.assets.push(...assets)); if (assets.length > 0) { this.updateValue(); @@ -138,8 +138,8 @@ export class AssetsEditorComponent extends StatefulControlComponent ({ ...s, - newAssets: s.newAssets.remove(file), - oldAssets: s.oldAssets.pushFront(asset) + assetFiles: s.assetFiles.remove(file), + assets: s.assets.pushFront(asset) })); this.updateValue(); @@ -156,14 +156,14 @@ export class AssetsEditorComponent extends StatefulControlComponent ({ ...s, newAssets: s.newAssets.remove(file) })); + this.next(s => ({ ...s, assetFiles: s.assetFiles.remove(file) })); } public changeView(isListView: boolean) { @@ -173,7 +173,7 @@ export class AssetsEditorComponent extends StatefulControlComponent x.id); + let ids: string[] | null = this.snapshot.assets.values.map(x => x.id); if (ids.length === 0) { ids = null; diff --git a/src/Squidex/app/features/content/shared/preview-button.component.html b/src/Squidex/app/features/content/shared/preview-button.component.html index 013dc9abb..1c189d2c4 100644 --- a/src/Squidex/app/features/content/shared/preview-button.component.html +++ b/src/Squidex/app/features/content/shared/preview-button.component.html @@ -1,16 +1,16 @@ - + Preview:
- - diff --git a/src/Squidex/app/features/content/shared/preview-button.component.ts b/src/Squidex/app/features/content/shared/preview-button.component.ts index e47e393fd..197fe8b65 100644 --- a/src/Squidex/app/features/content/shared/preview-button.component.ts +++ b/src/Squidex/app/features/content/shared/preview-button.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ContentDto, @@ -13,9 +13,16 @@ import { interpolate, LocalStoreService, ModalModel, - SchemaDetailsDto + SchemaDetailsDto, + StatefulComponent } from '@app/shared'; +interface State { + selectedName?: string; + + alternativeNames: string[]; +} + @Component({ selector: 'sqx-preview-button', styleUrls: ['./preview-button.component.scss'], @@ -25,7 +32,7 @@ import { ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class PreviewButtonComponent implements OnInit { +export class PreviewButtonComponent extends StatefulComponent implements OnInit { @Input() public content: ContentDto; @@ -34,13 +41,12 @@ export class PreviewButtonComponent implements OnInit { public dropdown = new ModalModel(); - public selectedName: string | undefined; - - public alternativeNames: string[]; - - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly localStore: LocalStoreService ) { + super(changeDetector, { + alternativeNames: [] + }); } public ngOnInit() { @@ -62,16 +68,23 @@ export class PreviewButtonComponent implements OnInit { } private selectUrl(selectedName: string) { - if (this.selectedName !== selectedName) { + this.next(s => { + if (selectedName === s.selectedName) { + return s; + } + const state = { ...s }; + const keys = Object.keys(this.schema.previewUrls); - this.selectedName = selectedName; + state.selectedName = selectedName; - this.alternativeNames = keys.filter(x => x !== this.selectedName); - this.alternativeNames.sort(); + state.alternativeNames = keys.filter(x => x !== s.selectedName); + state.alternativeNames.sort(); this.localStore.set(this.configKey(), selectedName); - } + + return state; + }); } private configKey() { diff --git a/src/Squidex/app/features/content/shared/references-editor.component.ts b/src/Squidex/app/features/content/shared/references-editor.component.ts index 8485eb2f9..fd5befd25 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.ts +++ b/src/Squidex/app/features/content/shared/references-editor.component.ts @@ -29,7 +29,8 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { }; interface State { - schema?: SchemaDetailsDto; + schema?: SchemaDetailsDto | null; + schemaInvalid: boolean; contentItems: ImmutableArray; @@ -61,6 +62,7 @@ export class ReferencesEditorComponent extends StatefulControlComponent this.usagesService.getTodayStorage(app.name))) .subscribe(dto => { @@ -123,7 +114,7 @@ export class DashboardPageComponent implements OnDestroy, OnInit { this.assetsMax = dto.maxAllowed; })); - this.subscriptions.push( + this.takeOver( this.app.pipe( switchMap(app => this.usagesService.getMonthCalls(app.name))) .subscribe(dto => { @@ -131,14 +122,14 @@ export class DashboardPageComponent implements OnDestroy, OnInit { this.callsMax = dto.maxAllowed; })); - this.subscriptions.push( + this.takeOver( this.app.pipe( switchMap(app => this.historyService.getHistory(app.name, ''))) .subscribe(dto => { this.history = dto; })); - this.subscriptions.push( + this.takeOver( this.app.pipe( switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))) .subscribe(dtos => { @@ -175,7 +166,7 @@ export class DashboardPageComponent implements OnDestroy, OnInit { }; })); - this.subscriptions.push( + this.takeOver( this.app.pipe( switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))) .subscribe(dtos => { diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index 4c5978824..ba66a2d37 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -9,8 +9,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; -import { filter, onErrorResumeNext } from 'rxjs/operators'; +import { onErrorResumeNext } from 'rxjs/operators'; import { AppsState, @@ -21,6 +20,7 @@ import { MessageBus, ModalModel, PatternsState, + ResourceOwner, SchemaDetailsDto, SchemasState, Types @@ -38,9 +38,7 @@ import { fadeAnimation ] }) -export class SchemaPageComponent implements OnDestroy, OnInit { - private selectedSchemaSubscription: Subscription; - +export class SchemaPageComponent extends ResourceOwner implements OnDestroy, OnInit { public fieldTypes = fieldTypes; public schemaExport: any; @@ -64,22 +62,21 @@ export class SchemaPageComponent implements OnDestroy, OnInit { private readonly router: Router, private readonly messageBus: MessageBus ) { - } - - public ngOnDestroy() { - this.selectedSchemaSubscription.unsubscribe(); + super(); } public ngOnInit() { this.patternsState.load().pipe(onErrorResumeNext()).subscribe(); - this.selectedSchemaSubscription = - this.schemasState.selectedSchema.pipe(filter(s => !!s)) + this.takeOver( + this.schemasState.selectedSchema .subscribe(schema => { - this.schema = schema!; + if (schema) { + this.schema = schema; - this.export(); - }); + this.export(); + } + })); } public publish() { diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts index 5a1394aaf..306edee1c 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts @@ -5,22 +5,24 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { Observable, Subscription } from 'rxjs'; +import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; -import { FieldDto, FloatConverter, NumberFieldPropertiesDto } from '@app/shared'; +import { + FieldDto, + FloatConverter, + NumberFieldPropertiesDto, + ResourceOwner +} from '@app/shared'; @Component({ selector: 'sqx-number-ui', styleUrls: ['number-ui.component.scss'], templateUrl: 'number-ui.component.html' }) -export class NumberUIComponent implements OnDestroy, OnInit { - private hideAllowedValuesSubscription: Subscription; - private hideInlineEditableSubscription: Subscription; - +export class NumberUIComponent extends ResourceOwner implements OnInit { @Input() public editForm: FormGroup; @@ -35,11 +37,6 @@ export class NumberUIComponent implements OnDestroy, OnInit { public hideAllowedValues: Observable; public hideInlineEditable: Observable; - public ngOnDestroy() { - this.hideAllowedValuesSubscription.unsubscribe(); - this.hideInlineEditableSubscription.unsubscribe(); - } - public ngOnInit() { this.editForm.setControl('editor', new FormControl(this.properties.editor, [ @@ -60,18 +57,18 @@ export class NumberUIComponent implements OnDestroy, OnInit { this.editForm.controls['editor'].valueChanges.pipe( startWith(this.properties.editor), map(x => !(x && (x === 'Input' || x === 'Dropdown')))); - this.hideAllowedValuesSubscription = + this.takeOver( this.hideAllowedValues.subscribe(isSelection => { if (isSelection) { this.editForm.controls['allowedValues'].setValue(undefined); } - }); + })); - this.hideInlineEditableSubscription = + this.takeOver( this.hideInlineEditable.subscribe(isSelection => { if (isSelection) { this.editForm.controls['inlineEditable'].setValue(false); } - }); + })); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts index cee172769..8e1ad06dd 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts @@ -5,22 +5,23 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { Observable, Subscription } from 'rxjs'; +import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; -import { FieldDto, StringFieldPropertiesDto } from '@app/shared'; +import { + FieldDto, + ResourceOwner, + StringFieldPropertiesDto +} from '@app/shared'; @Component({ selector: 'sqx-string-ui', styleUrls: ['string-ui.component.scss'], templateUrl: 'string-ui.component.html' }) -export class StringUIComponent implements OnDestroy, OnInit { - private hideAllowedValuesSubscription: Subscription; - private hideInlineEditableSubscription: Subscription; - +export class StringUIComponent extends ResourceOwner implements OnInit { @Input() public editForm: FormGroup; @@ -33,11 +34,6 @@ export class StringUIComponent implements OnDestroy, OnInit { public hideAllowedValues: Observable; public hideInlineEditable: Observable; - public ngOnDestroy() { - this.hideAllowedValuesSubscription.unsubscribe(); - this.hideInlineEditableSubscription.unsubscribe(); - } - public ngOnInit() { this.editForm.setControl('editor', new FormControl(this.properties.editor, [ @@ -58,18 +54,18 @@ export class StringUIComponent implements OnDestroy, OnInit { this.editForm.controls['editor'].valueChanges.pipe( startWith(this.properties.editor), map(x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug')))); - this.hideAllowedValuesSubscription = + this.takeOver( this.hideAllowedValues.subscribe(isSelection => { if (isSelection) { this.editForm.controls['allowedValues'].setValue(undefined); } - }); + })); - this.hideInlineEditableSubscription = + this.takeOver( this.hideInlineEditable.subscribe(isSelection => { if (isSelection) { this.editForm.controls['inlineEditable'].setValue(false); } - }); + })); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts index 3f1c137a0..ed7cd48e4 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { Observable, Subscription } from 'rxjs'; +import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { @@ -15,6 +15,7 @@ import { FieldDto, ImmutableArray, ModalModel, + ResourceOwner, RootFieldDto, StringFieldPropertiesDto, Types @@ -25,9 +26,7 @@ import { styleUrls: ['string-validation.component.scss'], templateUrl: 'string-validation.component.html' }) -export class StringValidationComponent implements OnDestroy, OnInit { - private patternSubscription: Subscription; - +export class StringValidationComponent extends ResourceOwner implements OnDestroy, OnInit { @Input() public editForm: FormGroup; @@ -49,10 +48,6 @@ export class StringValidationComponent implements OnDestroy, OnInit { public showUnique: boolean; - public ngOnDestroy() { - this.patternSubscription.unsubscribe(); - } - public ngOnInit() { this.showUnique = Types.is(this.field, RootFieldDto) && !this.field.isLocalizable; @@ -87,14 +82,15 @@ export class StringValidationComponent implements OnDestroy, OnInit { this.showPatternMessage = this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0; - this.patternSubscription = + this.takeOver( this.editForm.controls['pattern'].valueChanges .subscribe((value: string) => { if (!value || value.length === 0) { this.editForm.controls['patternMessage'].setValue(undefined); } + this.setPatternName(); - }); + })); this.setPatternName(); } diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index 6b212f8b5..62bc3ec20 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -5,10 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; import { map, onErrorResumeNext } from 'rxjs/operators'; import { @@ -16,6 +15,7 @@ import { CreateCategoryForm, DialogModel, MessageBus, + ResourceOwner, SchemaDto, SchemasState } from '@app/shared'; @@ -27,9 +27,7 @@ import { SchemaCloning } from './../messages'; styleUrls: ['./schemas-page.component.scss'], templateUrl: './schemas-page.component.html' }) -export class SchemasPageComponent implements OnDestroy, OnInit { - private schemaCloningSubscription: Subscription; - +export class SchemasPageComponent extends ResourceOwner implements OnInit { public addSchemaDialog = new DialogModel(); public addCategoryForm = new CreateCategoryForm(this.formBuilder); @@ -45,27 +43,25 @@ export class SchemasPageComponent implements OnDestroy, OnInit { private readonly route: ActivatedRoute, private readonly router: Router ) { - } - - public ngOnDestroy() { - this.schemaCloningSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.schemaCloningSubscription = + this.takeOver( this.messageBus.of(SchemaCloning) .subscribe(m => { this.import = m.schema; this.addSchemaDialog.show(); - }); - - this.route.params.pipe(map(q => q['showDialog'])) - .subscribe(showDialog => { - if (showDialog) { - this.addSchemaDialog.show(); - } - }); + })); + + this.takeOver( + this.route.params.pipe(map(q => q['showDialog'])) + .subscribe(showDialog => { + if (showDialog) { + this.addSchemaDialog.show(); + } + })); this.schemasState.load().pipe(onErrorResumeNext()).subscribe(); } diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts index b2ef916a0..3ae10a231 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts @@ -5,14 +5,15 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Subscription, timer } from 'rxjs'; +import { Component, OnInit } from '@angular/core'; +import { timer } from 'rxjs'; import { onErrorResumeNext, switchMap } from 'rxjs/operators'; import { AppsState, BackupDto, - BackupsState + BackupsState, + ResourceOwner } from '@app/shared'; @Component({ @@ -20,25 +21,20 @@ import { styleUrls: ['./backups-page.component.scss'], templateUrl: './backups-page.component.html' }) -export class BackupsPageComponent implements OnInit, OnDestroy { - private timerSubscription: Subscription; - +export class BackupsPageComponent extends ResourceOwner implements OnInit { constructor( public readonly appsState: AppsState, public readonly backupsState: BackupsState ) { - } - - public ngOnDestroy() { - this.timerSubscription.unsubscribe(); + super(); } public ngOnInit() { this.backupsState.load().pipe(onErrorResumeNext()).subscribe(); - this.timerSubscription = - timer(3000, 3000).pipe(switchMap(t => this.backupsState.load(true, true).pipe(onErrorResumeNext()))) - .subscribe(); + this.takeOver( + timer(3000, 3000).pipe(switchMap(() => this.backupsState.load(true, true).pipe(onErrorResumeNext()))) + .subscribe()); } public reload() { @@ -53,7 +49,7 @@ export class BackupsPageComponent implements OnInit, OnDestroy { this.backupsState.delete(backup).pipe(onErrorResumeNext()).subscribe(); } - public trackByBackup(index: number, item: BackupDto) { + public trackByBackup(item: BackupDto) { return item.id; } } diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts index fddd713e5..b540f1b75 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts @@ -8,7 +8,7 @@ import { Component, Injectable, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { Observable } from 'rxjs'; -import { filter, onErrorResumeNext, withLatestFrom } from 'rxjs/operators'; +import { onErrorResumeNext, withLatestFrom } from 'rxjs/operators'; import { AppContributorDto, @@ -34,7 +34,7 @@ export class UsersDataSource implements AutocompleteSource { public find(query: string): Observable { return this.usersService.getUsers(query).pipe( - withLatestFrom(this.contributorsState.contributors.pipe(filter(x => !!x)), (users, contributors) => { + withLatestFrom(this.contributorsState.contributors, (users, contributors) => { const results: any[] = []; for (let user of users) { diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts index 179323835..a56c5b648 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts @@ -5,16 +5,16 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; -import { Subscription } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { AddLanguageForm, AppLanguageDto, AppsState, - LanguagesState + LanguagesState, + ResourceOwner } from '@app/shared'; @Component({ @@ -22,9 +22,7 @@ import { styleUrls: ['./languages-page.component.scss'], templateUrl: './languages-page.component.html' }) -export class LanguagesPageComponent implements OnDestroy, OnInit { - private newLanguagesSubscription: Subscription; - +export class LanguagesPageComponent extends ResourceOwner implements OnInit { public addLanguageForm = new AddLanguageForm(this.formBuilder); constructor( @@ -32,20 +30,17 @@ export class LanguagesPageComponent implements OnDestroy, OnInit { public readonly languagesState: LanguagesState, private readonly formBuilder: FormBuilder ) { - } - - public ngOnDestroy() { - this.newLanguagesSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.newLanguagesSubscription = + this.takeOver( this.languagesState.newLanguages .subscribe(languages => { if (languages.length > 0) { this.addLanguageForm.load({ language: languages.at(0) }); } - }); + })); this.languagesState.load().pipe(onErrorResumeNext()).subscribe(); } @@ -67,7 +62,7 @@ export class LanguagesPageComponent implements OnDestroy, OnInit { } } - public trackByLanguage(index: number, language: { language: AppLanguageDto }) { + public trackByLanguage(language: { language: AppLanguageDto }) { return language.language; } } diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts index 580593e34..5586ef6d4 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts @@ -10,7 +10,7 @@ import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators'; -import { StatefulControlComponent } from '@app/shared'; +import { StatefulControlComponent } from '@app/framework/internal'; export interface AutocompleteSource { find(query: string): Observable; @@ -27,6 +27,7 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { interface State { suggestedItems: any[]; + suggestedIndex: number; } @@ -66,7 +67,7 @@ export class AutocompleteComponent extends StatefulControlComponent { this.callChange(query); diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.ts b/src/Squidex/app/framework/angular/forms/code-editor.component.ts index 056ebfe55..6d99cdef0 100644 --- a/src/Squidex/app/framework/angular/forms/code-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/code-editor.component.ts @@ -41,8 +41,7 @@ export class CodeEditorComponent extends ExternalControlComponent implem @Input() public mode = 'ace/mode/javascript'; - constructor( - changeDetector: ChangeDetectorRef, + constructor(changeDetector: ChangeDetectorRef, private readonly resourceLoader: ResourceLoaderService ) { super(changeDetector); diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/src/Squidex/app/framework/angular/forms/control-errors.component.ts index 26f8a0ce7..cdafba5b4 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.ts @@ -91,7 +91,7 @@ export class ControlErrorsComponent extends StatefulComponent implements this.control = control; if (control) { - this.observe( + this.takeOver( merge(control.valueChanges, control.statusChanges) .subscribe(() => { this.createMessages(); @@ -127,6 +127,6 @@ export class ControlErrorsComponent extends StatefulComponent implements } } - this.next(() => ({ errorMessages })); + this.next(s => ({ ...s, errorMessages })); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts index cca58a42b..cb7c39b86 100644 --- a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts @@ -60,7 +60,7 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string } public ngOnInit() { - this.observe( + this.takeOver( this.timeControl.valueChanges.subscribe(value => { if (!value || value.length === 0) { this.timeValue = null; @@ -71,7 +71,7 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string this.updateValue(); })); - this.observe( + this.takeOver( this.dateControl.valueChanges.subscribe(value => { if (!value || value.length === 0) { this.dateValue = null; diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts index 11146a73c..7573ea6a7 100644 --- a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts @@ -50,7 +50,7 @@ export class IFrameEditorComponent extends ExternalControlComponent impleme } public ngOnInit(): void { - this.observe( + this.takeOver( this.renderer.listen('window', 'message', (event: MessageEvent) => { if (event.source === this.plugin.contentWindow) { const { type } = event.data; diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.ts b/src/Squidex/app/framework/angular/forms/json-editor.component.ts index 143fd095a..e2baa6854 100644 --- a/src/Squidex/app/framework/angular/forms/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/json-editor.component.ts @@ -39,6 +39,8 @@ export class JsonEditorComponent extends ExternalControlComponent implem private readonly resourceLoader: ResourceLoaderService ) { super(changeDetector); + + changeDetector.detach(); } public writeValue(obj: any) { diff --git a/src/Squidex/app/framework/angular/forms/stars.component.ts b/src/Squidex/app/framework/angular/forms/stars.component.ts index 69a0e24ca..59be6fac2 100644 --- a/src/Squidex/app/framework/angular/forms/stars.component.ts +++ b/src/Squidex/app/framework/angular/forms/stars.component.ts @@ -16,6 +16,7 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { interface State { stars: number; + starsArray: number[]; value: number | null; diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/src/Squidex/app/framework/angular/forms/tag-editor.component.html index 36d93bf22..a49056221 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -2,7 +2,7 @@
+ [class.disabled]="addInput.disabled"> {{item}} diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index 8927195c7..ee11740f8 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -78,6 +78,7 @@ interface State { hasFocus: boolean; suggestedItems: string[]; + suggestedIndex: number; items: any[]; @@ -146,7 +147,7 @@ export class TagEditorComponent extends StatefulControlComponent i } public ngOnInit() { - this.observe( + this.takeOver( this.addInput.valueChanges.pipe( tap(() => { this.resetSize(); diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/src/Squidex/app/framework/angular/forms/toggle.component.ts index 5eb23cb82..7105ada32 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.ts +++ b/src/Squidex/app/framework/angular/forms/toggle.component.ts @@ -8,9 +8,7 @@ import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Types } from '@app/framework/internal'; - -import { StatefulControlComponent } from '../stateful.component'; +import { StatefulControlComponent, Types } from '@app/framework/internal'; export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToggleComponent), multi: true diff --git a/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts b/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts index 384bcb25c..8de848e47 100644 --- a/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts +++ b/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts @@ -5,27 +5,23 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, OnInit, Renderer2 } from '@angular/core'; +import { timer } from 'rxjs'; + +import { ResourceOwner } from '@app/framework/internal'; @Directive({ selector: '[sqxIgnoreScrollbar]' }) -export class IgnoreScrollbarDirective implements OnDestroy, OnInit, AfterViewInit { - private resizeListener: Function; +export class IgnoreScrollbarDirective extends ResourceOwner implements OnInit, AfterViewInit { private parent: any; - private checkTimer: any; private scollbarWidth = 0; constructor( private readonly element: ElementRef, private readonly renderer: Renderer2 ) { - } - - public ngOnDestroy() { - clearTimeout(this.checkTimer); - - this.resizeListener(); + super(); } public ngOnInit() { @@ -33,15 +29,12 @@ export class IgnoreScrollbarDirective implements OnDestroy, OnInit, AfterViewIni this.parent = this.renderer.parentNode(this.element.nativeElement); } - this.resizeListener = + this.takeOver( this.renderer.listen(this.element.nativeElement, 'resize', () => { this.reposition(); - }); + })); - this.checkTimer = - setTimeout(() => { - this.reposition(); - }, 100); + this.takeOver(timer(100, 100).subscribe(() => this.reposition)); } public ngAfterViewInit() { diff --git a/src/Squidex/app/framework/angular/image-source.directive.ts b/src/Squidex/app/framework/angular/image-source.directive.ts index cdfb16fd7..7b45bfd4f 100644 --- a/src/Squidex/app/framework/angular/image-source.directive.ts +++ b/src/Squidex/app/framework/angular/image-source.directive.ts @@ -7,18 +7,16 @@ import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, Renderer2 } from '@angular/core'; -import { MathHelper } from '@app/framework/internal'; +import { MathHelper, ResourceOwner } from '@app/framework/internal'; const LAYOUT_CACHE: { [key: string]: { width: number, height: number } } = {}; @Directive({ selector: '[sqxImageSource]' }) -export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, AfterViewInit { - private parentResizeListener: Function; - - private loadingTimer: any; +export class ImageSourceDirective extends ResourceOwner implements OnChanges, OnDestroy, OnInit, AfterViewInit { private size: any; + private loadTimer: any; private loadRetries = 0; private loadQuery: string | null = null; @@ -38,12 +36,13 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After private readonly element: ElementRef, private readonly renderer: Renderer2 ) { + super(); } public ngOnDestroy() { - clearTimeout(this.loadingTimer); + super.ngOnDestroy(); - this.parentResizeListener(); + clearTimeout(this.loadTimer); } public ngOnInit() { @@ -51,10 +50,10 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After this.parent = this.renderer.parentNode(this.element.nativeElement); } - this.parentResizeListener = + this.takeOver( this.renderer.listen(this.parent, 'resize', () => { this.resize(); - }); + })); } public ngAfterViewInit() { @@ -127,7 +126,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After this.loadRetries++; if (this.loadRetries <= 10) { - this.loadingTimer = + this.loadTimer = setTimeout(() => { this.loadQuery = MathHelper.guid(); diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts index 666da8e15..42dff0f73 100644 --- a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts +++ b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts @@ -13,11 +13,10 @@ import { DialogRequest, DialogService, fadeAnimation, - Notification + Notification, + StatefulComponent } from '@app/framework/internal'; -import { StatefulComponent } from '../stateful.component'; - interface State { dialogRequest?: DialogRequest | null; @@ -46,14 +45,14 @@ export class DialogRendererComponent extends StatefulComponent implements } public ngOnInit() { - this.observe( + this.takeOver( this.dialogView.isOpen.subscribe(isOpen => { if (!isOpen) { this.finishRequest(false); } })); - this.observe( + this.takeOver( this.dialogs.notifications.subscribe(notification => { this.next(s => ({ ...s, @@ -61,13 +60,13 @@ export class DialogRendererComponent extends StatefulComponent implements })); if (notification.displayTime > 0) { - this.observe(timer(notification.displayTime).subscribe(() => { + this.takeOver(timer(notification.displayTime).subscribe(() => { this.close(notification); })); } })); - this.observe( + this.takeOver( this.dialogs.dialogs .subscribe(dialogRequest => { this.cancel(); diff --git a/src/Squidex/app/framework/angular/modals/modal-dialog.component.html b/src/Squidex/app/framework/angular/modals/modal-dialog.component.html index d7f3e5955..bf7383621 100644 --- a/src/Squidex/app/framework/angular/modals/modal-dialog.component.html +++ b/src/Squidex/app/framework/angular/modals/modal-dialog.component.html @@ -13,7 +13,7 @@
- -