File

src/app/shared/widgets/dynamic-form/dynamic-form-component/dynamic-form-fields.component.ts

Index

Properties

Properties

additionalProperties
additionalProperties: any
Type : any
Optional

Array of additional properties to view in select dialog list. used with 'select-dialog' type.

autofocus
autofocus: boolean
Type : boolean
Optional

Auto focus field.

autosize
autosize: boolean
Type : boolean
Optional

Sets autosize property for type 'textarea'.

autosizeMinRows
autosizeMinRows: string
Type : string
Optional

min rows for textarea field

categoryUrl
categoryUrl: string
Type : string
Optional

URL used to load version from. default is 'category-select-publications' for publications and 'category-select-asset-trees' for asset-trees Used with type 'category-selector'.

chipRemovable
chipRemovable: boolean
Type : boolean
Optional

Makes chips removable in 'chip-list' type. @default(true)

clearable
clearable: boolean
Type : boolean
Optional

Shows clear selection button. used with 'lookup' and 'multi-lookup' types. @default(true)

confirmationLabel
confirmationLabel: string
Type : string
Optional

Localized key for password confirmation lable. used with 'password' type.

dataType
dataType: string
Type : string
Optional

List folder data type. used with 'worklist' type.

dateFormat
dateFormat: string
Type : string
Optional

Date format. used with 'date' type. @default(dd.mm.yyyy)

defaultValue
defaultValue: any
Type : any
Optional

Initial value of the field.

descriptionField
descriptionField: string
Type : string
Optional

Description field of the object. it is used with 'lookup' and 'multi-lookup' types to define description field. @default(description)

dialogContext
dialogContext: any
Type : any
Optional

Dialog data for page-dialog type.

dialogDescription
dialogDescription: any
Type : any
Optional

Dialog data property used to set the dialog description when closed for page-dialog type.

dialogIdentifier
dialogIdentifier: string
Type : string
Optional

Dialog identifier for page-dialog type.

dialogModule
dialogModule: string
Type : string
Optional

Dialog module for page-dialog type.

disabled
disabled: boolean
Type : boolean
Optional

Enables\Disables the field.

dropValueIdentifier
dropValueIdentifier: string
Type : string
Optional

Identifier field that will be used to get the value while dropping it in the textarea field.

enableMinimap
enableMinimap: boolean
Type : boolean
Optional

Enable/Disable editor minimap for source-code-editor type.

errors
errors: ValidationError[]
Type : ValidationError[]
Optional

List of field validation errors.

field
field: string
Type : string

Unique field identifier.

fileUploadUrl
fileUploadUrl: string
Type : string
Optional

File upload url. used with 'file-upload type.

filterChannelTypes
filterChannelTypes: string[]
Type : string[]
Optional

Applies channel types filter on versions. supported values are PURCHASE, SALES, ARCHIVE. used with 'category-selector' type.

floatingLabel
floatingLabel: boolean
Type : boolean
Optional

Shows floating label for the field. @default(false)

height
height: number
Type : number
Optional

Field height. used with 'text' type.

hidden
hidden: boolean
Type : boolean
Optional

Hides or shows the field.

hideConfirmationField
hideConfirmationField: boolean
Type : boolean
Optional

Hides\Shows second password confirmation field. used with 'password' type. @default(true)

hint
hint: string
Type : string
Optional

Localized key for field hint.

hintOnFocus
hintOnFocus: boolean
Type : boolean
Optional

Shows hint on focus.

hintTranslations
hintTranslations: any
Type : any
Optional

hint translation in other locales.

identifierField
identifierField: string
Type : string
Optional

Identifier field of the object. it is used with 'lookup' and 'multi-lookup' types to define identifier field. @default(identifier)

includeSubCategories
includeSubCategories: boolean
Type : boolean
Optional

Includes sub-categories value. used with 'category-selector' type.

items
items: DynamicFormField[]
Type : DynamicFormField[]
Optional

collection of all data for multi-components

label
label: string
Type : string

Localized key for field label

listfolderType
listfolderType: string
Type : string
Optional

List folder type. used with 'worklist' type.

localizedText
localizedText: boolean
Type : boolean
Optional

Sets field as localized text. @default(false)

lookupAllowEmpty
lookupAllowEmpty: boolean
Type : boolean
Optional

Allows empty selection for lookup types. @default(false)

lookupOptions
lookupOptions: any[]
Type : any[]
Optional

Array of static lookup options. used with these types: lookup, multi-lookup, select-dialog. (See also lookupUrl)

lookupUrl
lookupUrl: string
Type : string
Optional

URL to load lookup options from. used with these types: lookup, multi-lookup, select-dialog. (See also lookupOptions)

maxLength
maxLength: number
Type : number
Optional

Sets maximum characters length for text types.

menuInteractions
menuInteractions: Selectors
Type : Selectors
Optional

Identifiers of the interactions that are supposed to be loaded for the menu.

multiCategorySelect
multiCategorySelect: boolean
Type : boolean
Optional

Allows multiple category selection. Used with type 'category-selector'. @default(false)

order
order: number
Type : number
Optional

Order of the field in the form. @default(0)

publication
publication: string
Type : string
Optional

If not set the version select lookup will be shown, otherwise the provided version will be used and the category select lookup only will be shown. Used with type 'category-selector'.

readonly
readonly: boolean
Type : boolean
Optional

Makes the field Readonly or editable. @default(false)

required
required: boolean
Type : boolean
Optional

Makes the field required or optional.

selectDialogItemType
selectDialogItemType: string
Type : string
Optional

Item type used for select-dialog type.

