Browse Source

Refactoring/forms (#800)

* Get rid of formbuilder

* Temp

* Cleanup forms.

* More tests.

* More improvements.

* Cleanup.
pull/802/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
d41b63837d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/i18n/frontend_en.json
  2. 1
      backend/i18n/frontend_it.json
  3. 1
      backend/i18n/frontend_nl.json
  4. 1
      backend/i18n/frontend_zh.json
  5. 1
      backend/i18n/source/frontend_en.json
  6. 4
      frontend/app/features/administration/pages/restore/restore-page.component.ts
  7. 4
      frontend/app/features/administration/pages/users/user-page.component.ts
  8. 40
      frontend/app/features/administration/state/users.forms.ts
  9. 4
      frontend/app/features/assets/pages/asset-tag-dialog.component.ts
  10. 4
      frontend/app/features/content/pages/content/editor/content-field.component.ts
  11. 2
      frontend/app/features/content/pages/content/editor/field-copy-button.component.ts
  12. 4
      frontend/app/features/content/shared/forms/array-item.component.html
  13. 57
      frontend/app/features/content/shared/forms/array-item.component.ts
  14. 2
      frontend/app/features/content/shared/forms/field-editor.component.html
  15. 59
      frontend/app/features/content/shared/forms/iframe-editor.component.ts
  16. 4
      frontend/app/features/rules/pages/rule/rule-page.component.ts
  17. 4
      frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.ts
  18. 4
      frontend/app/features/schemas/pages/schema/export/schema-export-form.component.ts
  19. 6
      frontend/app/features/schemas/pages/schema/fields/field-wizard.component.ts
  20. 4
      frontend/app/features/schemas/pages/schema/fields/field.component.ts
  21. 4
      frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts
  22. 4
      frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts
  23. 4
      frontend/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts
  24. 4
      frontend/app/features/schemas/pages/schemas/schema-form.component.ts
  25. 5
      frontend/app/features/schemas/pages/schemas/schemas-page.component.ts
  26. 4
      frontend/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts
  27. 4
      frontend/app/features/settings/pages/clients/client-add-form.component.ts
  28. 4
      frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts
  29. 4
      frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts
  30. 4
      frontend/app/features/settings/pages/languages/language-add-form.component.ts
  31. 4
      frontend/app/features/settings/pages/languages/language.component.ts
  32. 4
      frontend/app/features/settings/pages/more/more-page.component.ts
  33. 4
      frontend/app/features/settings/pages/roles/role-add-form.component.ts
  34. 4
      frontend/app/features/settings/pages/roles/role.component.ts
  35. 4
      frontend/app/features/settings/pages/settings/settings-page.component.ts
  36. 4
      frontend/app/features/settings/pages/workflows/workflow-add-form.component.ts
  37. 4
      frontend/app/framework/angular/forms/editable-title.component.html
  38. 25
      frontend/app/framework/angular/forms/editable-title.component.ts
  39. 128
      frontend/app/framework/angular/forms/extended-form-array.spec.ts
  40. 38
      frontend/app/framework/angular/forms/extended-form-array.ts
  41. 110
      frontend/app/framework/angular/forms/extended-form-group.spec.ts
  42. 44
      frontend/app/framework/angular/forms/extended-form-group.ts
  43. 12
      frontend/app/framework/angular/forms/forms-helper.ts
  44. 4
      frontend/app/framework/angular/forms/model.ts
  45. 2
      frontend/app/framework/angular/forms/templated-form-array.ts
  46. 2
      frontend/app/framework/angular/forms/templated-form-group.ts
  47. 89
      frontend/app/framework/angular/forms/undefinable-form-array.spec.ts
  48. 71
      frontend/app/framework/angular/forms/undefinable-form-group.spec.ts
  49. 8
      frontend/app/framework/declarations.ts
  50. 4
      frontend/app/shared/components/app-form.component.ts
  51. 4
      frontend/app/shared/components/assets/asset-dialog.component.ts
  52. 4
      frontend/app/shared/components/assets/asset-folder-dialog.component.ts
  53. 4
      frontend/app/shared/components/comments/comments.component.ts
  54. 21
      frontend/app/shared/components/forms/geolocation-editor.component.ts
  55. 8
      frontend/app/shared/components/search/search-form.component.ts
  56. 98
      frontend/app/shared/state/apps.forms.ts
  57. 3
      frontend/app/shared/state/assets.forms.spec.ts
  58. 110
      frontend/app/shared/state/assets.forms.ts
  59. 32
      frontend/app/shared/state/backups.forms.ts
  60. 34
      frontend/app/shared/state/clients.forms.ts
  61. 14
      frontend/app/shared/state/comments.form.ts
  62. 11
      frontend/app/shared/state/contents.forms-helpers.ts
  63. 126
      frontend/app/shared/state/contents.forms.spec.ts
  64. 106
      frontend/app/shared/state/contents.forms.ts
  65. 46
      frontend/app/shared/state/contributors.forms.ts
  66. 46
      frontend/app/shared/state/languages.forms.ts
  67. 44
      frontend/app/shared/state/roles.forms.ts
  68. 41
      frontend/app/shared/state/rules.forms.ts
  69. 416
      frontend/app/shared/state/schemas.forms.ts
  70. 22
      frontend/app/shared/state/workflows.forms.ts

1
backend/i18n/frontend_en.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "Change content item(s) to {action}",
"contents.changeStatusToImmediately": "Set to {action} immediately.",
"contents.changeStatusToLater": "Set to {action} at a later point date and time.",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).",

1
backend/i18n/frontend_it.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "Cambia gli elementi del contenuto in {action}",
"contents.changeStatusToImmediately": "Imposta {action} immediatamente.",
"contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Un elemento del contenuto non è valido, verifica il campo con la barra rossa per tutte le lingue impostate (se presenti).",

1
backend/i18n/frontend_nl.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "Verander inhoud item(s) in {action}",
"contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.",
"contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Inhoudselement niet geldig, controleer het veld met de rode balk aan de linkerkant in alle talen (indien lokaliseerbaar).",

1
backend/i18n/frontend_zh.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "将内容项更改为 {action}",
"contents.changeStatusToImmediately": "立即设置为 {action}。",
"contents.changeStatusToLater": "在稍后的日期和时间设置为 {action}。",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "添加至少一个Schemas来设置组件。",
"contents.componentsNoSchema": "添加至少一个Schemas来添加组件。",
"contents.contentNotValid": "内容元素无效,请用所有语言(如果可本地化)检查左侧带有红色条的字段。",

1
backend/i18n/source/frontend_en.json

@ -408,6 +408,7 @@
"contents.changeStatusTo": "Change content item(s) to {action}",
"contents.changeStatusToImmediately": "Set to {action} immediately.",
"contents.changeStatusToLater": "Set to {action} at a later point date and time.",
"contents.componentInvalid": "Invalid component, schema has been deleted.",
"contents.componentNoSchema": "Add at least one schema to set component.",
"contents.componentsNoSchema": "Add at least one schema to add components.",
"contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).",

4
frontend/app/features/administration/pages/restore/restore-page.component.ts

