/* * © 2017 Stratio Big Data Inc., Sucursal en España. * * This software is licensed under the Apache License, Version 2.0. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the terms of the License for more details. * * SPDX-License-Identifier: Apache-2.0. */ import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, forwardRef, Input, OnDestroy, Output, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms'; import { Subscription } from 'rxjs'; import { JSONSchema4 } from 'json-schema'; import { StInputError } from '../st-input/st-input.error.model'; import { StFormSchema } from './st-form.model'; import { FORM_UI_COMPONENT, StFormFieldTranslations } from './st-form-field/st-form-field.interface'; /** * @description {Component} [Dynamic form] * * The form component allows to generate forms dynamically using a JSON schema. * * @model * * [Form Schema] {./st-form.model.ts#StFormSchema} * [Form UI Definition] {./st-form.model.ts#StFormUIDefinition} * * @example * * {html} * * ``` * * * * ``` * */ @Component({ selector: 'st-form', templateUrl: './st-form.component.html', styleUrls: ['./st-form.component.scss'], host: { class: 'st-form' }, changeDetection: ChangeDetectionStrategy.OnPush, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StFormComponent), multi: true }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => StFormComponent), multi: true } ] }) export class StFormComponent implements AfterViewInit, AfterViewChecked, ControlValueAccessor, OnDestroy { /** @Input {string} [parentName=] Name of the parent section. By default, it is undefined */ @Input() parentName: string; /** @Input {string} [nestingLevel=0] This informs about the nesting level of the form. This input is only used for design purposes */ @Input() nestingLevel: number = 0; /** @Input {boolean} [forceValidations=] Boolean to force the field validations */ @Input() forceValidations: boolean; /** @Input {string} [disabledSectionMessage='for this instance is disabled.'] * Message displayed when a section is disabled. This is always displayed after the section name */ @Input() disabledSectionMessage: string = 'for this instance is disabled.'; /** @Input {number} [sectionDescriptionLevel=-1] Level of the section to which description is displayed. * By default, section descriptions are not displayed. */ @Input() sectionDescriptionLevel: number = -1; /** @Input {number} [textFieldMaxWidth=] Maximum width of a field needed to paint a input or textarea */ @Input() textFieldMaxWidth: number; /** @Input {StInputError} [errorMessages=] Field error translations */ @Input() errorMessages: StInputError; /** @Input {boolean} [showTooltips=-1] Enable or disable displaying of tooltips * By default, tooltips are displayed */ @Input() showTooltips: boolean = true; /** @Input {StFormFieldTranslations} [translations=] Field translations */ @Input() translations?: StFormFieldTranslations; /** @Output {any} [valueChange=] Event emitted when value is changed. This emits the current form value */ @Output() valueChange: EventEmitter = new EventEmitter(); /** @Output {string} [clickLink=] Event emitted when link is clicked. It returns the field path */ @Output() clickLink: EventEmitter = new EventEmitter(); @ViewChild('form', { static: false }) form: NgForm; public showCollapsedSectionFields: boolean = false; public innerValue: any = {}; private _value: any = {}; private _parentFieldSubscription: Subscription[] = []; private _parentFields: string[]; private _schema: StFormSchema; /** @Input {StFormSchema [schema=] Form schema needed to generate the form */ @Input() get schema(): StFormSchema { return this._schema; } set schema(schema: StFormSchema) { this._schema = schema; this._value = {}; if (this._schema.dependencies) { this._parentFields = Object.keys(this._schema.dependencies); } } ngAfterViewInit(): void { if (!this.forceValidations) { this.form.form.markAsPristine(); } } ngAfterViewChecked(): void { if (this._parentFields && this.form.control && this.form.control.controls) { for (let i = 0; i < this._parentFields.length; ++i) { let parentField: string = this._parentFields[i]; if (!this._parentFieldSubscription[i] && this.form.control.controls[parentField]) { this._parentFieldSubscription[i] = this.form.control.controls[this._parentFields[i]].valueChanges.subscribe((value) => { if (!value) { let childrenFields: JSONSchema4 | string[] = this.schema.dependencies[parentField]; for (let j = 0; j < childrenFields.length; ++j) { if (this.form.controls[childrenFields[j]]) { this._value[childrenFields[j]] = undefined; } } } }); } } } } // Function to call when the value changes. onChange(_: any): void { } onTouched = () => { } validate(control: FormControl): any { let errors: any = null; if (this.form) { Object.keys(this.form.controls).forEach((propertyName) => { if (this.form.controls[propertyName] && this.form.controls[propertyName].errors) { if (!errors) { errors = {}; } errors[propertyName] = this.form.controls[propertyName].errors; } }); this.form.control.setErrors(errors); } return errors; } isRequired(propertyName: string): boolean { return propertyName && this.schema.required && this.schema.required.indexOf(propertyName) !== -1; } isCollapsedSection(): boolean { return this.schema.type === 'object' && this.schema.ui && (this.schema.ui.component === FORM_UI_COMPONENT.SHOW_MORE || this.schema.ui.component === FORM_UI_COMPONENT.ACCORDION); } iShowMoreSection(): boolean { return this.schema.type === 'object' && this.schema.ui && this.schema.ui.component === FORM_UI_COMPONENT.SHOW_MORE; } isAParentField(propertyName: string): boolean { return this.schema.dependencies && this.schema.dependencies[propertyName] && this.schema.dependencies[propertyName].length > 0; } getOptionalButtonLabel(): string { let label: string = 'Additional options'; if (this.parentName || this.schema.title) { label += ' of '; label += this.parentName || this.schema.title; } return label; } onChangeOptionalFieldsVisibility(): void { this.showCollapsedSectionFields = !this.showCollapsedSectionFields; } getFieldClasses(propertyName: string): any { return { 'hidden': this.isCollapsedSection() && !this.showCollapsedSectionFields, 'parent-field': this.isAParentField(propertyName) || (this.isASwitchSection() && this.isTheFirstField(propertyName)) }; } fieldHasToBeCreated(propertyName: string): boolean { let createField: boolean = true; let parentField: string = this.getParentField(propertyName); if (((parentField && !this._value[parentField]) || (this.isInADisabledSection() && !this.isTheFirstField(propertyName))) || !this.fulfillDependencyVisibility(propertyName)) { createField = false; this._value[propertyName] = undefined; } return createField; } isRelatedField(propertyName: string): boolean { return this._schema.properties[propertyName].ui && this.schema.properties[propertyName].ui.relatedTo && this.schema.properties[propertyName].ui.relatedTo.length > 0; } // When value is received from outside writeValue(value: any): void { if (value) { this.onChange(value); this.innerValue = value; } } onChangeProperty(value: any, property: string): void { this._value[property] = value; this.valueChange.emit(this._value); setTimeout(() => { this.onChange(this._value); }); } // Registry the change function to propagate internal model changes registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } // Registry the touch function to propagate internal touch events TODO: make this function. registerOnTouched(fn: () => void): void { this.onTouched = fn; } // Allows Angular to disable the form. setDisabledState(disable: boolean): void { if (disable) { this.form.control.disable(); } else { this.form.control.enable(); } } ngOnDestroy(): void { if (this._parentFieldSubscription) { for (let i = 0; i < this._parentFieldSubscription.length; ++i) { this._parentFieldSubscription[i].unsubscribe(); } } } isInADisabledSection(): boolean { if (this.isASwitchSection()) { let sectionEnabler: string = Object.keys(this.schema.properties)[0]; return !(this.form && this.form.controls[sectionEnabler] && this.form.controls[sectionEnabler].value); } return false; } isASwitchSection(): boolean { return this.schema.ui && this.schema.ui.component === FORM_UI_COMPONENT.SWITCH; } isAnAccordion(): boolean { return this.schema.ui && this.schema.ui.component === FORM_UI_COMPONENT.ACCORDION; } onClickTitle(): void { if (this.isAnAccordion()) { this.onChangeOptionalFieldsVisibility(); } } isTheFirstField(propertyName: string): boolean { return propertyName === Object.keys(this.schema.properties)[0]; } onClickLink(fieldKey: string): void { this.clickLink.emit(fieldKey); } private getParentField(propertyName: string): string { let parentField: string = undefined; if (this.schema.dependencies) { Object.keys(this.schema.dependencies).forEach((key: string) => { if (this.schema.dependencies[key].indexOf(propertyName) !== -1) { parentField = key; } }); } return parentField; } private fulfillDependencyVisibility(propertyName: string): boolean { let propertySchema: StFormSchema = this.schema.properties[propertyName]; let fulfill: boolean = !propertySchema.ui || propertySchema.ui.visible !== false; if (propertySchema.ui && propertySchema.ui.visible) { const keys: string[] = Object.keys(propertySchema.ui.visible); let i = 0; while (fulfill && i < keys.length && propertySchema.ui.visible.hasOwnProperty(keys[i])) { if (this._value[keys[i]] !== propertySchema.ui.visible[keys[i]]) { fulfill = false; } ++i; } } return fulfill; } }