showEditorOnInit
showEditorOnInit: boolean
Type : boolean
Optional

Triggers the display of Tiny editor implmentation in its onInit callback

showEditpasswordButton
showEditpasswordButton: boolean
Type : boolean
Optional

Shows\Hides edit password button. used with 'password' type. @default(false)

showHint
showHint: boolean
Type : boolean
Optional

This property shouldn't be added in configuration. It is only used internally.

showIncludeSubCategories
showIncludeSubCategories: boolean
Type : boolean
Optional

Shows\Hides include sub-categories checkbox. used with 'category-selector' type. @default(false)

sourceCodeLanguage
sourceCodeLanguage: string
Type : string
Optional

Source code programming language. It is used with type = 'source-code-editor'.

supportedFileTypes
supportedFileTypes: string
Type : string
Optional

Comma-separated list of supported file types. used with 'file-upload' type.

supportedMimeTypes
supportedMimeTypes: string[]
Type : string[]
Optional

Array of supported MIME types. used with 'file-upload' type.

tab
tab: string
Type : string
Optional

The target tab. used in dynamic forms with mutliple tabs. supported tabs are localizedtext, listFolder, userRights

translations
translations: any
Type : any
Optional

Label translation in other locales.

type
type: string
Type : string

Type of the field. supported types are: boolean, boolean-indeterminate, category-selector, chip-list, date, date-time, file-upload, heading, lookup, multi-lookup, number, number-decimal, password, plain, select-dialog, text, textarea, toggler, multi-toggler, user-right, worklist, source-code-editor

url
url: string
Type : string
Optional

URL to load user rights. used with 'user-right' type.

validations
validations: DataValidation[]
Type : DataValidation[]
Optional

Validations to be applied on field input.

value
value: any
Type : any
Optional

This property shouldn't be added in configuration. It is only used internally.

import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  OnDestroy,
  Input,
  Output,
  EventEmitter,
  ViewChild,
} from "@angular/core";
import { EMPTY, Observable, Subject } from "rxjs";
import { NgUnsubscribe } from "../../../ng-unsubscribe";
import { map, takeUntil, withLatestFrom } from "rxjs/operators";
import { WidgetframeService } from "../../widgetframe/widgetframe.service";
import { SelectItemsDialogComponent } from "../../../components/select-items-dialog/select-items-dialog.component";
import { CurrentLocaleService } from "../../../components/i18n/currentLocale.service";
import { DEFAULT_TEXT_EDITOR_CONFIG } from "../../../components/tiny-text-editor/tiny-text-editor.component";
import { BaseConfiguration } from "../../widgetframe/widgetframe.component";
import { DataValidation } from "../../../components/validation/validation.service";
import { ValidationError } from "../../../components/validation/validators";
import { TranslateService } from "@ngx-translate/core";
import { OwlDateTimeComponent, OwlDateTimeIntl } from "ng-pick-datetime";
import { DateAdapter } from "@angular/material/core";
import { getOrDefault } from "../../widget.configuration";
import {
  angularWidgetBridgeInput,
  stripPasteContent,
} from "../../../components/util/util.service";
import { PageDialogComponent } from "../../../components/dialog/page-dialog.component";
import { COMMA, ENTER } from "@angular/cdk/keycodes";
import { MatChipInputEvent } from "@angular/material/chips";
import { AppContext } from "../../../components/app-context/app.context";
import { Content, Selectors } from "../../../components/app-context/api";
import { filter } from "rxjs/operators";
import { ValidationService } from "../../../components/validation/validation.service";
import { DateTimeAdapter } from "ng-pick-datetime";
import { WidgetForPipe } from "../../../widgets/container/widget-for.pipe";
import { DialogService } from "../../../components/dialog";

declare var contextPath: string;