@ -6,7 +6,6 @@
*/
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AuthService, BackupsService, DialogService, RestoreForm, switchSafe } from '@app/shared';
import { timer } from 'rxjs';
@ -16,7 +15,7 @@ import { timer } from 'rxjs';
templateUrl: './restore-page.component.html',
})
export class RestorePageComponent {
public restoreForm = new RestoreForm(this.formBuilder);
public restoreForm = new RestoreForm();
public restoreJob =
timer(0, 2000).pipe(switchSafe(() => this.backupsService.getRestore()));
@ -25,7 +24,6 @@ export class RestorePageComponent {
public readonly authState: AuthService,
private readonly backupsService: BackupsService,
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder,
) {
}

4
frontend/app/features/administration/pages/users/user-page.component.ts

@ -6,7 +6,6 @@
*/
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { CreateUserDto, UserDto, UserForm, UsersState } from '@app/features/administration/internal';
import { ResourceOwner } from '@app/shared';
@ -20,11 +19,10 @@ export class UserPageComponent extends ResourceOwner implements OnInit {
public isEditable = false;
public user?: UserDto | null;
public userForm = new UserForm(this.formBuilder);
public userForm = new UserForm();
constructor(
public readonly usersState: UsersState,
private readonly formBuilder: FormBuilder,
private readonly route: ActivatedRoute,
private readonly router: Router,
) {

40
frontend/app/features/administration/state/users.forms.ts

@ -5,39 +5,31 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, ValidatorsEx } from '@app/shared';
import { FormControl, Validators } from '@angular/forms';
import { Form, ExtendedFormGroup, ValidatorsEx } from '@app/shared';
import { UpdateUserDto, UserDto } from './../services/users.service';
export class UserForm extends Form<FormGroup, UpdateUserDto, UserDto> {
constructor(
formBuilder: FormBuilder,
) {
super(formBuilder.group({
email: ['',
[
export class UserForm extends Form<ExtendedFormGroup, UpdateUserDto, UserDto> {
constructor() {
super(new ExtendedFormGroup({
email: new FormControl('', [
Validators.email,
Validators.required,
Validators.maxLength(100),
],
],
displayName: ['',
[
]),
displayName: new FormControl('', [
Validators.required,
Validators.maxLength(100),
],
],
password: ['',
[
]),
password: new FormControl('',
Validators.required,
],
],
passwordConfirm: ['',
[
),
passwordConfirm: new FormControl('',
ValidatorsEx.match('password', 'i18n:users.passwordConfirmValidationMessage'),
],
],
permissions: [''],
),
permissions: new FormControl('',
Validators.nullValidator,
),
}));
}

4
frontend/app/features/assets/pages/asset-tag-dialog.component.ts

@ -6,7 +6,6 @@
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AssetsState, RenameAssetTagForm } from '@app/shared/internal';
@Component({
@ -21,11 +20,10 @@ export class AssetTagDialogComponent implements OnInit {
@Input()
public tagName: string;
public editForm = new RenameAssetTagForm(this.formBuilder);
public editForm = new RenameAssetTagForm();
constructor(
private readonly assetsState: AssetsState,
private readonly formBuilder: FormBuilder,
) {
}

4
frontend/app/features/content/pages/content/editor/content-field.component.ts

@ -91,12 +91,12 @@ export class ContentFieldComponent implements OnChanges {
public copy() {
if (this.formModel && this.formModelCompare) {
if (this.showAllControls) {
this.formModel.setValue(this.formModelCompare.getRawValue());
this.formModel.setValue(this.formModelCompare.form.value);
} else {
const target = this.formModel.get(this.language.iso2Code);
if (target) {
target.setValue(this.formModelCompare.get(this.language.iso2Code)?.getRawValue());
target.setValue(this.formModelCompare.get(this.language.iso2Code)?.form.value);
}
}
}

2
frontend/app/features/content/pages/content/editor/field-copy-button.component.ts

@ -49,7 +49,7 @@ export class FieldCopyButtonComponent implements OnChanges {
public copy() {
if (this.copySource && this.copyTargets?.length > 0) {
const source = this.formModel.get(this.copySource).getRawValue();
const source = this.formModel.get(this.copySource).form.value;
for (const target of this.copyTargets) {
if (target !== this.copySource) {

4
frontend/app/features/content/shared/forms/array-item.component.html

@ -54,5 +54,9 @@
[languages]="languages">
</sqx-component-section>
</div>
<sqx-form-hint *ngIf="isInvalidComponent | async">
{{ 'contents.componentInvalid' | sqxTranslate }}
</sqx-form-hint>
</div>
</div>

57
frontend/app/features/content/shared/forms/array-item.component.ts

@ -7,7 +7,8 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectFormBase, RootFieldDto, StatefulComponent, Types, valueProjection$ } from '@app/shared';
import { Observable } from 'rxjs';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { ComponentSectionComponent } from './component-section.component';
interface State {
@ -69,6 +70,7 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
public isCollapsed = false;
public isInvalid: Observable<boolean>;
public isInvalidComponent: Observable<boolean>;
public title: Observable<string>;
@ -87,34 +89,16 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
if (changes['formModel']) {
this.isInvalid = invalid$(this.formModel.form);
this.title = valueProjection$(this.formModel.form, x => this.getTitle(x));
if (Types.is(this.formModel, ComponentForm)) {
this.isInvalidComponent = this.formModel.schemaChanges.pipe(map(x => !x));
} else {
this.isInvalidComponent = of(false);
}
}
private getTitle(value: any) {
const values: string[] = [];
if (Types.is(this.formModel, ComponentForm) && this.formModel.schema) {
values.push(this.formModel.schema.displayName);
}
if (Types.is(this.formModel.field, RootFieldDto)) {
for (const field of this.formModel.field.nested) {
const fieldValue = value[field.name];
if (fieldValue) {
const formatted = FieldFormatter.format(field, fieldValue);
if (formatted) {
values.push(formatted);
}
}
this.title = valueProjection$(this.formModel.form, () => getTitle(this.formModel));
}
}
return values.join(', ');
}
public collapse() {
this.next({ isCollapsed: true });
}
@ -149,3 +133,28 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
return section.separator?.fieldId;
}
}
function getTitle(formModel: ObjectFormBase) {
const value = formModel.form.value;
const values: string[] = [];
if (Types.is(formModel, ComponentForm) && formModel.schema) {
values.push(formModel.schema.displayName);
}
if (Types.is(formModel.field, RootFieldDto)) {
for (const field of formModel.field.nested) {
const fieldValue = value[field.name];
if (fieldValue) {
const formatted = FieldFormatter.format(field, fieldValue);
if (formatted) {
values.push(formatted);
}
}
}
}
return values.join(', ');
}

2
frontend/app/features/content/shared/forms/field-editor.component.html

@ -11,7 +11,7 @@
<ng-container *ngIf="field.properties.editorUrl; else noEditor">
<sqx-iframe-editor [url]="field.properties.editorUrl" #editor
[context]="formContext"
[formControl]="$any(fieldForm)"
[formControlBinding]="$any(fieldForm)"
[formValue]="form.valueChanges | async"
[formIndex]="index"
[language]="language?.iso2Code">

59
frontend/app/features/content/shared/forms/iframe-editor.component.ts

@ -5,33 +5,27 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, HostListener, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogModel, DialogService, StatefulControlComponent, Types } from '@app/framework';
import { DialogModel, DialogService, disabled$, StatefulComponent, Types, value$ } from '@app/framework';
import { AppsState, AssetDto, computeEditorUrl } from '@app/shared';
export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IFrameEditorComponent), multi: true,
};
interface State {
// True, when the editor is shown as fullscreen.
isFullscreen: boolean;
}
@Component({
selector: 'sqx-iframe-editor[context][formValue]',
selector: 'sqx-iframe-editor[context][formValue][formControlBinding]',
styleUrls: ['./iframe-editor.component.scss'],
templateUrl: './iframe-editor.component.html',
providers: [
SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IFrameEditorComponent extends StatefulControlComponent<State, any> implements OnChanges, OnDestroy {
export class IFrameEditorComponent extends StatefulComponent<State> implements OnChanges, OnDestroy {
private value: any;
private isInitialized = false;
private isDisabled = false;
private assetsCorrelationId: any;
@ViewChild('iframe', { static: false })
@ -55,9 +49,12 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
@Input()
public language?: string | null;
@Input()
public formControlBinding: AbstractControl;
@Input()
public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true);
this.updatedisabled(value === true);
}
@Input()
@ -81,11 +78,12 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
}
public ngOnDestroy() {
super.ngOnDestroy();
this.toggleFullscreen(false);
}
public ngOnChanges(changes: SimpleChanges) {
if (this.iframe?.nativeElement) {
if (changes['formValue']) {
this.sendFormValue();
}
@ -97,6 +95,23 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
if (changes['formIndex']) {
this.sendMoved();
}
if (changes['formControlBinding']) {
this.unsubscribeAll();
const control = this.formControlBinding;
if (control) {
this.own(value$(control)
.subscribe(value => {
this.updateValue(value);
}));
this.own(disabled$(control)
.subscribe(isDisabled => {
this.updatedisabled(isDisabled);
}));
}
}
}
@ -135,10 +150,10 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
if (!Types.equals(this.value, value)) {
this.value = value;
this.callChange(value);
this.formControlBinding?.reset(value);
}
} else if (type === 'touched') {
this.callTouched();
this.formControlBinding?.markAsTouched();
} else if (type === 'notifyInfo') {
const { text } = event.data;
@ -182,15 +197,21 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
this.assetsDialog.hide();
}
public writeValue(obj: any) {
public updateValue(obj: any) {
if (!Types.equals(obj, this.value)) {
this.value = obj;
this.sendValue();
}
}
public updatedisabled(isDisabled: boolean) {
if (isDisabled !== this.isDisabled) {
this.isDisabled === isDisabled;
public onDisabled() {
this.sendDisabled();
}
}
public reset() {
this.sendInit();
@ -209,7 +230,7 @@ export class IFrameEditorComponent extends StatefulControlComponent<State, any>
}
private sendDisabled() {
this.sendMessage('disabled', { isDisabled: this.snapshot.isDisabled });
this.sendMessage('disabled', { isDisabled: this.isDisabled });
}
private sendFormValue() {

4
frontend/app/features/rules/pages/rule/rule-page.component.ts

@ -6,7 +6,6 @@
*/
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ActionForm, ALL_TRIGGERS, ResourceOwner, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, TriggerForm } from '@app/shared';
@ -45,7 +44,6 @@ export class RulePageComponent extends ResourceOwner implements OnInit {
public readonly rulesState: RulesState,
public readonly rulesService: RulesService,
public readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder,
private readonly route: ActivatedRoute,
private readonly router: Router,
) {
@ -97,7 +95,7 @@ export class RulePageComponent extends ResourceOwner implements OnInit {
}
public selectTrigger(type: string, values = {}) {
const form = new TriggerForm(this.formBuilder, type);
const form = new TriggerForm(type);
form.setEnabled(this.isEditable);
form.load(values);

4
frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.ts

@ -6,7 +6,6 @@
*/
import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { EditSchemaForm, SchemaDto, SchemasState } from '@app/shared';
@Component({
@ -18,12 +17,11 @@ export class SchemaEditFormComponent implements OnChanges {
@Input()
public schema: SchemaDto;
public fieldForm = new EditSchemaForm(this.formBuilder);
public fieldForm = new EditSchemaForm();
public isEditable?: boolean | null;
constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState,
) {
}

4
frontend/app/features/schemas/pages/schema/export/schema-export-form.component.ts

@ -6,7 +6,6 @@
*/
import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { SchemaDto, SchemasState, SynchronizeSchemaForm } from '@app/shared';
@Component({
@ -18,12 +17,11 @@ export class SchemaExportFormComponent implements OnChanges {
@Input()
public schema: SchemaDto;
public synchronizeForm = new SynchronizeSchemaForm(this.formBuilder);
public synchronizeForm = new SynchronizeSchemaForm();
public isEditable = false;
constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState,
) {
}

6
frontend/app/features/schemas/pages/schema/fields/field-wizard.component.ts

@ -6,7 +6,6 @@
*/
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddFieldForm, AppSettingsDto, createProperties, EditFieldForm, FieldDto, fieldTypes, LanguagesState, RootFieldDto, SchemaDto, SchemasState, Types } from '@app/shared';
const DEFAULT_FIELD = { name: '', partitioning: 'invariant', properties: createProperties('String') };
@ -39,12 +38,11 @@ export class FieldWizardComponent implements OnInit {
public fieldTypes = fieldTypes;
public field: FieldDto;
public addFieldForm = new AddFieldForm(this.formBuilder);
public addFieldForm = new AddFieldForm();
public editForm?: EditFieldForm;
constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState,
public readonly languagesState: LanguagesState,
) {
@ -76,7 +74,7 @@ export class FieldWizardComponent implements OnInit {
this.nameInput.nativeElement.focus();
}
} else if (edit) {
this.editForm = new EditFieldForm(this.formBuilder, this.field.properties);
this.editForm = new EditFieldForm(this.field.properties);
this.editForm.load(this.field.properties);
} else {
this.emitComplete();

4
frontend/app/features/schemas/pages/schema/fields/field.component.ts

@ -7,7 +7,6 @@
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppSettingsDto, createProperties, DialogModel, EditFieldForm, fadeAnimation, LanguageDto, ModalModel, NestedFieldDto, RootFieldDto, SchemaDto, SchemasState, sorted } from '@app/shared';
@Component({
@ -50,7 +49,6 @@ export class FieldComponent implements OnChanges {
public addFieldDialog = new DialogModel();
constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState,
) {
this.trackByFieldFn = this.trackByField.bind(this);
@ -60,7 +58,7 @@ export class FieldComponent implements OnChanges {
if (changes['field']) {
this.isEditable = this.field.canUpdate;
this.editForm = new EditFieldForm(this.formBuilder, this.field.properties);
this.editForm = new EditFieldForm(this.field.properties);
this.editForm.load(this.field.properties);
}
}

4
frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts

@ -6,7 +6,6 @@
*/
import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ConfigurePreviewUrlsForm, SchemaDto, SchemasState } from '@app/shared';
@Component({
@ -18,12 +17,11 @@ export class SchemaPreviewUrlsFormComponent implements OnChanges {
@Input()
public schema: SchemaDto;
public editForm = new ConfigurePreviewUrlsForm(this.formBuilder);
public editForm = new ConfigurePreviewUrlsForm();
public isEditable = false;
constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState,
) {
}

4
frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts

@ -6,7 +6,6 @@
*/
import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ConfigureFieldRulesForm, FIELD_RULE_ACTIONS, SchemaDto, SchemasState } from '@app/shared';
@Component({
@ -18,7 +17,7 @@ export class SchemaFieldRulesFormComponent implements OnChanges {
@Input()
public schema: SchemaDto;
public editForm = new ConfigureFieldRulesForm(this.formBuilder);
public editForm = new ConfigureFieldRulesForm();
public fieldNames: ReadonlyArray<string>;
public fieldActions = FIELD_RULE_ACTIONS;
@ -26,7 +25,6 @@ export class SchemaFieldRulesFormComponent implements OnChanges {
public isEditable = false;
constructor(
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState,
) {
}

4
frontend/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts

@ -6,7 +6,6 @@
*/
import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppsState, EditSchemaScriptsForm, SchemaCompletions, SchemaDto, SchemasService, SchemasState } from '@app/shared';
import { EMPTY, Observable } from 'rxjs';
@ -22,13 +21,12 @@ export class SchemaScriptsFormComponent implements OnChanges {
public schemaScript = 'query';
public schemaCompletions: Observable<SchemaCompletions> = EMPTY;
public editForm = new EditSchemaScriptsForm(this.formBuilder);
public editForm = new EditSchemaScriptsForm();
public isEditable = false;
constructor(
private readonly appsState: AppsState,
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState,
private readonly schemasService: SchemasService,
) {

4
frontend/app/features/schemas/pages/schemas/schema-form.component.ts

@ -6,7 +6,6 @@
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ApiUrlConfig, AppsState, CreateSchemaForm, SchemaDto, SchemasState } from '@app/shared';
@Component({
@ -24,7 +23,7 @@ export class SchemaFormComponent implements OnInit {
@Input()
public import: any;
public createForm = new CreateSchemaForm(this.formBuilder);
public createForm = new CreateSchemaForm();
public showImport = false;
@ -32,7 +31,6 @@ export class SchemaFormComponent implements OnInit {
public readonly apiUrl: ApiUrlConfig,
public readonly appsState: AppsState,
public readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder,
) {
}

5
frontend/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -6,7 +6,7 @@
*/
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { CreateCategoryForm, DialogModel, getCategoryTree, MessageBus, ResourceOwner, SchemaCategory, SchemaDto, SchemasState, value$ } from '@app/shared';
import { combineLatest } from 'rxjs';
@ -20,7 +20,7 @@ import { SchemaCloning } from './../messages';
})
export class SchemasPageComponent extends ResourceOwner implements OnInit {
public addSchemaDialog = new DialogModel();
public addCategoryForm = new CreateCategoryForm(this.formBuilder);
public addCategoryForm = new CreateCategoryForm();
public schemasFilter = new FormControl();
@ -37,7 +37,6 @@ export class SchemasPageComponent extends ResourceOwner implements OnInit {
constructor(
public readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute,
private readonly router: Router,

4
frontend/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts

@ -6,7 +6,6 @@
*/
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppsState, AssetCompletions, AssetScriptsState, AssetsService, EditAssetScriptsForm, ResourceOwner } from '@app/shared';
import { EMPTY, Observable } from 'rxjs';
@ -19,13 +18,12 @@ export class AssetScriptsPageComponent extends ResourceOwner implements OnInit {
public assetScript = 'create';
public assetCompletions: Observable<AssetCompletions> = EMPTY;
public editForm = new EditAssetScriptsForm(this.formBuilder);
public editForm = new EditAssetScriptsForm();
public isEditable = false;
constructor(
private readonly appsState: AppsState,
private readonly formBuilder: FormBuilder,
private readonly assetScriptsState: AssetScriptsState,
private readonly assetsService: AssetsService,
) {

4
frontend/app/features/settings/pages/clients/client-add-form.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddClientForm, ClientsState } from '@app/shared';
@Component({
@ -16,11 +15,10 @@ import { AddClientForm, ClientsState } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClientAddFormComponent {
public addClientForm = new AddClientForm(this.formBuilder);
public addClientForm = new AddClientForm();
constructor(
private readonly clientsState: ClientsState,
private readonly formBuilder: FormBuilder,
) {
}

4
frontend/app/features/settings/pages/contributors/contributor-add-form.component.ts

@ -6,7 +6,6 @@
*/
import { Component, Injectable, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AssignContributorForm, AutocompleteSource, ContributorsState, DialogModel, DialogService, RoleDto, UsersService } from '@app/shared';
import { Observable } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
@ -48,7 +47,7 @@ export class ContributorAddFormComponent implements OnChanges {
@Input()
public roles: ReadonlyArray<RoleDto>;
public assignContributorForm = new AssignContributorForm(this.formBuilder);
public assignContributorForm = new AssignContributorForm();
public importDialog = new DialogModel();
@ -56,7 +55,6 @@ export class ContributorAddFormComponent implements OnChanges {
public readonly contributorsState: ContributorsState,
public readonly usersDataSource: UsersDataSource,
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder,
) {
}

4
frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts

@ -6,7 +6,6 @@
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ContributorsState, ErrorDto, ImportContributorsForm, RoleDto } from '@app/shared';
import { EMPTY, of } from 'rxjs';
import { catchError, mergeMap, tap } from 'rxjs/operators';
@ -30,12 +29,11 @@ export class ImportContributorsDialogComponent {
@Input()
public roles: ReadonlyArray<RoleDto>;
public importForm = new ImportContributorsForm(this.formBuilder);
public importForm = new ImportContributorsForm();
public importStatus: ReadonlyArray<ImportStatus> = [];
public importStage: 'Start' | 'Change' | 'Wait' = 'Start';
constructor(
private readonly formBuilder: FormBuilder,
private readonly contributorsState: ContributorsState,
) {
}

4
frontend/app/features/settings/pages/languages/language-add-form.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddLanguageForm, LanguageDto, LanguagesState } from '@app/shared';
@Component({
@ -19,11 +18,10 @@ export class LanguageAddFormComponent implements OnChanges {
@Input()
public newLanguages: ReadonlyArray<LanguageDto>;
public addLanguageForm = new AddLanguageForm(this.formBuilder);
public addLanguageForm = new AddLanguageForm();
constructor(
private readonly languagesState: LanguagesState,
private readonly formBuilder: FormBuilder,
) {
}

4
frontend/app/features/settings/pages/languages/language.component.ts

@ -7,7 +7,6 @@
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppLanguageDto, EditLanguageForm, LanguageDto, LanguagesState, sorted } from '@app/shared';
@Component({
@ -30,10 +29,9 @@ export class LanguageComponent implements OnChanges {
public isEditing?: boolean | null;
public isEditable = false;
public editForm = new EditLanguageForm(this.formBuilder);
public editForm = new EditLanguageForm();
constructor(
private readonly formBuilder: FormBuilder,
private readonly languagesState: LanguagesState,
) {
}

4
frontend/app/features/settings/pages/more/more-page.component.ts

@ -6,7 +6,6 @@
*/
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { AppDto, AppsState, defined, ResourceOwner, Types, UpdateAppForm } from '@app/shared';
@ -25,11 +24,10 @@ export class MorePageComponent extends ResourceOwner implements OnInit {
public uploading = false;
public uploadProgress = 10;
public updateForm = new UpdateAppForm(this.formBuilder);
public updateForm = new UpdateAppForm();
constructor(
private readonly appsState: AppsState,
private readonly formBuilder: FormBuilder,
private readonly router: Router,
) {
super();

4
frontend/app/features/settings/pages/roles/role-add-form.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddRoleForm, RolesState } from '@app/shared';
@Component({
@ -16,11 +15,10 @@ import { AddRoleForm, RolesState } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RoleAddFormComponent {
public addRoleForm = new AddRoleForm(this.formBuilder);
public addRoleForm = new AddRoleForm();
constructor(
private readonly rolesState: RolesState,
private readonly formBuilder: FormBuilder,
) {
}

4
frontend/app/features/settings/pages/roles/role.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddPermissionForm, AutocompleteComponent, AutocompleteSource, EditRoleForm, RoleDto, RolesState, SchemaDto, Settings } from '@app/shared';
const DESCRIPTIONS = {
@ -64,12 +63,11 @@ export class RoleComponent implements OnChanges {
public isEditing = false;
public isEditable = false;
public addPermissionForm = new AddPermissionForm(this.formBuilder);
public addPermissionForm = new AddPermissionForm();
public editForm = new EditRoleForm();
constructor(
private readonly formBuilder: FormBuilder,
private readonly rolesState: RolesState,
) {
}

4
frontend/app/features/settings/pages/settings/settings-page.component.ts

@ -6,7 +6,6 @@
*/
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AppSettingsDto, AppsState, EditAppSettingsForm, ResourceOwner } from '@app/shared';
@Component({
@ -17,12 +16,11 @@ import { AppSettingsDto, AppsState, EditAppSettingsForm, ResourceOwner } from '@
export class SettingsPageComponent extends ResourceOwner implements OnInit {
public isEditable = false;
public editForm = new EditAppSettingsForm(this.formBuilder);
public editForm = new EditAppSettingsForm();
public editingSettings: AppSettingsDto;
constructor(
private readonly appsState: AppsState,
private readonly formBuilder: FormBuilder,
) {
super();
}

4
frontend/app/features/settings/pages/workflows/workflow-add-form.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AddWorkflowForm, WorkflowsState } from '@app/shared';
@Component({
@ -16,11 +15,10 @@ import { AddWorkflowForm, WorkflowsState } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorkflowAddFormComponent {
public addWorkflowForm = new AddWorkflowForm(this.formBuilder);
public addWorkflowForm = new AddWorkflowForm();
constructor(
private readonly workflowsState: WorkflowsState,
private readonly formBuilder: FormBuilder,
) {
}

4
frontend/app/framework/angular/forms/editable-title.component.html

@ -1,11 +1,11 @@
<div class="title">
<form *ngIf="renaming; else noRenaming" [formGroup]="renameForm" (ngSubmit)="rename()">
<form *ngIf="renaming; else noRenaming" (ngSubmit)="rename()">
<div class="row g-0">
<div class="col">
<div class="form-group me-2">
<sqx-control-errors for="name"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" [maxLength]="maxLength" sqxFocusOnInit (keydown)="onKeyDown($event)" spellcheck="false">
<input type="text" class="form-control" [formControl]="renameForm" [maxLength]="maxLength" sqxFocusOnInit (keydown)="onKeyDown($event)" spellcheck="false">
</div>
</div>
<div class="col-auto">

25
frontend/app/framework/angular/forms/editable-title.component.ts

@ -6,7 +6,7 @@
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { FormControl, Validators } from '@angular/forms';
import { Keys } from '@app/framework/internal';
@Component({
@ -37,22 +37,11 @@ export class EditableTitleComponent {
Validators.required :
Validators.nullValidator;
this.renameForm.controls['name'].setValidators(validator);
this.renameForm.setValidators(validator);
}
public renaming = false;
public renameForm = this.formBuilder.group({
name: ['',
[
Validators.required,
],
],
});
constructor(
private readonly formBuilder: FormBuilder,
) {
}
public renameForm = new FormControl();
public onKeyDown(event: KeyboardEvent) {
if (Keys.isEscape(event)) {
@ -65,7 +54,7 @@ export class EditableTitleComponent {
return;
}
this.renameForm.setValue({ name: this.name || '' });
this.renameForm.setValue(this.name || '');
this.renaming = !this.renaming;
}
@ -75,10 +64,10 @@ export class EditableTitleComponent {
}
if (this.renameForm.valid) {
const value = this.renameForm.value;
const name = this.renameForm.value;
this.nameChange.emit(value.name);
this.name = value.name;
this.nameChange.emit(name);
this.name = name;
this.renaming = false;
}

128
frontend/app/framework/angular/forms/extended-form-array.spec.ts

@ -0,0 +1,128 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormArray, FormControl } from '@angular/forms';
import { ExtendedFormArray, UndefinableFormArray } from './extended-form-array';
describe('ExtendedFormArray', () => {
it('should provide value even if controls are disabled', () => {
const control = new ExtendedFormArray([
new FormControl('1'),
new FormControl('2'),
]);
expect(control.value).toEqual(['1', '2']);
assertValue(control, ['1', '2'], () => {
control.controls[0].disable();
});
});
});
describe('UndefinableFormArray', () => {
const tests = [{
name: 'undefined (on)',
undefinable: true,
valueExpected: undefined,
valueActual: undefined,
}, {
name: 'defined (on)',
undefinable: true,
valueExpected: [1],
valueActual: [1],
}, {
name: 'defined (off)',
undefinable: false,
valueExpected: [1],
valueActual: [1],
}];
it('should provide value even if controls are disabled', () => {
const control = new UndefinableFormArray([
new FormControl('1'),
new FormControl('2'),
]);
expect(control.value).toEqual(['1', '2']);
assertValue(control, ['1', '2'], () => {
control.controls[0].disable();
});
});
tests.forEach(x => {
it(`should set value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.setValue(x.valueActual as any);
});
});
});
tests.forEach(x => {
it(`should patch value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.patchValue(x.valueActual as any);
});
});
});
tests.forEach(x => {
it(`should reset value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.reset(x.valueActual as any);
});
});
});
it('should reset value back after push', () => {
const control = new UndefinableFormArray([]);
assertValue(control, ['1'], () => {
control.setValue(undefined);
control.push(new FormControl('1'));
});
});
it('should reset value back after insert', () => {
const control = new UndefinableFormArray([]);
assertValue(control, ['1'], () => {
control.setValue(undefined);
control.insert(0, new FormControl('1'));
});
});
function buildControl(undefinable: boolean) {
return undefinable ?
new UndefinableFormArray([
new FormControl(''),
]) :
new ExtendedFormArray([
new FormControl(''),
]);
}
});
function assertValue(control: FormArray, expected: any, action: () => void) {
let currentValue: any;
control.valueChanges.subscribe(value => {
currentValue = value;
});
action();
expect(currentValue).toEqual(expected);
expect(control.getRawValue()).toEqual(expected);
expect(control.value).toEqual(expected);
}

38
frontend/app/framework/angular/forms/undefinable-form-array.ts → frontend/app/framework/angular/forms/extended-form-array.ts

@ -5,11 +5,24 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { EventEmitter } from '@angular/core';
import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, FormArray, ValidatorFn } from '@angular/forms';
import { Types } from '@app/framework/internal';
export class UndefinableFormArray extends FormArray {
export class ExtendedFormArray extends FormArray {
constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(controls, validatorOrOpts, asyncValidator);
this['_reduceValue'] = () => {
return this.controls.map(x => x.value);
};
this['_updateValue'] = () => {
(this as { value: any }).value = this['_reduceValue']();
};
}
}
export class UndefinableFormArray extends ExtendedFormArray {
private isUndefined = false;
constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
@ -79,25 +92,4 @@ export class UndefinableFormArray extends FormArray {
this.clear({ emitEvent: false });
}
}
public updateValueAndValidity(opts: { onlySelf?: boolean; emitEvent?: boolean } = {}) {
super.updateValueAndValidity({ emitEvent: false, onlySelf: true });
if (this.isUndefined) {
this.unsetValue();
}
if (opts.emitEvent !== false) {
(this.valueChanges as EventEmitter<any>).emit(this.value);
(this.statusChanges as EventEmitter<string>).emit(this.status);
}
if (this.parent && !opts.onlySelf) {
this.parent.updateValueAndValidity(opts);
}
}
private unsetValue() {
(this as { value: any }).value = undefined;
}
}

110
frontend/app/framework/angular/forms/extended-form-group.spec.ts

@ -0,0 +1,110 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormControl, FormGroup } from '@angular/forms';
import { ExtendedFormGroup, UndefinableFormGroup } from './extended-form-group';
describe('UndefinableFormGroup', () => {
it('should provide value even if controls are disabled', () => {
const control = new ExtendedFormGroup({
test1: new FormControl('1'),
test2: new FormControl('2'),
});
expect(control.value).toEqual({ test1: '1', test2: '2' });
assertValue(control, { test1: '1', test2: '2' }, () => {
control.controls['test1'].disable();
});
});
});
describe('ExtendedFormGroup', () => {
const tests = [{
name: 'undefined (on)',
undefinable: true,
valueExpected: undefined,
valueActual: undefined,
}, {
name: 'defined (on)',
undefinable: true,
valueExpected: { field: 1 },
valueActual: { field: 1 },
}, {
name: 'defined (off)',
undefinable: false,
valueExpected: { field: 1 },
valueActual: { field: 1 },
}];
it('should provide value even if controls are disabled', () => {
const control = new ExtendedFormGroup({
test1: new FormControl('1'),
test2: new FormControl('2'),
});
expect(control.value).toEqual({ test1: '1', test2: '2' });
assertValue(control, { test1: '1', test2: '2' }, () => {
control.controls['test1'].disable();
});
});
tests.forEach(x => {
it(`should set value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.setValue(x.valueActual as any);
});
});
});
tests.forEach(x => {
it(`should patch value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.patchValue(x.valueActual as any);
});
});
});
tests.forEach(x => {
it(`should reset value as <${x.name}>`, () => {
const control = buildControl(x.undefinable);
assertValue(control, x.valueExpected, () => {
control.reset(x.valueActual);
});
});
});
function buildControl(undefinable: boolean) {
return undefinable ?
new UndefinableFormGroup({
field: new FormControl(),
}) :
new ExtendedFormGroup({
field: new FormControl(),
});
}
});
function assertValue(control: FormGroup, expected: any, action: () => void) {
let currentValue: any;
control.valueChanges.subscribe(value => {
currentValue = value;
});
action();
expect(currentValue).toEqual(expected);
expect(control.getRawValue()).toEqual(expected);
expect(control.value).toEqual(expected);
}

44
frontend/app/framework/angular/forms/undefinable-form-group.ts → frontend/app/framework/angular/forms/extended-form-group.ts

@ -5,11 +5,30 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { EventEmitter } from '@angular/core';
import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms';
import { Types } from '@app/framework/internal';
export class UndefinableFormGroup extends FormGroup {
export class ExtendedFormGroup extends FormGroup {
constructor(controls: { [key: string]: AbstractControl }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(controls, validatorOrOpts, asyncValidator);
this['_reduceValue'] = () => {
const result = {};
for (const [key, control] of Object.entries(this.controls)) {
result[key] = control.value;
}
return result;
};
this['_updateValue'] = () => {
(this as { value: any }).value = this['_reduceValue']();
};
}
}
export class UndefinableFormGroup extends ExtendedFormGroup {
private isUndefined = false;
constructor(controls: { [key: string]: AbstractControl }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
@ -63,25 +82,4 @@ export class UndefinableFormGroup extends FormGroup {
private checkUndefined(value?: {}) {
this.isUndefined = Types.isUndefined(value);
}
public updateValueAndValidity(opts: { onlySelf?: boolean; emitEvent?: boolean } = {}) {
super.updateValueAndValidity({ emitEvent: false, onlySelf: true });
if (this.isUndefined) {
this.unsetValue();
}
if (opts.emitEvent !== false) {
(this.valueChanges as EventEmitter<any>).emit(this.value);
(this.statusChanges as EventEmitter<string>).emit(this.status);
}
if (this.parent && !opts.onlySelf) {
this.parent.updateValueAndValidity(opts);
}
}
private unsetValue() {
(this as { value: any }).value = undefined;
}
}

12
frontend/app/framework/angular/forms/forms-helper.ts

@ -96,7 +96,7 @@ export function invalid$(form: AbstractControl): Observable<boolean> {
}
export function value$<T = any>(form: AbstractControl): Observable<T> {
return form.valueChanges.pipe(map(() => getRawValue(form)), startWith(getRawValue(form)), distinctUntilChanged());
return form.valueChanges.pipe(startWith(form.value), distinctUntilChanged());
}
export function valueProjection$<T = any>(form: AbstractControl, projection: (value: any) => T): Observable<T> {
@ -159,16 +159,6 @@ function isValid(value: any) {
return !Types.isNull(value) && !Types.isUndefined(value);
}
export function getRawValue(form: AbstractControl): any {
if (Types.is(form, FormGroup)) {
return form.getRawValue();
} else if (Types.is(form, FormArray)) {
return form.getRawValue();
} else {
return form.value;
}
}
export function hasNonCustomError(form: AbstractControl) {
if (form.errors) {
for (const key in form.errors) {

4
frontend/app/framework/angular/forms/model.ts

@ -9,7 +9,7 @@ import { AbstractControl, ValidatorFn } from '@angular/forms';
import { ErrorDto, Types } from '@app/framework/internal';
import { State } from './../../state';
import { ErrorValidator } from './error-validator';
import { addValidator, getRawValue, hasNonCustomError, updateAll } from './forms-helper';
import { addValidator, hasNonCustomError, updateAll } from './forms-helper';
export interface FormState {
// The number of submits.
@ -92,7 +92,7 @@ export class Form<T extends AbstractControl, TOut, TIn = TOut> {
this.form.markAllAsTouched();
if (!hasNonCustomError(this.form)) {
const value = this.transformSubmit(getRawValue(this.form));
const value = this.transformSubmit(this.form.value);
if (value) {
this.disable();

2
frontend/app/framework/angular/forms/templated-form-array.ts

@ -7,7 +7,7 @@
import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, ValidatorFn } from '@angular/forms';
import { Types } from '@app/framework/internal';
import { UndefinableFormArray } from './undefinable-form-array';
import { UndefinableFormArray } from './extended-form-array';
export interface FormArrayTemplate {
createControl(value: any, initialValue?: any): AbstractControl;

2
frontend/app/framework/angular/forms/templated-form-group.ts

@ -7,7 +7,7 @@
import { AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms';
import { Types } from '@app/framework/internal';
import { UndefinableFormGroup } from './undefinable-form-group';
import { UndefinableFormGroup } from './extended-form-group';
export interface FormGroupTemplate {
setControls(form: FormGroup, value: any): void;

89
frontend/app/framework/angular/forms/undefinable-form-array.spec.ts

@ -1,89 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormArray, FormControl } from '@angular/forms';
import { UndefinableFormArray } from './undefinable-form-array';
describe('UndefinableFormArray', () => {
const tests = [{
name: 'undefined',
value: undefined,
}, {
name: 'defined',
value: ['1'],
}];
tests.forEach(x => {
it(`should set value as <${x.name}>`, () => {
const control =
new UndefinableFormArray([
new FormControl(''),
]);
assertValue(control, x.value, () => {
control.setValue(x.value);
});
});
});
tests.forEach(x => {
it(`should patch value as <${x.name}>`, () => {
const control =
new UndefinableFormArray([
new FormControl(''),
]);
assertValue(control, x.value, () => {
control.patchValue(x.value);
});
});
});
tests.forEach(x => {
it(`should reset value as <${x.name}>`, () => {
const control =
new UndefinableFormArray([
new FormControl(''),
]);
assertValue(control, x.value, () => {
control.reset(x.value);
});
});
});
it('should reset value back after push', () => {
const control = new UndefinableFormArray([]);
assertValue(control, ['1'], () => {
control.setValue(undefined);
control.push(new FormControl('1'));
});
});
it('should reset value back after insert', () => {
const control = new UndefinableFormArray([]);
assertValue(control, ['1'], () => {
control.setValue(undefined);
control.insert(0, new FormControl('1'));
});
});
function assertValue(control: FormArray, expected: any, action: () => void) {
let currentValue: any;
control.valueChanges.subscribe(value => {
currentValue = value;
});
action();
expect(currentValue).toEqual(expected);
expect(control.getRawValue()).toEqual(expected);
}
});

71
frontend/app/framework/angular/forms/undefinable-form-group.spec.ts

@ -1,71 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormControl, FormGroup } from '@angular/forms';
import { UndefinableFormGroup } from './undefinable-form-group';
describe('UndefinableFormGroup', () => {
const tests = [{
name: 'undefined',
value: undefined,
}, {
name: 'defined',
value: { field: ['1'] },
}];
tests.forEach(x => {
it(`should set value as <${x.name}>`, () => {
const control =
new UndefinableFormGroup({
field: new FormControl(),
});
assertValue(control, x.value, () => {
control.setValue(x.value);
});
});
});
tests.forEach(x => {
it(`should patch value as <${x.name}>`, () => {
const control =
new UndefinableFormGroup({
field: new FormControl(),
});
assertValue(control, x.value, () => {
control.patchValue(x.value);
});
});
});
tests.forEach(x => {
it(`should reset value as <${x.name}>`, () => {
const control =
new UndefinableFormGroup({
field: new FormControl(),
});
assertValue(control, x.value, () => {
control.reset(x.value);
});
});
});
function assertValue(control: FormGroup, expected: any, action: () => void) {
let currentValue: any;
control.valueChanges.subscribe(value => {
currentValue = value;
});
action();
expect(currentValue).toEqual(expected);
expect(control.getRawValue()).toEqual(expected);
}
});

8
frontend/app/framework/declarations.ts

@ -22,6 +22,8 @@ export * from './angular/forms/editors/localized-input.component';
export * from './angular/forms/editors/stars.component';
export * from './angular/forms/editors/tag-editor.component';
export * from './angular/forms/editors/toggle.component';
export * from './angular/forms/extended-form-array';
export * from './angular/forms/extended-form-group';
export * from './angular/forms/file-drop.directive';
export * from './angular/forms/focus-on-init.directive';
export * from './angular/forms/form-alert.component';
@ -33,17 +35,15 @@ export * from './angular/forms/model';
export * from './angular/forms/progress-bar.component';
export * from './angular/forms/templated-form-array';
export * from './angular/forms/transform-input.directive';
export * from './angular/forms/undefinable-form-array';
export * from './angular/forms/undefinable-form-group';
export * from './angular/forms/validators';
export * from './angular/hover-background.directive';
export * from './angular/http/caching.interceptor';
export * from './angular/http/http-extensions';
export * from './angular/http/loading.interceptor';
export * from './angular/image-source.directive';
export * from './angular/layout.component';
export * from './angular/layout-container.directive';
export * from './angular/language-selector.component';
export * from './angular/layout-container.directive';
export * from './angular/layout.component';
export * from './angular/list-view.component';
export * from './angular/modals/dialog-renderer.component';
export * from './angular/modals/modal-dialog.component';

4
frontend/app/shared/components/app-form.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ApiUrlConfig, AppsState, CreateAppForm } from '@app/shared/internal';
@Component({
@ -22,12 +21,11 @@ export class AppFormComponent {
@Input()
public template = '';
public createForm = new CreateAppForm(this.formBuilder);
public createForm = new CreateAppForm();
constructor(
public readonly apiUrl: ApiUrlConfig,
private readonly appsStore: AppsState,
private readonly formBuilder: FormBuilder,
) {
}

4
frontend/app/shared/components/assets/asset-dialog.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, ViewChildren } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AnnotateAssetDto, AnnotateAssetForm, AppsState, AssetDto, AssetsState, AssetUploaderState, AuthService, DialogService, Types, UploadCanceled } from '@app/shared/internal';
import { AssetsService } from '@app/shared/services/assets.service';
import { AssetPathItem, ROOT_ITEM } from '@app/shared/state/assets.state';
@ -53,7 +52,7 @@ export class AssetDialogComponent implements OnChanges {
public selectedTab = 0;
public annotateForm = new AnnotateAssetForm(this.formBuilder);
public annotateForm = new AnnotateAssetForm();
public get isImage() {
return this.asset.type === 'Image';
@ -74,7 +73,6 @@ export class AssetDialogComponent implements OnChanges {
private readonly assetsService: AssetsService,
private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder,
public readonly authService: AuthService,
) {
}

4
frontend/app/shared/components/assets/asset-folder-dialog.component.ts

@ -6,7 +6,6 @@
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { AssetFolderDto, AssetsState, RenameAssetFolderForm } from '@app/shared/internal';
@Component({
@ -21,11 +20,10 @@ export class AssetFolderDialogComponent implements OnInit {
@Input()
public assetFolder: AssetFolderDto;
public editForm = new RenameAssetFolderForm(this.formBuilder);
public editForm = new RenameAssetFolderForm();
constructor(
private readonly assetsState: AssetsState,
private readonly formBuilder: FormBuilder,
) {
}

4
frontend/app/shared/components/comments/comments.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectorRef, Component, ElementRef, Input, OnChanges, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { switchSafe } from '@app/framework';
import { AppsState, AuthService, CommentDto, CommentsService, CommentsState, ContributorsState, DialogService, ResourceOwner, UpsertCommentForm } from '@app/shared/internal';
@ -31,7 +30,7 @@ export class CommentsComponent extends ResourceOwner implements OnChanges {
public commentsUrl: string;
public commentsState: CommentsState;
public commentForm = new UpsertCommentForm(this.formBuilder);
public commentForm = new UpsertCommentForm();
public mentionUsers = this.contributorsState.contributors;
public mentionConfig: MentionConfig = { dropUp: true, labelKey: 'contributorEmail' };
@ -44,7 +43,6 @@ export class CommentsComponent extends ResourceOwner implements OnChanges {
private readonly contributorsState: ContributorsState,
private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder,
private readonly router: Router,
) {
super();

21
frontend/app/shared/components/forms/geolocation-editor.component.ts

@ -6,8 +6,8 @@
*/
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core';
import { FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { LocalStoreService, ResourceLoaderService, Settings, StatefulControlComponent, Types, UIOptions, ValidatorsEx } from '@app/shared/internal';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { LocalStoreService, ResourceLoaderService, Settings, StatefulControlComponent, Types, UIOptions, ExtendedFormGroup, ValidatorsEx } from '@app/shared/internal';
declare const L: any;
declare const google: any;
@ -54,19 +54,13 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
}
public geolocationForm =
this.formBuilder.group({
latitude: [
'',
[
new ExtendedFormGroup({
latitude: new FormControl('',
ValidatorsEx.between(-90, 90),
],
],
longitude: [
'',
[
),
longitude: new FormControl('',
ValidatorsEx.between(-180, 180),
],
],
),
});
@ViewChild('editor', { static: false })
@ -78,7 +72,6 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
constructor(changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService,
private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder,
private readonly uiOptions: UIOptions,
) {
super(changeDetector, {

8
frontend/app/shared/components/search/search-form.component.ts

@ -6,7 +6,6 @@
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { DialogModel, equalsQuery, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm, Types } from '@app/shared/internal';
import { Observable } from 'rxjs';
@ -53,17 +52,12 @@ export class SearchFormComponent implements OnChanges {
public saveKey: Observable<string | undefined>;
public saveQueryDialog = new DialogModel();
public saveQueryForm = new SaveQueryForm(this.formBuilder);
public saveQueryForm = new SaveQueryForm();
public searchDialog = new DialogModel(false);
public hasFilter: boolean;
constructor(
private readonly formBuilder: FormBuilder,
) {
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['query'] || changes['queries']) {
this.updateSaveKey();

98
frontend/app/shared/state/apps.forms.ts

@ -7,99 +7,99 @@
/* eslint-disable no-useless-escape */
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, TemplatedFormArray, ValidatorsEx } from '@app/framework';
import { FormControl, Validators } from '@angular/forms';
import { Form, TemplatedFormArray, ExtendedFormGroup, ValidatorsEx } from '@app/framework';
import { AppDto, AppSettingsDto, CreateAppDto, UpdateAppDto, UpdateAppSettingsDto } from './../services/apps.service';
export class CreateAppForm extends Form<FormGroup, CreateAppDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
export class CreateAppForm extends Form<ExtendedFormGroup, CreateAppDto> {
constructor() {
super(new ExtendedFormGroup({
name: new FormControl('', [
Validators.required,
Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:apps.appNameValidationMessage'),
],
],
]),
}));
}
}
export class UpdateAppForm extends Form<FormGroup, UpdateAppDto, AppDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
label: ['',
[
export class UpdateAppForm extends Form<ExtendedFormGroup, UpdateAppDto, AppDto> {
constructor() {
super(new ExtendedFormGroup({
label: new FormControl('',
Validators.maxLength(40),
],
],
description: '',
),
description: new FormControl('',
Validators.nullValidator,
),
}));
}
}
export class EditAppSettingsForm extends Form<FormGroup, UpdateAppSettingsDto, AppSettingsDto> {
export class EditAppSettingsForm extends Form<ExtendedFormGroup, UpdateAppSettingsDto, AppSettingsDto> {
public get patterns() {
return this.form.controls['patterns']! as TemplatedFormArray;
return this.form.controls['patterns'] as TemplatedFormArray;
}
public get patternsControls(): ReadonlyArray<FormGroup> {
public get patternsControls(): ReadonlyArray<ExtendedFormGroup> {
return this.patterns.controls as any;
}
public get editors() {
return this.form.controls['editors']! as TemplatedFormArray;
return this.form.controls['editors'] as TemplatedFormArray;
}
public get editorsControls(): ReadonlyArray<FormGroup> {
public get editorsControls(): ReadonlyArray<ExtendedFormGroup> {
return this.editors.controls as any;
}
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
patterns: new TemplatedFormArray(new PatternTemplate(formBuilder)),
hideScheduler: false,
hideDateTimeButtons: false,
editors: new TemplatedFormArray(new EditorTemplate(formBuilder)),
constructor() {
super(new ExtendedFormGroup({
patterns: new TemplatedFormArray(
PatternTemplate.INSTANCE,
),
hideScheduler: new FormControl(false,
Validators.nullValidator,
),
hideDateTimeButtons: new FormControl(false,
Validators.nullValidator,
),
editors: new TemplatedFormArray(
EditorTemplate.INSTANCE,
),
}));
}
}
class PatternTemplate {
constructor(private readonly formBuilder: FormBuilder) {}
public static readonly INSTANCE = new PatternTemplate();
public createControl() {
return this.formBuilder.group({
name: ['',
[
return new FormControl({
name: new FormControl('',
Validators.required,
],
],
regex: ['',
[
),
regex: new FormControl('',
Validators.required,
],
],
message: '',
),
message: new FormControl('',
Validators.nullValidator,
),
});
}
}
class EditorTemplate {
constructor(private readonly formBuilder: FormBuilder) {}
public static readonly INSTANCE = new EditorTemplate();
public createControl() {
return this.formBuilder.group({
name: ['',
[
return new FormControl({
name: new FormControl('',
Validators.required,
],
],
url: ['',
[
),
url: new FormControl('',
Validators.required,
],
],
),
});
}
}

3
frontend/app/shared/state/assets.forms.spec.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder } from '@angular/forms';
import { AnnotateAssetForm } from './assets.forms';
describe('AnnotateAssetForm', () => {
@ -28,7 +27,7 @@ describe('AnnotateAssetForm', () => {
};
beforeEach(() => {
form = new AnnotateAssetForm(new FormBuilder());
form = new AnnotateAssetForm();
});
it('shoulde remove extension if loading asset file name', () => {

110
frontend/app/shared/state/assets.forms.ts

@ -5,43 +5,37 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, Mutable, TemplatedFormArray, Types } from '@app/framework';
import { FormControl, Validators } from '@angular/forms';
import { Form, Mutable, TemplatedFormArray, Types, ExtendedFormGroup } from '@app/framework';
import slugify from 'slugify';
import { AnnotateAssetDto, AssetDto, AssetFolderDto, RenameAssetFolderDto, RenameAssetTagDto } from './../services/assets.service';
export class AnnotateAssetForm extends Form<FormGroup, AnnotateAssetDto, AssetDto> {
export class AnnotateAssetForm extends Form<ExtendedFormGroup, AnnotateAssetDto, AssetDto> {
public get metadata() {
return this.form.get('metadata')! as TemplatedFormArray;
return this.form.controls['metadata'] as TemplatedFormArray;
}
public get metadataControls(): ReadonlyArray<FormGroup> {
public get metadataControls(): ReadonlyArray<ExtendedFormGroup> {
return this.metadata.controls as any;
}
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
isProtected: [false,
[
constructor() {
super(new ExtendedFormGroup({
isProtected: new FormControl(false,
Validators.nullValidator,
],
],
fileName: ['',
[
),
fileName: new FormControl('',
Validators.required,
],
],
slug: ['',
[
),
slug: new FormControl('',
Validators.required,
],
],
tags: [[],
[
),
tags: new FormControl([],
Validators.nullValidator,
],
],
metadata: new TemplatedFormArray(new MetadataTemplate(formBuilder)),
),
metadata: new TemplatedFormArray(
MetadataTemplate.INSTANCE,
),
}));
}
@ -149,7 +143,7 @@ export class AnnotateAssetForm extends Form<FormGroup, AnnotateAssetDto, AssetDt
}
public generateSlug(asset: AssetDto) {
const fileName = this.form.get('fileName')!.value;
const fileName = this.form.controls['fileName'].value;
if (fileName) {
let slug = slugify(fileName, { lower: true });
@ -162,58 +156,64 @@ export class AnnotateAssetForm extends Form<FormGroup, AnnotateAssetDto, AssetDt
}
}
this.form.get('slug')!.setValue(slug);
this.form.controls['slug'].setValue(slug);
}
}
}
class MetadataTemplate {
constructor(private readonly formBuilder: FormBuilder) {}
public static readonly INSTANCE = new MetadataTemplate();
public createControl() {
return this.formBuilder.group({
name: ['',
[
return new ExtendedFormGroup({
name: new FormControl('',
Validators.required,
],
],
value: [''],
),
value: new FormControl('',
Validators.nullValidator,
),
});
}
}
export class EditAssetScriptsForm extends Form<FormGroup, {}, object> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
annotate: '',
create: '',
delete: '',
move: '',
update: '',
export class EditAssetScriptsForm extends Form<ExtendedFormGroup, {}, object> {
constructor() {
super(new ExtendedFormGroup({
annotate: new FormControl('',
Validators.nullValidator,
),
create: new FormControl('',
Validators.nullValidator,
),
delete: new FormControl('',
Validators.nullValidator,
),
move: new FormControl('',
Validators.nullValidator,
),
update: new FormControl('',
Validators.nullValidator,
),
}));
}
}
export class RenameAssetFolderForm extends Form<FormGroup, RenameAssetFolderDto, AssetFolderDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
folderName: ['',
[
export class RenameAssetFolderForm extends Form<ExtendedFormGroup, RenameAssetFolderDto, AssetFolderDto> {
constructor() {
super(new ExtendedFormGroup({
folderName: new FormControl('',
Validators.required,
],
],
),
}));
}
}
export class RenameAssetTagForm extends Form<FormGroup, RenameAssetTagDto, RenameAssetTagDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
tagName: ['',
[
export class RenameAssetTagForm extends Form<ExtendedFormGroup, RenameAssetTagDto, RenameAssetTagDto> {
constructor() {
super(new ExtendedFormGroup({
tagName: new FormControl('',
Validators.required,
],
],
),
}));
}
}

32
frontend/app/shared/state/backups.forms.ts

@ -7,26 +7,28 @@
/* eslint-disable no-useless-escape */
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, hasNoValue$, ValidatorsEx } from '@app/framework';
import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$, ExtendedFormGroup, ValidatorsEx } from '@app/framework';
import { StartRestoreDto } from './../services/backups.service';
export class RestoreForm extends Form<FormGroup, StartRestoreDto> {
public hasNoUrl = hasNoValue$(this.form.controls['url']);
export class RestoreForm extends Form<ExtendedFormGroup, StartRestoreDto> {
public get url() {
return this.form.controls['url'];
}
public hasNoUrl = hasNoValue$(this.url);
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
constructor() {
super(
new ExtendedFormGroup({
name: new FormControl('', [
Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:apps.appNameValidationMessage'),
],
],
url: ['',
[
]),
url: new FormControl('',
Validators.required,
],
],
}));
),
}),
);
}
}

34
frontend/app/shared/state/clients.forms.ts

@ -7,33 +7,33 @@
/* eslint-disable no-useless-escape */
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, hasNoValue$, ValidatorsEx } from '@app/framework';
import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$, ExtendedFormGroup, ValidatorsEx } from '@app/framework';
import { ClientDto, CreateClientDto, UpdateClientDto } from './../services/clients.service';
export class RenameClientForm extends Form<FormGroup, UpdateClientDto, ClientDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
export class RenameClientForm extends Form<ExtendedFormGroup, UpdateClientDto, ClientDto> {
constructor() {
super(new ExtendedFormGroup({
name: new FormControl('',
Validators.required,
],
],
),
}));
}
}
export class AddClientForm extends Form<FormGroup, CreateClientDto> {
public hasNoId = hasNoValue$(this.form.controls['id']);
export class AddClientForm extends Form<ExtendedFormGroup, CreateClientDto> {
public get id() {
return this.form.controls['id'];
}
public hasNoId = hasNoValue$(this.id);
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
id: ['',
[
constructor() {
super(new ExtendedFormGroup({
id: new FormControl('', [
Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:clients.clientIdValidationMessage'),
],
],
]),
}));
}
}

14
frontend/app/shared/state/comments.form.ts

@ -5,14 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormGroup } from '@angular/forms';
import { Form } from '@app/framework';
import { FormControl, Validators } from '@angular/forms';
import { Form, ExtendedFormGroup } from '@app/framework';
import { UpsertCommentDto } from './../services/comments.service';
export class UpsertCommentForm extends Form<FormGroup, UpsertCommentDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
text: '',
export class UpsertCommentForm extends Form<ExtendedFormGroup, UpsertCommentDto> {
constructor() {
super(new ExtendedFormGroup({
text: new FormControl('',
Validators.nullValidator,
),
}));
}
}

11
frontend/app/shared/state/contents.forms-helpers.ts

@ -9,7 +9,6 @@
/* eslint-disable no-useless-return */
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { getRawValue } from '@app/framework';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AppLanguageDto } from './../services/app-languages.service';
@ -149,7 +148,7 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
return `${this.fieldPath}.${relative}`;
}
public updateState(context: RuleContext, fieldData: any, itemData: any, parentState: AbstractContentFormState) {
public updateState(context: RuleContext, itemData: any, parentState: AbstractContentFormState) {
const state = {
isDisabled: this.field.isDisabled || parentState.isDisabled === true,
isHidden: parentState.isHidden === true,
@ -178,11 +177,7 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
}
}
this.updateCustomState(context, fieldData, itemData, state);
}
public getRawValue() {
return getRawValue(this.form);
this.updateCustomState(context, itemData, state);
}
public setValue(value: any) {
@ -193,7 +188,7 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
this.form.setValue(undefined);
}
protected updateCustomState(_context: RuleContext, _fieldData: any, _itemData: any, _state: AbstractContentFormState): void {
protected updateCustomState(_context: RuleContext, _itemData: any, _state: AbstractContentFormState): void {
return;
}
}

126
frontend/app/shared/state/contents.forms.spec.ts

@ -499,10 +499,132 @@ describe('ContentForm', () => {
},
});
// Should hide fields.
expect(array.get(0)!.get('field1')!.hidden).toBeTruthy();
expect(array.get(1)!.get('field1')!.hidden).toBeFalsy();
});
it('should replace component with new fields', () => {
const component1Id = MathHelper.guid();
const component1 = createSchema({
id: 1,
fields: [
createField({
id: 11,
properties: createProperties('String'),
partitioning: 'invariant',
}),
],
});
const component2Id = MathHelper.guid();
const component2 = createSchema({
id: 2,
fields: [
createField({
id: 21,
properties: createProperties('String'),
partitioning: 'invariant',
}),
],
});
const contentForm = createForm([
createField({
id: 4,
properties: createProperties('Component'),
partitioning: 'invariant',
}),
], [], {
[component1Id]: component1,
[component2Id]: component2,
});
contentForm.load({});
// Should be undefined by default.
expect(contentForm.value).toEqual({
field4: {
iv: undefined,
},
});
contentForm.load({
field4: {
iv: {
schemaId: component1Id,
},
},
});
// Should add field from component1.
expect(contentForm.value).toEqual({
field4: {
iv: {
schemaId: component1Id,
field11: null,
},
},
});
contentForm.load({
field4: {
iv: {
schemaId: component2Id,
},
},
});
// Should add field from component1.
expect(contentForm.value).toEqual({
field4: {
iv: {
schemaId: component2Id,
field21: null,
},
},
});
});
it('should ignore invalid schema ids', () => {
const componentId = MathHelper.guid();
const component = createSchema({
id: 1,
fields: [
createField({
id: 11,
properties: createProperties('String'),
partitioning: 'invariant',
}),
],
});
const contentForm = createForm([
createField({
id: 4,
properties: createProperties('Component'),
partitioning: 'invariant',
}),
], [], {
[componentId]: component,
});
contentForm.load({
field4: {
iv: {
schemaId: 'invalid',
},
},
});
// Should ignore invalid id.
expect(contentForm.value).toEqual({
field4: {
iv: {},
},
});
});
it('should load with array and not enable disabled nested fields', () => {
const { contentForm, array } = createArrayFormWith2Items();
@ -539,7 +661,7 @@ describe('ContentForm', () => {
array.sort([array.get(1), array.get(0)]);
expectLength(array, 2);
expect(array.form.value).toEqual([{ nested41: 'Text2' }, { nested41: 'Text1' }]);
expect(array.form.value).toEqual([{ nested41: 'Text2', nested42: null }, { nested41: 'Text1', nested42: null }]);
});
it('should remove array item', () => {
@ -548,7 +670,7 @@ describe('ContentForm', () => {
array.removeItemAt(0);
expectLength(array, 1);
expect(array.form.value).toEqual([{ nested41: 'Text2' }]);
expect(array.form.value).toEqual([{ nested41: 'Text2', nested42: null }]);
});
it('should reset array item', () => {

106
frontend/app/shared/state/contents.forms.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { debounceTimeSafe, Form, FormArrayTemplate, getRawValue, TemplatedFormArray, Types, value$ } from '@app/framework';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { debounceTimeSafe, Form, FormArrayTemplate, TemplatedFormArray, Types, ExtendedFormGroup, value$ } from '@app/framework';
import { FormGroupTemplate, TemplatedFormGroup } from '@app/framework/angular/forms/templated-form-group';
import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs';
import { AppLanguageDto } from './../services/app-languages.service';
@ -19,27 +19,27 @@ import { FieldDefaultValue, FieldsValidators } from './contents.forms.visitors';
type SaveQueryFormType = { name: string; user: boolean };
export class SaveQueryForm extends Form<FormGroup, SaveQueryFormType> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
export class SaveQueryForm extends Form<ExtendedFormGroup, SaveQueryFormType> {
constructor() {
super(new ExtendedFormGroup({
name: new FormControl('',
Validators.required,
],
],
user: false,
),
user: new FormControl(false,
Validators.nullValidator,
),
}));
}
}
export class PatchContentForm extends Form<FormGroup, any> {
export class PatchContentForm extends Form<ExtendedFormGroup, any> {
private readonly editableFields: ReadonlyArray<RootFieldDto>;
constructor(
private readonly listFields: ReadonlyArray<TableField>,
private readonly language: AppLanguageDto,
) {
super(new FormGroup({}));
super(new ExtendedFormGroup({}));
this.editableFields = this.listFields.filter(x => Types.is(x, RootFieldDto) && x.isInlineEditable) as any;
@ -73,7 +73,7 @@ export class PatchContentForm extends Form<FormGroup, any> {
}
}
export class EditContentForm extends Form<FormGroup, any> {
export class EditContentForm extends Form<ExtendedFormGroup, any> {
private readonly fields: { [name: string]: FieldForm } = {};
private readonly valueChange$ = new BehaviorSubject<any>(this.form.value);
private initialData: any;
@ -94,7 +94,7 @@ export class EditContentForm extends Form<FormGroup, any> {
public context: any,
debounce = 100,
) {
super(new FormGroup({}));
super(new ExtendedFormGroup({}));
const globals: FormGlobals = {
schema,
@ -185,7 +185,7 @@ export class EditContentForm extends Form<FormGroup, any> {
const context = { ...this.context || {}, data };
for (const field of Object.values(this.fields)) {
field.updateState(context, data[field.field.name], data, { isDisabled: this.form.disabled });
field.updateState(context, data, { isDisabled: this.form.disabled });
}
for (const section of this.sections) {
@ -194,7 +194,7 @@ export class EditContentForm extends Form<FormGroup, any> {
}
private updateInitialData() {
this.initialData = this.form.getRawValue();
this.initialData = this.form.value;
}
}
@ -236,7 +236,7 @@ export class FieldForm extends AbstractContentForm<RootFieldDto, FormGroup> {
}
}
protected updateCustomState(context: any, fieldData: any, itemData: any, state: AbstractContentFormState) {
protected updateCustomState(context: any, itemData: any, state: AbstractContentFormState) {
const isRequired = state.isRequired === true;
if (this.isRequired !== isRequired) {
@ -262,13 +262,13 @@ export class FieldForm extends AbstractContentForm<RootFieldDto, FormGroup> {
}
}
for (const [key, partition] of Object.entries(this.partitions)) {
partition.updateState(context, fieldData?.[key], itemData, state);
for (const partition of Object.values(this.partitions)) {
partition.updateState(context, itemData, state);
}
}
private static buildForm() {
return new FormGroup({});
return new ExtendedFormGroup({});
}
}
@ -283,7 +283,7 @@ export class FieldValueForm extends AbstractContentForm<FieldDto, FormControl> {
this.isRequired = field.properties.isRequired && !isOptional;
}
protected updateCustomState(_context: any, _fieldData: any, _itemData: any, state: AbstractContentFormState) {
protected updateCustomState(_context: any, _itemData: any, state: AbstractContentFormState) {
const isRequired = state.isRequired === true;
if (!this.isOptional && this.isRequired !== isRequired) {
@ -335,7 +335,7 @@ export class FieldArrayForm extends AbstractContentForm<FieldDto, TemplatedFormA
public readonly isComponents: boolean,
) {
super(globals, field, fieldPath,
FieldArrayForm.buildControl(field, isOptional),
new TemplatedFormArray(new ArrayTemplate(() => this), FieldsValidators.create(field, isOptional)),
isOptional, rules);
this.form.template['form'] = this;
@ -346,7 +346,7 @@ export class FieldArrayForm extends AbstractContentForm<FieldDto, TemplatedFormA
}
public addCopy(source: ObjectFormBase) {
this.form.add().reset(getRawValue(source.form));
this.form.add().reset(source.form.value);
}
public addComponent(schemaId: string) {
@ -378,56 +378,59 @@ export class FieldArrayForm extends AbstractContentForm<FieldDto, TemplatedFormA
}
}
protected updateCustomState(context: any, fieldData: any, itemData: any, state: AbstractContentFormState) {
for (let i = 0; i < this.items.length; i++) {
this.items[i].updateState(context, fieldData?.[i], itemData, state);
}
protected updateCustomState(context: any, itemData: any, state: AbstractContentFormState) {
for (const item of this.items) {
item.updateState(context, itemData, state);
}
private static buildControl(field: FieldDto, isOptional: boolean) {
return new TemplatedFormArray(new ArrayTemplate(), FieldsValidators.create(field, isOptional));
}
}
class ArrayTemplate implements FormArrayTemplate {
public form: FieldArrayForm;
protected get model() {
return this.modelProvider();
}
constructor(
private readonly modelProvider: () => FieldArrayForm,
) {
}
public createControl() {
const child = this.form.isComponents ?
const child = this.model.isComponents ?
this.createComponent() :
this.createItem();
this.form.items = [...this.form.items, child];
this.model.items = [...this.model.items, child];
return child.form;
}
public removeControl(index: number) {
this.form.items = this.form.items.filter((_, i) => i !== index);
this.model.items = this.model.items.filter((_, i) => i !== index);
}
public clearControls() {
this.form.items = [];
this.model.items = [];
}
private createItem() {
return new ArrayItemForm(
this.form.globals,
this.form.field as RootFieldDto,
this.form.fieldPath,
this.form.isOptional,
this.form.rules,
this.form.partition);
this.model.globals,
this.model.field as RootFieldDto,
this.model.fieldPath,
this.model.isOptional,
this.model.rules,
this.model.partition);
}
private createComponent() {
return new ComponentForm(
this.form.globals,
this.form.field as RootFieldDto,
this.form.fieldPath,
this.form.isOptional,
this.form.rules,
this.form.partition);
this.model.globals,
this.model.field as RootFieldDto,
this.model.fieldPath,
this.model.isOptional,
this.model.rules,
this.model.partition);
}
}
@ -475,9 +478,9 @@ export class ObjectFormBase<TField extends FieldDto = FieldDto> extends Abstract
return this.fields[field['name'] || field];
}
protected updateCustomState(context: any, fieldData: any, _: any, state: AbstractContentFormState) {
for (const [key, field] of Object.entries(this.fields)) {
field.updateState(context, fieldData?.[key], fieldData, state);
protected updateCustomState(context: any, _: any, state: AbstractContentFormState) {
for (const field of Object.values(this.fields)) {
field.updateState(context, this.form.value, state);
}
for (const section of this.fieldSections) {
@ -606,6 +609,7 @@ export class ComponentForm extends ObjectFormBase {
new ComponentTemplate(() => this),
partition);
this.form.reset(undefined);
this.form.build();
}
@ -616,7 +620,7 @@ export class ComponentForm extends ObjectFormBase {
class ComponentTemplate extends ObjectTemplate<ComponentForm> {
public getSchema(value: any, model: ComponentForm) {
return model.globals.schemas[value?.schemaId].fields;
return model.globals.schemas[value?.schemaId]?.fields;
}
protected setControlsCore(schema: ReadonlyArray<FieldDto>, value: any, model: ComponentForm, form: FormGroup) {

46
frontend/app/shared/state/contributors.forms.ts

@ -5,27 +5,27 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, hasNoValue$, Types, value$ } from '@app/framework';
import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$, Types, ExtendedFormGroup, value$ } from '@app/framework';
import { debounceTime, map, shareReplay } from 'rxjs/operators';
import { AssignContributorDto } from './../services/contributors.service';
import { UserDto } from './../services/users.service';
export class AssignContributorForm extends Form<FormGroup, AssignContributorDto> {
public hasNoUser = hasNoValue$(this.form.controls['user']);
export class AssignContributorForm extends Form<ExtendedFormGroup, AssignContributorDto> {
public get user() {
return this.form.controls['user'];
}
public hasNoUser = hasNoValue$(this.user);
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
user: [null,
[
constructor() {
super(new ExtendedFormGroup({
user: new FormControl('',
Validators.required,
],
],
role: [null,
[
),
role: new FormControl('',
Validators.required,
],
],
),
}));
}
@ -42,18 +42,20 @@ export class AssignContributorForm extends Form<FormGroup, AssignContributorDto>
type ImportContributorsFormType = ReadonlyArray<AssignContributorDto>;
export class ImportContributorsForm extends Form<FormGroup, ImportContributorsFormType> {
public numberOfEmails = value$(this.form.controls['import']).pipe(debounceTime(100), map(v => extractEmails(v).length), shareReplay(1));
export class ImportContributorsForm extends Form<ExtendedFormGroup, ImportContributorsFormType> {
public get import() {
return this.form.controls['import'];
}
public numberOfEmails = value$(this.import).pipe(debounceTime(100), map(v => extractEmails(v).length), shareReplay(1));
public hasNoUser = this.numberOfEmails.pipe(map(v => v === 0));
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
import: ['',
[
constructor() {
super(new ExtendedFormGroup({
import: new FormControl('',
Validators.required,
],
],
),
}));
}

46
frontend/app/shared/state/languages.forms.ts

@ -5,29 +5,41 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, value$ } from '@app/framework';
import { FormControl, Validators } from '@angular/forms';
import { Form, ExtendedFormGroup, value$ } from '@app/framework';
import { AppLanguageDto, UpdateAppLanguageDto } from './../services/app-languages.service';
import { LanguageDto } from './../services/languages.service';
export class EditLanguageForm extends Form<FormGroup, UpdateAppLanguageDto, AppLanguageDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
isMaster: false,
isOptional: false,
export class EditLanguageForm extends Form<ExtendedFormGroup, UpdateAppLanguageDto, AppLanguageDto> {
public get isMaster() {
return this.form.controls['isMaster'];
}
public get isOptional() {
return this.form.controls['isOptional'];
}
constructor() {
super(new ExtendedFormGroup({
isMaster: new FormControl(false,
Validators.nullValidator,
),
isOptional: new FormControl(false,
Validators.nullValidator,
),
}));
value$(this.form.controls['isMaster'])
value$(this.isMaster)
.subscribe(value => {
if (value) {
this.form.controls['isOptional'].setValue(false);
this.isOptional.setValue(false);
}
});
value$(this.form.controls['isOptional'])
value$(this.isMaster)
.subscribe(value => {
if (value) {
this.form.controls['isMaster'].setValue(false);
this.isOptional.setValue(false);
}
});
}
@ -35,14 +47,12 @@ export class EditLanguageForm extends Form<FormGroup, UpdateAppLanguageDto, AppL
type AddLanguageFormType = { language: LanguageDto };
export class AddLanguageForm extends Form<FormGroup, AddLanguageFormType> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
language: [null,
[
export class AddLanguageForm extends Form<ExtendedFormGroup, AddLanguageFormType> {
constructor() {
super(new ExtendedFormGroup({
language: new FormControl(null,
Validators.required,
],
],
),
}));
}
}

44
frontend/app/shared/state/roles.forms.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Form, hasNoValue$, hasValue$, TemplatedFormArray } from '@app/framework';
import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$, hasValue$, TemplatedFormArray, ExtendedFormGroup } from '@app/framework';
import { CreateRoleDto, RoleDto, UpdateRoleDto } from './../services/roles.service';
export class EditRoleForm extends Form<TemplatedFormArray, UpdateRoleDto, RoleDto> {
@ -15,7 +15,7 @@ export class EditRoleForm extends Form<TemplatedFormArray, UpdateRoleDto, RoleDt
}
constructor() {
super(new TemplatedFormArray(new PermissionTemplate()));
super(new TemplatedFormArray(PermissionTemplate.INSTANCE));
}
public transformSubmit(value: any) {
@ -28,6 +28,8 @@ export class EditRoleForm extends Form<TemplatedFormArray, UpdateRoleDto, RoleDt
}
class PermissionTemplate {
public static readonly INSTANCE = new PermissionTemplate();
public createControl(_: any, initialValue: string) {
return new FormControl(initialValue, Validators.required);
}
@ -35,30 +37,34 @@ class PermissionTemplate {
type AddPermissionFormType = { permission: string };
export class AddPermissionForm extends Form<FormGroup, AddPermissionFormType> {
public hasPermission = hasValue$(this.form.controls['permission']);
export class AddPermissionForm extends Form<ExtendedFormGroup, AddPermissionFormType> {
public get permission() {
return this.form.controls['permission'];
}
public hasPermission = hasValue$(this.permission);
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
permission: ['',
[
constructor() {
super(new ExtendedFormGroup({
permission: new FormControl('',
Validators.required,
],
],
),
}));
}
}
export class AddRoleForm extends Form<FormGroup, CreateRoleDto> {
public hasNoName = hasNoValue$(this.form.controls['name']);
export class AddRoleForm extends Form<ExtendedFormGroup, CreateRoleDto> {
public get name() {
return this.form.controls['name'];
}
public hasNoName = hasNoValue$(this.name);
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
constructor() {
super(new ExtendedFormGroup({
name: new FormControl('',
Validators.required,
],
],
),
}));
}
}

41
frontend/app/shared/state/rules.forms.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Form, ValidatorsEx } from '@app/framework';
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
import { Form, ExtendedFormGroup, ValidatorsEx } from '@app/framework';
import { RuleElementDto } from '../services/rules.service';
export class ActionForm extends Form<any, FormGroup> {
@ -28,7 +28,7 @@ export class ActionForm extends Form<any, FormGroup> {
controls[property.name] = new FormControl(undefined, validator);
}
return new FormGroup(controls);
return new ExtendedFormGroup(controls);
}
protected transformSubmit(value: any): any {
@ -39,33 +39,40 @@ export class ActionForm extends Form<any, FormGroup> {
}
export class TriggerForm extends Form<any, FormGroup> {
constructor(formBuilder: FormBuilder,
constructor(
private readonly triggerType: string,
) {
super(TriggerForm.builForm(formBuilder, triggerType));
super(TriggerForm.builForm(triggerType));
}
private static builForm(formBuilder: FormBuilder, triggerType: string) {
private static builForm(triggerType: string) {
switch (triggerType) {
case 'ContentChanged': {
return formBuilder.group({ handleAll: false, schemas: undefined });
return new ExtendedFormGroup({
handleAll: new FormControl(false,
Validators.nullValidator,
),
schemas: new FormControl(undefined,
Validators.nullValidator,
),
});
}
case 'Usage': {
return formBuilder.group({
limit: [20000,
[
return new ExtendedFormGroup({
limit: new FormControl(20000,
Validators.required,
],
],
numDays: [3,
[
),
numDays: new FormControl(3,
ValidatorsEx.between(1, 30),
],
],
),
});
}
default: {
return formBuilder.group({ condition: undefined });
return new ExtendedFormGroup({
condition: new FormControl('',
Validators.nullValidator,
),
});
}
}
}

416
frontend/app/shared/state/schemas.forms.ts

@ -7,39 +7,41 @@
/* eslint-disable no-useless-escape */
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, TemplatedFormArray, ValidatorsEx, value$ } from '@app/framework';
import { AbstractControl, FormControl, Validators } from '@angular/forms';
import { Form, TemplatedFormArray, ExtendedFormGroup, ValidatorsEx, value$ } from '@app/framework';
import { map } from 'rxjs/operators';
import { AddFieldDto, CreateSchemaDto, FieldRule, SchemaDto, SchemaPropertiesDto, SynchronizeSchemaDto, UpdateSchemaDto } from './../services/schemas.service';
import { createProperties, FieldPropertiesDto, FieldPropertiesVisitor } from './../services/schemas.types';
type CreateCategoryFormType = { name: string };
export class CreateCategoryForm extends Form<FormGroup, CreateCategoryFormType> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: [''],
export class CreateCategoryForm extends Form<ExtendedFormGroup, CreateCategoryFormType> {
constructor() {
super(new ExtendedFormGroup({
name: new FormControl('',
Validators.nullValidator,
),
}));
}
}
export class CreateSchemaForm extends Form<FormGroup, CreateSchemaDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
export class CreateSchemaForm extends Form<ExtendedFormGroup, CreateSchemaDto> {
constructor() {
super(new ExtendedFormGroup({
name: new FormControl('', [
Validators.required,
Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'i18n:schemas.schemaNameValidationMessage'),
],
],
type: ['Default',
[
]),
type: new FormControl('Default',
Validators.required,
],
],
initialCategory: undefined,
importing: {},
),
initialCategory: new FormControl(undefined,
Validators.nullValidator,
),
importing: new FormControl({},
Validators.nullValidator,
),
}));
}
@ -56,17 +58,23 @@ export class CreateSchemaForm extends Form<FormGroup, CreateSchemaDto> {
}
}
export class SynchronizeSchemaForm extends Form<FormGroup, SynchronizeSchemaDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
json: {},
fieldsDelete: false,
fieldsRecreate: false,
export class SynchronizeSchemaForm extends Form<ExtendedFormGroup, SynchronizeSchemaDto> {
constructor() {
super(new ExtendedFormGroup({
json: new FormControl({},
Validators.nullValidator,
),
fieldsDelete: new FormControl(false,
Validators.nullValidator,
),
fieldsRecreate: new FormControl(false,
Validators.nullValidator,
),
}));
}
public loadSchema(schema: SchemaDto) {
this.form.get('json')!.setValue(schema.export());
this.form.patchValue({ json: schema.export() });
}
public transformSubmit(value: any) {
@ -79,12 +87,12 @@ export class SynchronizeSchemaForm extends Form<FormGroup, SynchronizeSchemaDto>
}
export class ConfigureFieldRulesForm extends Form<TemplatedFormArray, ReadonlyArray<FieldRule>, SchemaDto> {
public get rulesControls(): ReadonlyArray<FormGroup> {
public get rulesControls(): ReadonlyArray<ExtendedFormGroup> {
return this.form.controls as any;
}
constructor(formBuilder: FormBuilder) {
super(new TemplatedFormArray(new FieldRuleTemplate(formBuilder)));
constructor() {
super(new TemplatedFormArray(FieldRuleTemplate.INSTANCE));
}
public add(fieldNames: ReadonlyArray<string>) {
@ -101,25 +109,19 @@ export class ConfigureFieldRulesForm extends Form<TemplatedFormArray, ReadonlyAr
}
class FieldRuleTemplate {
constructor(private readonly formBuilder: FormBuilder) {}
public static readonly INSTANCE = new FieldRuleTemplate();
public createControl(_: any, fieldNames?: ReadonlyArray<string>) {
return this.formBuilder.group({
action: ['Disable',
[
return new ExtendedFormGroup({
name: new FormControl('Disable',
Validators.required,
],
],
field: [fieldNames?.[0],
[
),
field: new FormControl(fieldNames?.[0],
Validators.required,
],
],
condition: ['',
[
),
condition: new FormControl('',
Validators.required,
],
],
),
});
}
}
@ -127,12 +129,12 @@ class FieldRuleTemplate {
type ConfigurePreviewUrlsFormType = { [name: string]: string };
export class ConfigurePreviewUrlsForm extends Form<TemplatedFormArray, ConfigurePreviewUrlsFormType, SchemaDto> {
public get previewControls(): ReadonlyArray<FormGroup> {
public get previewControls(): ReadonlyArray<ExtendedFormGroup> {
return this.form.controls as any;
}
constructor(formBuilder: FormBuilder) {
super(new TemplatedFormArray(new PreviewUrlTemplate(formBuilder)));
constructor() {
super(new TemplatedFormArray(PreviewUrlTemplate.INSTANCE));
}
public transformLoad(value: Partial<SchemaDto>) {
@ -159,179 +161,189 @@ export class ConfigurePreviewUrlsForm extends Form<TemplatedFormArray, Configure
}
class PreviewUrlTemplate {
constructor(private readonly formBuilder: FormBuilder) {}
public static readonly INSTANCE = new PreviewUrlTemplate();
public createControl() {
return this.formBuilder.group({
name: ['',
[
return new ExtendedFormGroup({
name: new FormControl('',
Validators.required,
],
],
url: ['',
[
),
url: new FormControl('',
Validators.required,
],
],
),
});
}
}
export class EditSchemaScriptsForm extends Form<FormGroup, {}, object> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
query: '',
create: '',
change: '',
delete: '',
update: '',
export class EditSchemaScriptsForm extends Form<ExtendedFormGroup, {}, object> {
constructor() {
super(new ExtendedFormGroup({
query: new FormControl('',
Validators.nullValidator,
),
create: new FormControl('',
Validators.nullValidator,
),
change: new FormControl('',
Validators.nullValidator,
),
delete: new FormControl('',
Validators.nullValidator,
),
update: new FormControl('',
Validators.nullValidator,
),
}));
}
}
export class EditFieldForm extends Form<FormGroup, {}, FieldPropertiesDto> {
constructor(formBuilder: FormBuilder, properties: FieldPropertiesDto) {
super(EditFieldForm.buildForm(formBuilder, properties));
export class EditFieldForm extends Form<ExtendedFormGroup, {}, FieldPropertiesDto> {
constructor(properties: FieldPropertiesDto) {
super(EditFieldForm.buildForm(properties));
}
private static buildForm(formBuilder: FormBuilder, properties: FieldPropertiesDto) {
private static buildForm(properties: FieldPropertiesDto) {
const config = {
label: ['',
[
label: new FormControl('',
Validators.maxLength(100),
],
],
hints: ['',
[
),
hints: new FormControl('',
Validators.maxLength(1000),
],
],
placeholder: ['',
[
),
placeholder: new FormControl('',
Validators.maxLength(1000),
],
],
editor: undefined,
editorUrl: undefined,
isRequired: false,
isRequiredOnPublish: false,
isHalfWidth: false,
tags: [],
),
editor: new FormControl(undefined,
Validators.nullValidator,
),
editorUrl: new FormControl(undefined,
Validators.nullValidator,
),
isRequired: new FormControl(false,
Validators.nullValidator,
),
isRequiredOnPublish: new FormControl(false,
Validators.nullValidator,
),
isHalfWidth: new FormControl(false,
Validators.nullValidator,
),
tags: new FormControl([],
Validators.nullValidator,
),
};
const visitor = new EditFieldFormVisitor(config);
properties.accept(new EditFieldFormVisitor(config));
properties.accept(visitor);
return formBuilder.group(config);
return new ExtendedFormGroup(config);
}
}
export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
constructor(
private readonly config: { [key: string]: any },
private readonly config: { [key: string]: AbstractControl },
) {
}
public visitArray() {
this.config['maxItems'] = undefined;
this.config['minItems'] = undefined;
this.config['uniqueFields'] = undefined;
this.config['maxItems'] = new FormControl(undefined);
this.config['minItems'] = new FormControl(undefined);
this.config['uniqueFields'] = new FormControl(undefined);
}
public visitAssets() {
this.config['allowDuplicates'] = undefined;
this.config['allowedExtensions'] = undefined;
this.config['aspectHeight'] = undefined;
this.config['aspectHeight'] = undefined;
this.config['aspectWidth'] = undefined;
this.config['defaultValue'] = undefined;
this.config['defaultValues'] = undefined;
this.config['expectedType'] = undefined;
this.config['folderId'] = undefined;
this.config['maxHeight'] = undefined;
this.config['maxItems'] = undefined;
this.config['maxSize'] = undefined;
this.config['maxWidth'] = undefined;
this.config['minHeight'] = undefined;
this.config['minItems'] = undefined;
this.config['minSize'] = undefined;
this.config['minWidth'] = undefined;
this.config['previewMode'] = undefined;
this.config['resolveFirst'] = undefined;
this.config['allowDuplicates'] = new FormControl(undefined);
this.config['allowedExtensions'] = new FormControl(undefined);
this.config['aspectHeight'] = new FormControl(undefined);
this.config['aspectHeight'] = new FormControl(undefined);
this.config['aspectWidth'] = new FormControl(undefined);
this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = new FormControl(undefined);
this.config['expectedType'] = new FormControl(undefined);
this.config['folderId'] = new FormControl(undefined);
this.config['maxHeight'] = new FormControl(undefined);
this.config['maxItems'] = new FormControl(undefined);
this.config['maxSize'] = new FormControl(undefined);
this.config['maxWidth'] = new FormControl(undefined);
this.config['minHeight'] = new FormControl(undefined);
this.config['minItems'] = new FormControl(undefined);
this.config['minSize'] = new FormControl(undefined);
this.config['minWidth'] = new FormControl(undefined);
this.config['previewMode'] = new FormControl(undefined);
this.config['resolveFirst'] = new FormControl(undefined);
}
public visitBoolean() {
this.config['inlineEditable'] = undefined;
this.config['defaultValues'] = undefined;
this.config['defaultValue'] = undefined;
this.config['inlineEditable'] = new FormControl(undefined);
this.config['defaultValues'] = new FormControl(undefined);
this.config['defaultValue'] = new FormControl(undefined);
}
public visitComponent() {
this.config['schemaIds'] = undefined;
this.config['schemaIds'] = new FormControl(undefined);
}
public visitComponents() {
this.config['schemaIds'] = undefined;
this.config['maxItems'] = undefined;
this.config['minItems'] = undefined;
this.config['uniqueFields'] = undefined;
this.config['schemaIds'] = new FormControl(undefined);
this.config['maxItems'] = new FormControl(undefined);
this.config['minItems'] = new FormControl(undefined);
this.config['uniqueFields'] = new FormControl(undefined);
}
public visitDateTime() {
this.config['calculatedDefaultValue'] = undefined;
this.config['defaultValue'] = undefined;
this.config['defaultValues'] = undefined;
this.config['format'] = undefined;
this.config['maxValue'] = [undefined, ValidatorsEx.validDateTime()];
this.config['minValue'] = [undefined, ValidatorsEx.validDateTime()];
this.config['calculatedDefaultValue'] = new FormControl(undefined);
this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = new FormControl(undefined);
this.config['format'] = new FormControl(undefined);
this.config['maxValue'] = new FormControl(undefined, ValidatorsEx.validDateTime());
this.config['minValue'] = new FormControl(undefined, ValidatorsEx.validDateTime());
}
public visitNumber() {
this.config['allowedValues'] = undefined;
this.config['defaultValue'] = undefined;
this.config['defaultValues'] = undefined;
this.config['inlineEditable'] = undefined;
this.config['isUnique'] = undefined;
this.config['maxValue'] = undefined;
this.config['minValue'] = undefined;
this.config['allowedValues'] = new FormControl(undefined);
this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = new FormControl(undefined);
this.config['inlineEditable'] = new FormControl(undefined);
this.config['isUnique'] = new FormControl(undefined);
this.config['maxValue'] = new FormControl(undefined);
this.config['minValue'] = new FormControl(undefined);
}
public visitReferences() {
this.config['allowDuplicates'] = undefined;
this.config['defaultValue'] = undefined;
this.config['defaultValues'] = undefined;
this.config['maxItems'] = undefined;
this.config['minItems'] = undefined;
this.config['mustBePublished'] = false;
this.config['resolveReference'] = false;
this.config['schemaIds'] = undefined;
this.config['allowDuplicates'] = new FormControl(undefined);
this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = new FormControl(undefined);
this.config['maxItems'] = new FormControl(undefined);
this.config['minItems'] = new FormControl(undefined);
this.config['mustBePublished'] = new FormControl(false);
this.config['resolveReference'] = new FormControl(false);
this.config['schemaIds'] = new FormControl(undefined);
}
public visitString() {
this.config['allowedValues'] = undefined;
this.config['contentType'] = undefined;
this.config['defaultValue'] = undefined;
this.config['defaultValues'] = undefined;
this.config['folderId'] = undefined;
this.config['inlineEditable'] = undefined;
this.config['isUnique'] = undefined;
this.config['maxCharacters'] = undefined;
this.config['maxLength'] = undefined;
this.config['maxWords'] = undefined;
this.config['minCharacters'] = undefined;
this.config['minLength'] = undefined;
this.config['minWords'] = undefined;
this.config['pattern'] = undefined;
this.config['patternMessage'] = undefined;
this.config['allowedValues'] = new FormControl(undefined);
this.config['contentType'] = new FormControl(undefined);
this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = new FormControl(undefined);
this.config['folderId'] = new FormControl(undefined);
this.config['inlineEditable'] = new FormControl(undefined);
this.config['isUnique'] = new FormControl(undefined);
this.config['maxCharacters'] = new FormControl(undefined);
this.config['maxLength'] = new FormControl(undefined);
this.config['maxWords'] = new FormControl(undefined);
this.config['minCharacters'] = new FormControl(undefined);
this.config['minLength'] = new FormControl(undefined);
this.config['minWords'] = new FormControl(undefined);
this.config['pattern'] = new FormControl(undefined);
this.config['patternMessage'] = new FormControl(undefined);
}
public visitTags() {
this.config['allowedValues'] = undefined;
this.config['defaultValue'] = undefined;
this.config['defaultValues'] = undefined;
this.config['maxItems'] = undefined;
this.config['minItems'] = undefined;
this.config['allowedValues'] = new FormControl(undefined);
this.config['defaultValue'] = new FormControl(undefined);
this.config['defaultValues'] = new FormControl(undefined);
this.config['maxItems'] = new FormControl(undefined);
this.config['minItems'] = new FormControl(undefined);
}
public visitGeolocation() {
@ -347,64 +359,72 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
}
}
export class EditSchemaForm extends Form<FormGroup, UpdateSchemaDto, SchemaPropertiesDto> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
label: ['',
[
export class EditSchemaForm extends Form<ExtendedFormGroup, UpdateSchemaDto, SchemaPropertiesDto> {
constructor() {
super(new ExtendedFormGroup({
label: new FormControl('',
Validators.maxLength(100),
],
],
hints: ['',
[
),
hints: new FormControl('',
Validators.maxLength(1000),
],
],
contentsSidebarUrl: '',
contentSidebarUrl: '',
contentEditorUrl: '',
validateOnPublish: false,
tags: [],
),
contentsSidebarUrl: new FormControl('',
Validators.nullValidator,
),
contentSidebarUrl: new FormControl('',
Validators.nullValidator,
),
contentEditorUrl: new FormControl('',
Validators.nullValidator,
),
validateOnPublish: new FormControl(false,
Validators.nullValidator,
),
tags: new FormControl([],
Validators.nullValidator,
),
}));
}
}
export class AddFieldForm extends Form<FormGroup, AddFieldDto> {
public isContentField = value$(this.form.get('type')!).pipe(map(x => x !== 'UI'));
export class AddFieldForm extends Form<ExtendedFormGroup, AddFieldDto> {
public isContentField = value$(this.form.controls['type']).pipe(map(x => x !== 'UI'));
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
type: ['String',
[
constructor() {
super(new ExtendedFormGroup({
type: new FormControl('String',
Validators.required,
],
],
name: ['',
[
),
name: new FormControl('', [
Validators.required,
Validators.maxLength(40),
ValidatorsEx.pattern('[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*', 'i18n:schemas.field.nameValidationMessage'),
],
],
isLocalizable: false,
]),
isLocalizable: new FormControl(false,
Validators.nullValidator,
),
}));
}
public transformLoad(value: Partial<AddFieldDto>) {
const isLocalizable = value.partitioning === 'language';
const { name, properties, partitioning } = value;
const isLocalizable = partitioning === 'language';
const type =
value.properties ?
value.properties.fieldType :
properties ?
properties.fieldType :
'String';
return { name: value.name, isLocalizable, type };
return { name, isLocalizable, type };
}
public transformSubmit(value: any) {
const properties = createProperties(value.type);
const partitioning = value.isLocalizable ? 'language' : 'invariant';
const { name, type, isLocalizable } = value;
const properties = createProperties(type);
const partitioning = isLocalizable ? 'language' : 'invariant';
return { name: value.name, partitioning, properties };
return { name, partitioning, properties };
}
}

22
frontend/app/shared/state/workflows.forms.ts

@ -5,20 +5,22 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, hasNoValue$ } from '@app/framework';
import { FormControl, Validators } from '@angular/forms';
import { Form, hasNoValue$, ExtendedFormGroup } from '@app/framework';
import { CreateWorkflowDto } from './../services/workflows.service';
export class AddWorkflowForm extends Form<FormGroup, CreateWorkflowDto> {
public hasNoName = hasNoValue$(this.form.controls['name']);
export class AddWorkflowForm extends Form<ExtendedFormGroup, CreateWorkflowDto> {
public get name() {
return this.form.controls['name'];
}
public hasNoName = hasNoValue$(this.name);
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
constructor() {
super(new ExtendedFormGroup({
name: new FormControl('',
Validators.required,
],
],
),
}));
}
}

Loading…
Cancel
Save