/* * Squidex Headless CMS * * @license * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ 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'; import { LanguageDto } from './../services/languages.service'; import { FieldDto, RootFieldDto, SchemaDto, TableField } from './../services/schemas.service'; import { ComponentFieldPropertiesDto, fieldInvariant } from './../services/schemas.types'; import { ComponentRulesProvider, RootRulesProvider, RulesProvider } from './contents.form-rules'; import { AbstractContentForm, AbstractContentFormState, FieldSection, FormGlobals, groupFields, PartitionConfig } from './contents.forms-helpers'; import { FieldDefaultValue, FieldsValidators } from './contents.forms.visitors'; type SaveQueryFormType = { name: string; user: boolean }; export class SaveQueryForm extends Form { constructor() { super(new ExtendedFormGroup({ name: new FormControl('', Validators.required, ), user: new FormControl(false, Validators.nullValidator, ), })); } } export class PatchContentForm extends Form { private readonly editableFields: ReadonlyArray; constructor( private readonly listFields: ReadonlyArray, private readonly language: AppLanguageDto, ) { super(new ExtendedFormGroup({})); this.editableFields = this.listFields.filter(x => Types.is(x, RootFieldDto) && x.isInlineEditable) as any; for (const field of this.editableFields) { const validators = FieldsValidators.create(field, this.language.isOptional); this.form.setControl(field.name, new FormControl(undefined, { validators })); } } public submit() { const result = super.submit(); if (result) { const request = {}; for (const field of this.editableFields) { const value = result[field.name]; if (field.isLocalizable) { request[field.name] = { [this.language.iso2Code]: value }; } else { request[field.name] = { iv: value }; } } return request; } return result; } } export class EditContentForm extends Form { private readonly fields: { [name: string]: FieldForm } = {}; private readonly valueChange$ = new BehaviorSubject(this.form.value); private initialData: any; public readonly sections: ReadonlyArray>; public get valueChanges(): Observable { return this.valueChange$; } public get value() { return this.valueChange$.value; } constructor( public readonly languages: ReadonlyArray, public readonly schema: SchemaDto, schemas: { [id: string ]: SchemaDto }, public context: any, debounce = 100, ) { super(new ExtendedFormGroup({})); const globals: FormGlobals = { schema, schemas, partitions: new PartitionConfig(languages), remoteValidator: this.remoteValidator, }; const rules = new RootRulesProvider(schema); this.sections = groupFields(schema.fields).map(({ separator, fields }) => { const forms: FieldForm[] = []; for (const field of fields) { const childForm = new FieldForm( globals, field, field.name, rules); this.form.setControl(field.name, childForm.form); forms.push(childForm); this.fields[field.name] = childForm; } return new FieldSection(separator, forms); }); value$(this.form).pipe(debounceTimeSafe(debounce), distinctUntilChanged(Types.equals)).subscribe(value => { this.valueChange$.next(value); this.updateState(value); }); this.updateInitialData(); } public get(field: string | RootFieldDto): FieldForm | undefined { if (Types.is(field, RootFieldDto)) { return this.fields[field.name]; } else { return this.fields[field]; } } public hasChanged() { return !Types.equals(this.initialData, this.value, true); } public hasChanges(changes: any) { return !Types.equals(this.initialData, changes, true); } public load(value: any, isInitial?: boolean) { super.load(value); if (isInitial) { this.updateInitialData(); } } protected disable() { this.form.disable(); } protected enable() { this.form.enable({ onlySelf: true }); this.updateState(this.value); } public setContext(context?: any) { this.context = context; this.updateState(this.value); } public submitCompleted(options?: { newValue?: any; noReset?: boolean }) { super.submitCompleted(options); this.updateInitialData(); } private updateState(data: any) { const context = { ...this.context || {}, data }; for (const field of Object.values(this.fields)) { field.updateState(context, data, { isDisabled: this.form.disabled }); } for (const section of this.sections) { section.updateHidden(); } } private updateInitialData() { this.initialData = this.form.value; } } export class FieldForm extends AbstractContentForm { private readonly partitions: { [partition: string]: FieldItemForm } = {}; private isRequired: boolean; constructor( globals: FormGlobals, field: RootFieldDto, fieldPath: string, rules: RulesProvider, ) { super(globals, field, fieldPath, FieldForm.buildForm(), false, rules); for (const { key, isOptional } of globals.partitions.getAll(field)) { const childForm = buildForm( this.globals, field, this.path(key), isOptional, rules, key); this.partitions[key] = childForm; this.form.setControl(key, childForm.form); } this.isRequired = field.properties.isRequired; } public get(language: string | LanguageDto) { if (this.field.isLocalizable) { return this.partitions[language['iso2Code'] || language]; } else { return this.partitions[fieldInvariant]; } } protected updateCustomState(context: any, itemData: any, state: AbstractContentFormState) { const isRequired = state.isRequired === true; if (this.isRequired !== isRequired) { this.isRequired = isRequired; for (const partition of Object.values(this.partitions)) { if (!partition.isOptional) { let validators = FieldsValidators.create(this.field, false); if (isRequired) { validators.push(Validators.required); } else { validators = validators.filter(x => x !== Validators.required); } if (this.globals.remoteValidator) { validators.push(this.globals.remoteValidator); } partition.form.setValidators(validators); partition.form.updateValueAndValidity(); } } } for (const partition of Object.values(this.partitions)) { partition.updateState(context, itemData, state); } } private static buildForm() { return new ExtendedFormGroup({}); } } export class FieldValueForm extends AbstractContentForm { private isRequired = false; constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { super(globals, field, fieldPath, FieldValueForm.buildControl(field, isOptional, partition, globals), isOptional, rules); this.isRequired = field.properties.isRequired && !isOptional; } protected updateCustomState(_context: any, _itemData: any, state: AbstractContentFormState) { const isRequired = state.isRequired === true; if (!this.isOptional && this.isRequired !== isRequired) { this.isRequired = isRequired; let validators = FieldsValidators.create(this.field, true); if (isRequired) { validators.push(Validators.required); } else { validators = validators.filter(x => x !== Validators.required); } this.form.setValidators(validators); this.form.updateValueAndValidity(); } } private static buildControl(field: FieldDto, isOptional: boolean, partition: string, globals: FormGlobals) { const value = FieldDefaultValue.get(field, partition); const validators = FieldsValidators.create(field, isOptional); if (globals.remoteValidator) { validators.push(globals.remoteValidator); } return new FormControl(value, { validators }); } } export class FieldArrayForm extends AbstractContentForm { private readonly item$ = new BehaviorSubject>([]); public get itemChanges(): Observable> { return this.item$; } public get items() { return this.item$.value; } public set items(value: ReadonlyArray) { this.item$.next(value); } constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, public readonly partition: string, public readonly isComponents: boolean, ) { super(globals, field, fieldPath, new TemplatedFormArray(new ArrayTemplate(() => this), FieldsValidators.create(field, isOptional)), isOptional, rules); this.form.template['form'] = this; } public get(index: number) { return this.items[index]; } public addCopy(source: ObjectFormBase) { this.form.add().reset(source.form.value); } public addComponent(schemaId: string) { this.form.add().reset({ schemaId }); } public addItem() { this.form.add(); } public removeItemAt(index: number) { this.form.removeAt(index); } public move(index: number, item: ObjectFormBase) { const children = [...this.items]; children.splice(children.indexOf(item), 1); children.splice(index, 0, item); this.items = children; this.sort(children); } public sort(children: ReadonlyArray) { for (let i = 0; i < children.length; i++) { this.form.setControl(i, children[i].form); } } protected updateCustomState(context: any, itemData: any, state: AbstractContentFormState) { for (const item of this.items) { item.updateState(context, itemData, state); } } } class ArrayTemplate implements FormArrayTemplate { protected get model() { return this.modelProvider(); } constructor( private readonly modelProvider: () => FieldArrayForm, ) { } public createControl() { const child = this.model.isComponents ? this.createComponent() : this.createItem(); this.model.items = [...this.model.items, child]; return child.form; } public removeControl(index: number) { this.model.items = this.model.items.filter((_, i) => i !== index); } public clearControls() { this.model.items = []; } private createItem() { return new ArrayItemForm( 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.model.globals, this.model.field as RootFieldDto, this.model.fieldPath, this.model.isOptional, this.model.rules, this.model.partition); } } export type FieldItemForm = ComponentForm | FieldValueForm | FieldArrayForm; type FieldMap = { [name: string]: FieldItemForm }; export class ObjectFormBase extends AbstractContentForm { private readonly fieldSections$ = new BehaviorSubject>>([]); private readonly fields$ = new BehaviorSubject({}); public get fieldSectionsChanges(): Observable>> { return this.fieldSections$; } public get fieldSections() { return this.fieldSections$.value; } public set fieldSections(value: ReadonlyArray>) { this.fieldSections$.next(value); } public get fieldsChanges(): Observable { return this.fields$; } public get fields() { return this.fields$.value; } public set fields(value: FieldMap) { this.fields$.next(value); } constructor(globals: FormGlobals, field: TField, fieldPath: string, isOptional: boolean, rules: RulesProvider, template: ObjectTemplate, public readonly partition: string, ) { super(globals, field, fieldPath, ObjectFormBase.buildControl(template), isOptional, rules); } public get(field: string | { name: string }): FieldItemForm | undefined { return this.fields[field['name'] || field]; } 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) { section.updateHidden(); } } private static buildControl(template: ObjectTemplate) { return new TemplatedFormGroup(template); } } abstract class ObjectTemplate implements FormGroupTemplate { private currentSchema: ReadonlyArray | undefined; protected get model() { return this.modelProvider(); } constructor( private readonly modelProvider: () => T, ) { } protected abstract getSchema(value: any, model: T): ReadonlyArray | undefined; public setControls(form: FormGroup, value: any) { const schema = this.getSchema(value, this.model); if (this.currentSchema !== schema) { this.clearControlsCore(this.model); if (schema) { this.setControlsCore(schema, value, this.model, form); } this.currentSchema = schema; } } public clearControls() { if (this.currentSchema !== undefined) { this.clearControlsCore(this.model); this.currentSchema = undefined; } } protected setControlsCore(schema: ReadonlyArray, value: any, model: T, form: FormGroup) { const fieldMap: FieldMap = {}; const fieldSections: FieldSection[] = []; for (const { separator, fields } of groupFields(schema)) { const forms: FieldItemForm[] = []; for (const field of fields) { const childForm = buildForm( model.globals, field, model.path(field.name), model.isOptional, model.rules, model.partition); form.setControl(field.name, childForm.form); forms.push(childForm); fieldMap[field.name] = childForm; } fieldSections.push(new FieldSection(separator, forms)); } model.fields = fieldMap; model.fieldSections = fieldSections; } protected clearControlsCore(model: T) { for (const name of Object.keys(model.form.controls)) { model.form.removeControl(name); } model.fields = {}; model.fieldSections = []; } } export class ArrayItemForm extends ObjectFormBase { constructor(globals: FormGlobals, field: RootFieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { super(globals, field, fieldPath, isOptional, rules, new ArrayItemTemplate(() => this), partition); this.form.build({}); } } class ArrayItemTemplate extends ObjectTemplate { public getSchema() { return this.model.field.nested; } } export class ComponentForm extends ObjectFormBase { private readonly schema$ = new BehaviorSubject(undefined); public get schemaChanges(): Observable { return this.schema$; } public get schema() { return this.schema$.value; } public set schema(value: SchemaDto | undefined) { this.schema$.next(value); } public get properties() { return this.field.properties as ComponentFieldPropertiesDto; } constructor(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { super(globals, field, fieldPath, isOptional, new ComponentRulesProvider(fieldPath, rules, () => this.schema), new ComponentTemplate(() => this), partition); this.form.reset(undefined); this.form.build(); } public selectSchema(schemaId: string) { this.form.reset({ schemaId }); } } class ComponentTemplate extends ObjectTemplate { public getSchema(value: any, model: ComponentForm) { return model.globals.schemas[value?.schemaId]?.fields; } protected setControlsCore(schema: ReadonlyArray, value: any, model: ComponentForm, form: FormGroup) { form.setControl('schemaId', new FormControl()); this.model.schema = model.globals.schemas[value?.schemaId]; super.setControlsCore(schema, value, model, form); } protected clearControlsCore(model: ComponentForm) { this.model.schema = undefined; super.clearControlsCore(model); } } function buildForm(globals: FormGlobals, field: FieldDto, fieldPath: string, isOptional: boolean, rules: RulesProvider, partition: string) { switch (field.properties.fieldType) { case 'Array': return new FieldArrayForm(globals, field, fieldPath, isOptional, rules, partition, false); case 'Component': return new ComponentForm(globals, field, fieldPath, isOptional, rules, partition); case 'Components': return new FieldArrayForm(globals, field, fieldPath, isOptional, rules, partition, true); default: return new FieldValueForm(globals, field, fieldPath, isOptional, rules, partition); } }