@Component({
  selector: "nm-dynamic-form-fields-component",
  templateUrl: "./dynamic-form-fields.component.html",
  styleUrls: ["./dynamic-form-fields.component.scss"],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DynamicFormFieldsComponent implements OnDestroy {
  public unsubscribe = NgUnsubscribe.create();

  public editorConfig: any = DEFAULT_TEXT_EDITOR_CONFIG;

  public lookupOptions = {};
  public dialogSelectedItems = {};
  public value = {};
  public description = {};
  public passwordConfirmation = {};
  public sourceCodeValidations = {};

  public menuActions: { [key: string]: Observable<Content[]> } = {};
  public buttonActions: { [key: string]: Observable<Content[]> } = {};

  public resetDateValue = new Subject<any>();
  public showPassword: boolean = false;
  public separatorKeysCodes: number[] = [ENTER, COMMA];
  public currentLocale: any;

  /**
   * Sets dynamic form fields.
   */
  @Input()
  public fields: DynamicFormField[];

  /**
   * Sets dynamic form configurations.
   */
  @Input()
  public configuration: DynamicFormConfiguration;

  /**
   * Sets lookup options for lookup types.
   */
  @Input("lookupOptions")
  public set lookupOptionsInput(value) {
    angularWidgetBridgeInput(
      value,
      this.lookupOptionsInputChannel,
      this.unsubscribe
    );
  }

  /**
   * Resets field lookup options.
   */
  @Input("resetLookup")
  public set resetLookup(value) {
    angularWidgetBridgeInput(value, this.resetLookupChannel, this.unsubscribe);
  }

  /**
   * Sets field value.
   */
  @Input("valueSetter")
  public set valueSetter(value) {
    angularWidgetBridgeInput(value, this.valueSetterChannel, this.unsubscribe);
  }

  /**
   * Sets field label.
   */
  @Input("labelSetter")
  public set labelSetter(value) {
    angularWidgetBridgeInput(value, this.labelSetterChannel, this.unsubscribe);
  }

  /**
   * Enables\Disables field.
   */
  @Input("setDisabled")
  public set setDisabled(value) {
    angularWidgetBridgeInput(value, this.setDisabledChannel, this.unsubscribe);
  }

  /**
   * Sets dialog context for field used with page-dialog type fields.
   */
  @Input("dialogContext")
  public set dialogContext(value) {
    angularWidgetBridgeInput(
      value,
      this.dialogContextChannel,
      this.unsubscribe
    );
  }

  /**
   * Resets all form fields.
   */
  @Input("resetFormFields")
  public set resetFormFields(value) {
    angularWidgetBridgeInput(
      value,
      this.resetFormFieldsChannel,
      this.unsubscribe
    );
  }

  /**
   * Shows\Hides field.
   */
  @Input("setHidden")
  public set setHidden(value) {
    angularWidgetBridgeInput(value, this.setHiddenChannel, this.unsubscribe);
  }

  /**
   * Resets all form fields.
   */
  @Input("reset")
  public set reset(value) {
    angularWidgetBridgeInput(value, this.resetChannel, this.unsubscribe);
  }

  /**
   * Reloads field lookup options.
   */
  @Input("reloadLookup")
  public set reloadLookup(value) {
    angularWidgetBridgeInput(value, this.reloadLookupChannel, this.unsubscribe);
  }

  /**
   * Adds fields dynamically to the form.
   */
  @Input("addFields")
  public set addFields(value) {
    angularWidgetBridgeInput(value, this.addFieldsChannel, this.unsubscribe);
  }

  /**
   * Removes fields dynamically from the form.
   */
  @Input("removeFields")
  public set removeFields(value) {
    angularWidgetBridgeInput(value, this.removeFieldsChannel, this.unsubscribe);
  }

  /**
   * Removes all fields dynamically from the form.
   */
  @Input("removeAllFields")
  public set removeAllFields(value) {
    angularWidgetBridgeInput(
      value,
      this.removeAllFieldsChannel,
      this.unsubscribe
    );
  }

  /**
   * Triggers validation for form fields.
   */
  @Input("triggerValidation")
  public set triggerValidation(value) {
    angularWidgetBridgeInput(
      value,
      this.triggerValidationChannel,
      this.unsubscribe
    );
  }

  @ViewChild("datepicker", { static: false })
  datePicker: OwlDateTimeComponent<Date>;
  @ViewChild("datetimepicker", { static: false })
  dateTimePicker: OwlDateTimeComponent<any>;

  public lookupOptionsInputChannel = new Subject<any>();
  public resetLookupChannel = new Subject<string>();
  public reloadLookupChannel = new Subject<string>();
  public valueSetterChannel = new Subject<{ field: string; value: any }>();
  public labelSetterChannel = new Subject<{ field: string; value: any }>();
  public setDisabledChannel = new Subject<{ field: string; value: boolean }>();
  public dialogContextChannel = new Subject<{ field: string; context: any }>();
  public resetFormFieldsChannel = new Subject<any>();
  public setHiddenChannel = new Subject<{ field: string; value: boolean }>();
  public resetChannel = new Subject<any>();
  public addFieldsChannel = new Subject<DynamicFormField[]>();
  public removeFieldsChannel = new Subject<string[]>();
  public removeAllFieldsChannel = new Subject<any>();
  public triggerValidationChannel = new Subject();

  /**
   * Emits when any field value changed.
   */
  @Output("value")
  public valueOutputEmitter = new EventEmitter<any>();

  /**
   * Emits validation result when any field value changed.
   */
  @Output("valid")
  public validOutputEmitter = new EventEmitter<any>();

  /**
   * Emits when any field is clicked.
   */
  @Output("onClick")
  public clickOutputEmitter = new EventEmitter<any>();

  /**
   * Emits when enter is pressed.
   */
  @Output("onEnterPressed")
  public enterPressedOutputEmitter = new EventEmitter<any>();

  /**
   * Emits when initialization is finished for all fields and emits also before form destroy.
   */
  @Output("init")
  public initOutputEmitter = new EventEmitter<any>();

  /**
   * Emits the dropped value when using drag and drop featuer.
   */
  @Output("droppedValue")
  public droppedValueEmitter = new EventEmitter<any>();

  constructor(
    protected cdr: ChangeDetectorRef,
    protected widgetFrameService: WidgetframeService,
    protected currentLocaleService: CurrentLocaleService,
    protected dateAdapter: DateAdapter<Date>,
    protected owldateTimeAdapter: DateTimeAdapter<any>,
    protected translateService: TranslateService,
    protected owlDateTimeIntl: OwlDateTimeIntl,
    protected dialogService: DialogService,
    protected appContext: AppContext,
    protected validationService: ValidationService
  ) {}

  public ngOnInit() {
    if (!this.fields) {
      return;
    }

    this.initFields(this.fields);

    this.reloadLookupChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((identifier) => {
        let field = this.fields.find((entry) => entry.field === identifier);
        if (!field) {
          field = this.fields.find((entry) => entry.type === "multi-toggler");
          if (field) {
            field = field.items.find((item) => item.field === identifier);
          }
          if (!field) {
            console.error(
              `[ReloadLookup] Cant find field with identifier ${identifier}`
            );
            return;
          }
        }
        this.loadLookupData(field);
      });

    this.addFieldsChannel
      .pipe(
        withLatestFrom(this.currentLocaleService.getCurrentLocale()),
        takeUntil(this.unsubscribe)
      )
      .subscribe((data) => {
        const fields = data[0];
        const locale = data[1];
        this.fields.push(...fields);
        this.initFields(fields);
        this.updateLocale(locale, fields);
        this.cdr.markForCheck();
      });

    this.valueSetterChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        if (!this.value) {
          this.value = {};
        }

        const field = this.findField(data.field);
        if (!field) {
          return;
        }

        this.setFieldDescription(field, data.value);
        this.value[field.field] = data.value;
        this.onValueChange(field, false);
        this.cdr.markForCheck();
      });

    this.labelSetterChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        const field = this.findField(data.field);
        if (!field) {
          return;
        }

        if (field.label != null) {
          field.label = data.value;
        }

        this.cdr.markForCheck();
      });

    this.setDisabledChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        const field = this.findField(data.field);
        if (!field) {
          return;
        }

        field.disabled = data.value;
        this.cdr.markForCheck();
      });

    this.dialogContextChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        const field = this.findField(data.field);
        if (!field) {
          return;
        }

        field.dialogContext = data.context;
        this.cdr.markForCheck();
      });

    this.resetFormFieldsChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        this.fields.forEach((field) => {
          if (field.type === "category-selector") {
            field.includeSubCategories = false;
            this.cdr.markForCheck();
          }
        });
        Object.keys(this.value).forEach((field) => {
          this.value[field] = null;
          this.description[field] = null;
          this.cdr.markForCheck();
          this.resetDateValue.next(true);
        });
        this.dialogSelectedItems = {};
      });

    this.setHiddenChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        const field = this.fields.find((entry) => entry.field === data.field);
        if (!field) {
          console.error("Unable to find field " + data.field);
          return;
        }
        field.hidden = data.value;

        this.cdr.markForCheck();
      });

    this.lookupOptionsInputChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        this.setLookupOptions(data.field, data.value);
      });

    this.resetChannel.pipe(takeUntil(this.unsubscribe)).subscribe((reset) => {
      this.value = {};
      this.dialogSelectedItems = {};
      this.description = {};
      this.cdr.markForCheck();
    });

    this.resetLookupChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        this.lookupOptions[data] = [];
        this.value[data] = null;
        this.cdr.markForCheck();
      });

    this.removeFieldsChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((fields) => {
        this.fields = this.fields.filter((f) => fields.indexOf(f.field) === -1);
        fields.forEach((f) => delete this.value[f]);
        this.cdr.markForCheck();
      });

    this.removeAllFieldsChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        this.fields = [];
        this.value = {};
        this.description = {};
        this.cdr.markForCheck();
      });

    this.triggerValidationChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => this.onTriggerValidation());

    this.currentLocaleService
      .getCurrentLocale()
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((locale) => {
        this.updateLocale(locale, this.fields);
      });

    this.initOutputEmitter.next({ init: true });
  }

  private findField(identifier: string) {
    let field = this.fields.find((entry) => entry.field === identifier);
    if (!field) {
      field = this.fields.find((entry) => entry.type === "multi-toggler");
      if (field) {
        field = field.items.find((item) => item.field === identifier);
      }
      if (!field) {
        console.error(
          `Unable to find field with identifier ${identifier} `,
          this.fields
        );
        return;
      }
    }

    return field;
  }

  private initFields(fields: any[]) {
    this.fields.sort((a, b) => {
      if (!a.order) {
        a.order = 0;
      }
      if (!b.order) {
        b.order = 0;
      }
      return a.order - b.order;
    });

    fields.forEach((field) => {
      if (field.chipRemovable === undefined || field.chipRemovable === null) {
        field.chipRemovable = true;
      }

      if (field.clearable == null) {
        field.clearable = true;
      }

      if (field.type === "date") {
        if (!field.dateFormat) {
          field.dateFormat = "dd.mm.yyyy";
        }
      } else if (field.type === "text") {
        if (field.height) {
          //This will only work if all editors are supposed to have the same height..
          this.editorConfig = Object.assign(
            { height: field.height },
            this.editorConfig
          );
        }
      } else if (
        field.type === "lookup" ||
        field.type === "multi-lookup" ||
        field.type === "context"
      ) {
        this.loadLookupData(field);
      } else if (field.type === "category-selector") {
        if (field.publication) {
          this.value[field.field] = {
            publication: field.publication,
          };
        }
      } else if (field.type === "boolean-indeterminate") {
        this.value[field.field] = field.defaultValue;
      } else if (field.type === "password-confirmation") {
        if (field.hideConfirmationField === undefined) {
          field.hideConfirmationField = true;
        }
      } else if (field.type === "radio") {
        this.loadLookupData(field);
      } else if (field.type === "source-code-editor") {
        this.value[field.field] = field.defaultValue;
      }

      field.showHint = !field.hintOnFocus;

      this.setFieldDefaultValue(field);
      this.onValueChange(field, false);
    });

    this.currentLocaleService.getCurrentLocale().subscribe((locale) => {
      this.currentLocale = locale;
    });
  }

  private setFieldDefaultValue(field: DynamicFormField) {
    if (!field.defaultValue) {
      return;
    }

    this.value[field.field] = field.defaultValue;
  }

  private setFieldDescription(field: DynamicFormField, value: any) {
    if (!field) {
      return;
    }

    if (field.type === "user-right") {
      this.description[field.field] = value
        ? value.identifier + " (" + value.pimRef + ")"
        : null;
    } else if (field.type === "worklist") {
      this.description[field.field] = value ? value.name : null;
    } else if (field.type === "page-dialog") {
      this.description[field.field] = value
        ? value[field.dialogDescription]
        : null;
    }
  }

  private setLookupOptions(field, value) {
    this.lookupOptions[field] = value;
    this.cdr.markForCheck();
  }

  public getClass(field: DynamicFormField) {
    return String(field.field);
  }

  public getIdentifierField(field: DynamicFormField) {
    if (field.identifierField) {
      return field.identifierField;
    }
    return "identifier";
  }

  public getDescriptionField(field: DynamicFormField) {
    if (field.descriptionField) {
      return field.descriptionField;
    }
    return "description";
  }

  public loadLookupData(field: DynamicFormField) {
    if (field.lookupUrl) {
      this.widgetFrameService.getData(field.lookupUrl).subscribe((data) => {
        if (data._embedded) {
          if (data.type) {
            data = data._embedded[data.type];
          } else {
            const key = Object.keys(data._embedded)[0];
            data = data._embedded[key];
          }
        }
        if (field.lookupAllowEmpty) {
          const emptyEntry = {};
          emptyEntry[this.getIdentifierField(field)] = "";
          emptyEntry[this.getDescriptionField(field)] = "";
          data.unshift(emptyEntry);
        }
        this.setLookupOptions(field.field, data);
      });
    } else if (field.lookupOptions) {
      this.setLookupOptions(field.field, field.lookupOptions);
    }
  }

  ngOnDestroy(): void {
    this.initOutputEmitter.next({ init: false, value: this.value });
    this.unsubscribe.destroy();
  }

  onValueChange(field: DynamicFormField, isEdit: boolean = true) {
    this.onTriggerValidation();

    this.valueOutputEmitter.next({
      value: this.value,
      lastChange: field.field,
      isEdit: isEdit,
    });
  }

  onTriggerValidation() {
    const valid = this.validateFields();
    this.validOutputEmitter.next(valid);
  }

  onEditorValueChange(field: DynamicFormField, value: any) {
    this.value[field.field] = value.value;
    this.sourceCodeValidations[field.field] = value.isValid;
    this.onValueChange(field);
  }

  private validateFields(): boolean {
    this.validationService.validateDynamicFormFields(this.fields, this.value);

    let valid = true;
    for (const field of this.fields) {
      // the validation service updates the errors to field.errors
      // if field.errors is undefined or has no entries, the field is valid
      if (Array.isArray(field.errors) && field.errors.length > 0) {
        valid = false;
        break;
      }

      if (!field.required) {
        continue;
      }

      const value = this.value[field.field];

      if (
        value === undefined ||
        value === null ||
        value === "" ||
        value.length === 0
      ) {
        valid = false;
        break;
      }

      if (!this.validateLocalizedText(field)) {
        valid = false;
        break;
      }

      if (!this.validateCategorySelector(field)) {
        valid = false;
        break;
      }

      if (!this.validatePasswords(field)) {
        valid = false;
        break;
      }

      if (!this.validateSourceCodeEditor(field)) {
        valid = false;
        break;
      }
    }

    return valid;
  }

  private validateSourceCodeEditor(field) {
    if (field.type !== "source-code-editor") {
      return true;
    }
    return this.sourceCodeValidations[field.field];
  }

  private validateLocalizedText(field: DynamicFormField): boolean {
    if (!field.localizedText) {
      return true;
    }

    const value = this.value[field.field];

    let validLocalizedText = false;
    Object.keys(value).forEach((locale) => {
      if (locale && value[locale].trim()) {
        validLocalizedText = true;
      }
    });

    return validLocalizedText;
  }

  private validateCategorySelector(field: DynamicFormField): boolean {
    if (field.type !== "category-selector") {
      return true;
    }

    const value = this.value[field.field];

    if (!value.publication || !value.category || value.category.length === 0) {
      return false;
    }

    return true;
  }

  private validatePasswords(field: DynamicFormField): boolean {
    if (field.type !== "password") {
      return true;
    }

    const password = this.value[field.field];
    const confirmPassword = this.passwordConfirmation[field.field];

    if (!password || !confirmPassword) {
      return false;
    }

    return password === confirmPassword;
  }

  onClick(event, field: DynamicFormField) {
    event.stopPropagation();
    this.clickOutputEmitter.next(field);
  }

  onEnterPressed(event) {
    if (event.target) {
      // due to ngModelOptions: force blur event to trigger setting of asset-search´s formValue
      event.target.blur();
      this.enterPressedOutputEmitter.next(event.target.value);
    }
  }

  onKeyPressedDatePicker(event) {
    const key = event.key.toLowerCase();
    switch (key) {
      case "space":
      case "spacebar":
      case " ":
      case "enter":
      case "down":
      case "arrowdown":
        this.datePicker.open();
        this.onEnterPressed(event);
    }
  }

  onKeyPressedDateTimePicker(event) {
    const key = event.key.toLowerCase();
    switch (key) {
      case "space":
      case "spacebar":
      case " ":
      case "enter":
      case "down":
      case "arrowdown":
        this.dateTimePicker.open();
        this.onEnterPressed(event);
    }
  }

  public onExternalModelChange(field: DynamicFormField, value: any) {
    this.value[field.field] = value;
    this.onValueChange(field);
  }

  public openSelectDialog(field: DynamicFormField) {
    const dialogConfig = {
      minWidth: "900px",
      minHeight: "750px",
      data: {
        title: field.label,
        itemsUrl: field.lookupUrl,
        itemType: field.selectDialogItemType,
        itemTypeId: this.getIdentifierField(field),
        itemTypeDescription: this.getDescriptionField(field),
        additionalProperties: field.additionalProperties
          ? field.additionalProperties
          : [],
        preselectedItems: this.dialogSelectedItems[field.field],
        excludePreselected: false,
        allowNoSelection: true,
      },
    };

    let dialogRef = this.dialogService.open(
      SelectItemsDialogComponent,
      dialogConfig
    );

    dialogRef.afterClosed().subscribe((selectedItems) => {
      if (!selectedItems) {
        return;
      }
      if (selectedItems.length === 0) {
        this.value[field.field] = null;
        this.description[field.field] = null;
        this.dialogSelectedItems[field.field] = null;
      } else if (selectedItems.length > 0) {
        this.dialogSelectedItems[field.field] = selectedItems;
        let values = [];
        let descriptions = [];
        selectedItems.forEach((item) => {
          values.push(item[this.getIdentifierField(field)]);
          descriptions.push(item[this.getDescriptionField(field)]);
        });
        this.value[field.field] = values.toString();
        this.description[field.field] = descriptions.join(", ");
        this.onValueChange(field);
      }
      this.cdr.markForCheck();
    });
  }

  public openUserRightDialog(field: DynamicFormField) {
    const context = { userRightsUrl: field.url };
    this.pageDialog(field, "core", "select-userRight", context, "650px");
  }

  public openWorklistDialog(field: DynamicFormField) {
    this.pageDialog(field, "core", "select-worklist", null, "650px");
  }

  public openPageDialog(field: DynamicFormField) {
    this.pageDialog(
      field,
      field.dialogModule,
      field.dialogIdentifier,
      field.dialogContext,
      "900px"
    );
  }

  private pageDialog(
    field: DynamicFormField,
    module: string,
    identifier: string,
    context: any,
    minWidth: string
  ) {
    if (field.disabled) {
      return;
    }

    const dialogRef = this.dialogService.open(PageDialogComponent, {
      minWidth: minWidth,
      height: "700px",
      data: {
        title: field.label,
        acceptEnabled: true,
        customAcceptIcon: true,
        acceptIcon: "check",
        acceptText: "button.accept",
        cancelEnabled: true,
        cancelText: "button.cancel",
        module: module,
        identifier: identifier,
        context: context,
      },
    });

    dialogRef.afterClosed().subscribe((data) => {
      if (!data) {
        return;
      }

      this.setFieldDescription(field, data);
      this.value[field.field] = data;
      this.onValueChange(field);
      this.cdr.markForCheck();
    });
  }

  public clearField(field: DynamicFormField) {
    this.value[field.field] = null;
    this.description[field.field] = null;
    this.dialogSelectedItems[field.field] = null;

    this.onValueChange(field);
    this.cdr.markForCheck();
  }

  private updateLocale(locale: string, fields: DynamicFormField[]) {
    fields.forEach((field) => {
      if (field.translations) {
        const translations = field.translations;
        if (translations[locale]) {
          field.label = translations[locale];
        }
      }
      if (field.hintTranslations) {
        const translations = field.hintTranslations;
        if (translations[locale]) {
          field.hint = translations[locale];
        }
      }
    });
    this.dateAdapter.setLocale(locale);
    this.owldateTimeAdapter.setLocale(locale);
    this.owlDateTimeIntl.cancelBtnLabel =
      this.translateService.instant("button.cancel");
    this.owlDateTimeIntl.setBtnLabel =
      this.translateService.instant("button.accept");
  }

  public includeSubCategoriesValueChanged(field: DynamicFormField, value: any) {
    if (this.value[field.field]) {
      this.value[field.field].includeSubCategories = value;
    } else {
      this.value[field.field] = { includeSubCategories: value };
    }
    field.includeSubCategories = value;
  }

  public getCategoryUrl(field: DynamicFormField) {
    return getOrDefault(field.categoryUrl, "");
  }

  public booleanValueChanged(field: DynamicFormField) {
    switch (this.value[field.field]) {
      case true: {
        this.value[field.field] = false;
        break;
      }
      case false: {
        this.value[field.field] = null;
        break;
      }
      case undefined:
      case null: {
        this.value[field.field] = true;
        break;
      }
    }

    this.onValueChange(field);
  }

  addChip(event: MatChipInputEvent, field: DynamicFormField): void {
    const input = event.input;
    const value = event.value;

    if ((value || "").trim()) {
      if (this.value[field.field]) {
        this.value[field.field].push({
          identifier: value.trim(),
          description: value.trim(),
        });
      } else {
        this.value[field.field] = [
          { identifier: value.trim(), description: value.trim() },
        ];
      }

      this.onValueChange(field);
    }

    if (input) {
      input.value = "";
    }
  }

  removeChip(chip: any, field: DynamicFormField): void {
    if (!this.value[field.field]) {
      return;
    }

    const index = this.value[field.field].indexOf(chip);

    if (index >= 0) {
      this.value[field.field].splice(index, 1);
      this.onValueChange(field);
    }
  }

  public onItemDropped(ev, field) {
    const dropValueIdentifier = getOrDefault(
      field.dropValueIdentifier,
      "identifier"
    );
    const myValue = ev.dragData.source[dropValueIdentifier];
    this.droppedValueEmitter.emit(myValue);
    const cursorPosition = ev.owner.element.nativeElement.selectionStart;

    if (this.value[field.field]) {
      var firstHalf = this.value[field.field].substring(0, cursorPosition);
      if (firstHalf) {
        firstHalf += " ";
      }
      var secondHalf = this.value[field.field].substring(
        cursorPosition,
        this.value[field.field].length
      );
      if (secondHalf) {
        secondHalf = " " + secondHalf;
      }

      this.value[field.field] = firstHalf + myValue + secondHalf;
    } else {
      this.value[field.field] = myValue;
    }
    this.onValueChange(field);
  }

  public onFileUpload(field, uploadedFile: any) {
    if (uploadedFile) {
      this.value[field.field] = uploadedFile;
    } else {
      this.value[field.field] = null;
    }
    this.onValueChange(field);
  }

  public changePassword(field) {
    field.hideConfirmationField = !field.hideConfirmationField;
    field.required = !field.required;
    field.disabled = !field.disabled;
    if (field.hideConfirmationField) {
      this.value[field.field] = null;
      this.passwordConfirmation[field.field] = null;
    }
    this.onValueChange(field);
    this.clickOutputEmitter.next(field);
  }

  public getLocalizedValue(field: DynamicFormField): string {
    if (!field || this.value[field.field] == null) {
      return "";
    }

    if (field.localizedText) {
      return this.value[field.field][this.currentLocale];
    }
    return this.value[field.field];
  }

  private initializeMenuActions(field: DynamicFormField) {
    if (field.menuInteractions) {
      this.menuActions[field.field] = this.appContext.browserContext
        .subscribe(field.menuInteractions)
        .pipe(
          takeUntil(this.unsubscribe),
          filter((contents) => !!contents),
          map((contents) => {
            const interactions = contents
              .filter(
                (entry) =>
                  entry.showAsButton == null || entry.showAsButton === false
              )
              .sort((l, r) => (r.order || 0) - (l.order || 0));
            return interactions;
          })
        );
    } else {
      this.menuActions[field.field] = EMPTY;
    }
  }

  private initializeButtonActions(field: DynamicFormField) {
    if (field.menuInteractions) {
      this.buttonActions[field.field] = this.appContext.browserContext
        .subscribe(field.menuInteractions)
        .pipe(
          takeUntil(this.unsubscribe),
          filter((contents) => !!contents),
          map((contents) => {
            return contents
              .filter((entry) => entry?.showAsButton === true)
              .sort((l, r) => (r.order || 0) - (l.order || 0));
          })
        );
    } else {
      this.buttonActions[field.field] = EMPTY;
    }
  }

  public getMenuActions(field: DynamicFormField): Observable<Content[]> {
    if (this.menuActions[field.field]) {
      return this.menuActions[field.field];
    }

    if (field.menuInteractions) {
      this.initializeMenuActions(field);
    } else {
      this.menuActions[field.field] = EMPTY;
    }
    return this.menuActions[field.field];
  }

  public getButtonActions(field: DynamicFormField): Observable<Content[]> {
    if (this.buttonActions[field.field]) {
      return this.buttonActions[field.field];
    }

    if (field.menuInteractions) {
      this.initializeButtonActions(field);
    } else {
      this.buttonActions[field.field] = EMPTY;
    }

    return this.buttonActions[field.field];
  }

  public getInteractions(
    field: DynamicFormField
  ): { menu: Observable<Content[]>; buttons: Observable<Content[]> } | null {
    if (field.menuInteractions == null) {
      return null;
    }

    return {
      menu: this.getMenuActions(field),
      buttons: this.getButtonActions(field),
    };
  }

  onKeyPressed(event: KeyboardEvent, field) {
    if (field.tab && event.code !== "Tab") {
      event.preventDefault();
      return false;
    }
    return;
  }

  onFocusOut(field: DynamicFormField) {
    field.showHint = !field.hintOnFocus;
  }

  onFocusIn(field: DynamicFormField) {
    field.showHint = true;
  }

  getPreferredPosition(field: DynamicFormField) {
    return field.additionalProperties &&
      field.additionalProperties.preferredPosition
      ? field.additionalProperties.preferredPosition
      : ["right-below", "right", "bottom"];
  }

  resetDateTime(field, event) {
    event.stopPropagation();
    this.value[field.field] = null;
    this.onValueChange(field);
  }

  handlePaste(event: ClipboardEvent) {
    stripPasteContent(event);
  }
}

export interface DynamicFormConfiguration extends BaseConfiguration {
  /**
   * Array of dynamic form fields to be added in the form.
   */
  fields: DynamicFormField[];
}

export interface DynamicFormField {
  /**
   * Type of the field. supported types are: boolean, boolean-indeterminate, category-selector,
   * chip-list, date, date-time, file-upload, heading, lookup, multi-lookup,  number,
   * number-decimal, password, plain, select-dialog, text, textarea, toggler, multi-toggler, user-right, worklist, source-code-editor
   */
  type: string;

  /**
   * Unique field identifier.
   */
  field: string;

  /**
   * collection of all data for multi-components
   */
  items?: DynamicFormField[];

  /**
   * Localized key for field label
   */
  label: string;

  /**
   * URL to load lookup options from. used with these types: lookup, multi-lookup, select-dialog.
   * (See also lookupOptions)
   */
  lookupUrl?: string;

  /**
   * Array of static lookup options. used with these types: lookup, multi-lookup, select-dialog.
   * (See also lookupUrl)
   */
  lookupOptions?: any[];

  /**
   * Allows empty selection for lookup types. @default(false)
   */
  lookupAllowEmpty?: boolean;

  /**
   * Item type used for select-dialog type.
   */
  selectDialogItemType?: string;

  /**
   * Enables\Disables the field.
   */
  disabled?: boolean;

  /**
   * Makes the field required or optional.
   */
  required?: boolean;

  /**
   * Identifier field of the object. it is used with 'lookup' and 'multi-lookup' types
   * to define identifier field. @default(identifier)
   */
  identifierField?: string;

  /**
   * Description field of the object. it is used with 'lookup' and 'multi-lookup' types
   * to define description field. @default(description)
   */
  descriptionField?: string;

  /**
   * Field height. used with 'text' type.
   */
  height?: number;

  /**
   * Hides or shows the field.
   */
  hidden?: boolean;

  /**
   * Order of the field in the form. @default(0)
   */
  order?: number;

  /**
   * If not set the version select lookup will be shown,
   * otherwise the provided version will be used and the category select lookup only will be shown.
   * Used with type 'category-selector'.
   */
  publication?: string;

  /**
   * Allows multiple category selection. Used with type 'category-selector'. @default(false)
   */
  multiCategorySelect?: boolean;

  /**
   * URL used to load version from.
   * default is 'category-select-publications' for publications
   * and 'category-select-asset-trees' for asset-trees
   * Used with type 'category-selector'.
   */
  categoryUrl?: string;

  /**
   * Label translation in other locales.
   */
  translations?: any;

  /**
   * hint translation in other locales.
   */
  hintTranslations?: any;

  /**
   * Validations to be applied on field input.
   */
  validations?: DataValidation[];

  /**
   * Date format. used with 'date' type. @default(dd.mm.yyyy)
   */
  dateFormat?: string;

  /**
   * Shows\Hides include sub-categories checkbox. used with 'category-selector' type. @default(false)
   */
  showIncludeSubCategories?: boolean;

  /**
   * Includes sub-categories value. used with 'category-selector' type.
   */
  includeSubCategories?: boolean;

  /**
   * Applies channel types filter on versions. supported values are PURCHASE, SALES, ARCHIVE.
   * used with 'category-selector' type.
   */
  filterChannelTypes?: string[];

  /**
   * Array of additional properties to view in select dialog list. used with 'select-dialog' type.
   */
  additionalProperties?: any;

  /**
   * List folder type. used with 'worklist' type.
   */
  listfolderType?: string;

  /**
   * List folder data type. used with 'worklist' type.
   */
  dataType?: string;

  /**
   * Shows floating label for the field. @default(false)
   */
  floatingLabel?: boolean;

  /**
   * The target tab. used in dynamic forms with mutliple tabs.
   * supported tabs are localizedtext, listFolder, userRights
   */
  tab?: string;

  /**
   * This property shouldn't be added in configuration. It is only used internally.
   */
  value?: any;

  /**
   * Initial value of the field.
   */
  defaultValue?: any;

  /**
   * Makes the field Readonly or editable. @default(false)
   */
  readonly?: boolean;

  /**
   * Sets autosize property for type 'textarea'.
   */
  autosize?: boolean;

  /**
   * Shows clear selection button. used with 'lookup' and 'multi-lookup' types. @default(true)
   */
  clearable?: boolean;

  /**
   * Sets maximum characters length for text types.
   */
  maxLength?: number;

  /**
   * URL to load user rights. used with 'user-right' type.
   */
  url?: string;

  /**
   * Comma-separated list of supported file types. used with 'file-upload' type.
   */
  supportedFileTypes?: string;

  /**
   * File upload url. used with 'file-upload type.
   */
  fileUploadUrl?: string;

  /**
   * Sets field as localized text. @default(false)
   */
  localizedText?: boolean;

  /**
   * Identifiers of the interactions that are supposed to be loaded for the menu.
   */
  menuInteractions?: Selectors;

  /**
   * List of field validation errors.
   */
  errors?: ValidationError[];

  /**
   * Array of supported MIME types. used with 'file-upload' type.
   */
  supportedMimeTypes?: string[];

  /**
   * Makes chips removable in 'chip-list' type. @default(true)
   */
  chipRemovable?: boolean;

  /**
   * Shows\Hides edit password button. used with 'password' type. @default(false)
   */
  showEditpasswordButton?: boolean;

  /**
   * Hides\Shows second password confirmation field. used with 'password' type. @default(true)
   */
  hideConfirmationField?: boolean;

  /**
   * Localized key for password confirmation lable. used with 'password' type.
   */
  confirmationLabel?: string;

  /**
   * Auto focus field.
   */
  autofocus?: boolean;

  /**
   * Localized key for field hint.
   */
  hint?: string;

  /**
   * Shows hint on focus.
   */
  hintOnFocus?: boolean;

  /**
   * This property shouldn't be added in configuration. It is only used internally.
   */
  showHint?: boolean;

  /**
   * Identifier field that will be used to get the value while dropping it in the textarea field.
   */
  dropValueIdentifier?: string;

  /**
   * min rows for textarea field
   */
  autosizeMinRows?: string;

  /**
   * Source code programming language. It is used with type = 'source-code-editor'.
   */
  sourceCodeLanguage?: string;

  /**
   * Dialog module for page-dialog type.
   */
  dialogModule?: string;

  /**
   * Dialog identifier for page-dialog type.
   */
  dialogIdentifier?: string;

  /**
   * Dialog data for page-dialog type.
   */
  dialogContext?: any;

  /**
   * Dialog data property used to set the dialog description when closed for page-dialog type.
   */
  dialogDescription?: any;

  /**
   * Enable/Disable editor minimap for source-code-editor type.
   */
  enableMinimap?: boolean;

  /**
   * Triggers the display of Tiny editor implmentation in its onInit callback
   */
  showEditorOnInit?: boolean;
}

results matching ""

    No results matching ""