diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html index 5614e23c4a..a044a78e8e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html @@ -288,6 +288,7 @@ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html index 96ee0dd38e..a5d6d63418 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html @@ -10,6 +10,7 @@ [value]="year" [invalid]="showErrorMessages" [placeholder]='yearPlaceholder' + (blur)="onBlur($event)" (change)="onChange($event)" (focus)="onFocus($event)" > @@ -24,6 +25,7 @@ [value]="month" [placeholder]="monthPlaceholder" [disabled]="!year || model.disabled" + (blur)="onBlur($event)" (change)="onChange($event)" (focus)="onFocus($event)" > @@ -38,6 +40,7 @@ [value]="day" [placeholder]="dayPlaceholder" [disabled]="!month || model.disabled" + (blur)="onBlur($event)" (change)="onChange($event)" (focus)="onFocus($event)" > diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts index 6d50d4375b..bf3a5ed98e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { FormControl, FormGroup } from '@angular/forms'; import { DynamicDsDatePickerModel } from './date-picker.model'; import { hasValue, isNotEmpty } from '../../../../../empty.util'; @@ -23,6 +23,7 @@ export class DsDatePickerComponent implements OnInit { @Output() selected = new EventEmitter(); @Output() remove = new EventEmitter(); + @Output() blur = new EventEmitter(); @Output() change = new EventEmitter(); @Output() focus = new EventEmitter(); @@ -76,6 +77,10 @@ export class DsDatePickerComponent implements OnInit { } + onBlur(event) { + this.blur.emit(); + } + onChange(event) { // update year-month-day switch (event.field) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html index c628eaf8e5..d82ab9133d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html @@ -15,14 +15,16 @@ (click)="expandForm()"> -
+
+ [emitChange]="false" + (dfBlur)="onBlur($event)" + (dfFocus)="onFocus($event)">
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts index 7ad62ce479..2d5f33eac0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts @@ -11,12 +11,7 @@ import { } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { - DynamicFormControlEvent, - DynamicFormControlModel, - DynamicFormGroupModel, - DynamicInputModel -} from '@ng-dynamic-forms/core'; +import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core'; import { isEqual } from 'lodash'; import { DynamicGroupModel, PLACEHOLDER_PARENT_METADATA } from './dynamic-group.model'; @@ -25,7 +20,6 @@ import { SubmissionFormsModel } from '../../../../../../core/shared/config/confi import { FormService } from '../../../../form.service'; import { FormComponent } from '../../../../form.component'; import { Chips } from '../../../../../chips/models/chips.model'; -import { DynamicLookupModel } from '../lookup/dynamic-lookup.model'; import { hasValue, isEmpty, isNotEmpty } from '../../../../../empty.util'; import { shrinkInOut } from '../../../../../animations/shrink'; import { ChipsItem } from '../../../../../chips/models/chips-item.model'; @@ -48,6 +42,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { @Input() formId: string; @Input() group: FormGroup; @Input() model: DynamicGroupModel; + @Input() showErrorMessages = false; @Output() blur: EventEmitter = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); @@ -57,7 +52,6 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { public formCollapsed = Observable.of(false); public formModel: DynamicFormControlModel[]; public editMode = false; - public invalid = false; private selectedChipItem: ChipsItem; private subs: Subscription[] = []; @@ -72,7 +66,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { ngOnInit() { const config = {rows: this.model.formConfiguration} as SubmissionFormsModel; - if (isNotEmpty(this.model.value)) { + if (!this.model.isEmpty()) { this.formCollapsed = Observable.of(true); } this.model.valueUpdates.subscribe((value: any[]) => { @@ -80,38 +74,22 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { }); this.formId = this.formService.getUniqueId(this.model.id); - this.formModel = this.formBuilderService.modelFromConfiguration(config, this.model.scopeUUID, {}); - this.chips = new Chips(this.model.value, 'value', this.model.mandatoryField); + this.formModel = this.formBuilderService.modelFromConfiguration(config, this.model.scopeUUID, {}, this.model.submissionScope, this.model.readOnly); + const initChipsValue = this.model.isEmpty() ? [] : this.model.value; + this.chips = new Chips(initChipsValue, 'value', this.model.mandatoryField); this.subs.push( this.chips.chipsItems .subscribe((subItems: any[]) => { const items = this.chips.getChipsItems(); // Does not emit change if model value is equal to the current value if (!isEqual(items, this.model.value)) { - if (isEmpty(items)) { - // If items is empty, last element has been removed - // so emit an empty value that allows to dispatch - // a remove JSON PATCH operation - const emptyItem = Object.create({}); - Object.keys(this.model.value[0]) - .forEach((key) => { - emptyItem[key] = null; - }); - items.push(emptyItem); + // if ((isNotEmpty(items) && !this.model.isEmpty()) || (isEmpty(items) && !this.model.isEmpty())) { + if (!(isEmpty(items) && this.model.isEmpty())) { + this.model.valueUpdates.next(items); + this.change.emit(); } - - this.model.valueUpdates.next(items); - this.change.emit(); } }), - // Invalid state for year - this.group.get(this.model.id).statusChanges.subscribe((state) => { - if (state === 'INVALID') { - this.invalid = true; - } else { - this.invalid = false; - } - }) ) } @@ -130,8 +108,8 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { return res; } - onChange(event: DynamicFormControlEvent) { - return + onBlur(event) { + this.blur.emit(); } onChipSelected(event) { @@ -142,26 +120,23 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { modelRow.group.forEach((model: DynamicInputModel) => { const value = (this.selectedChipItem.item[model.name] === PLACEHOLDER_PARENT_METADATA || this.selectedChipItem.item[model.name].value === PLACEHOLDER_PARENT_METADATA) - ? null - : this.selectedChipItem.item[model.name]; + ? null + : this.selectedChipItem.item[model.name]; if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) { model.valueUpdates.next(value.display); } else { model.valueUpdates.next(value); } - // if (model instanceof DynamicLookupModel) { - // (model as DynamicLookupModel).valueUpdates.next(value); - // } else if (model instanceof DynamicInputModel) { - // model.valueUpdates.next(value); - // } else { - // (model as any).value = value; - // } }); }); this.editMode = true; } + onFocus(event) { + this.focus.emit(event); + } + collapseForm() { this.formCollapsed = Observable.of(true); this.clear(); @@ -178,7 +153,9 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { this.editMode = false; } this.resetForm(); - // this.change.emit(event); + if (!this.model.isEmpty()) { + this.formCollapsed = Observable.of(true); + } } save() { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts index 4ec029661b..59e442b4e9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts @@ -1,6 +1,8 @@ import { DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; import { FormRowModel } from '../../../../../../core/shared/config/config-submission-forms.model'; import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model'; +import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; +import { isEmpty, isNull } from '../../../../../empty.util'; export const DYNAMIC_FORM_CONTROL_TYPE_RELATION = 'RELATION'; export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#'; @@ -14,6 +16,7 @@ export interface DynamicGroupModelConfig extends DsDynamicInputModelConfig { name: string, relationFields: string[], scopeUUID: string, + submissionScope: string; value?: any; } @@ -25,7 +28,8 @@ export class DynamicGroupModel extends DsDynamicInputModel { @serializable() mandatoryField: string; @serializable() relationFields: string[]; @serializable() scopeUUID: string; - @serializable() value: any[]; + @serializable() submissionScope: string; + @serializable() _value: any[]; @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_RELATION; constructor(config: DynamicGroupModelConfig, layout?: DynamicFormControlLayout) { @@ -35,7 +39,32 @@ export class DynamicGroupModel extends DsDynamicInputModel { this.mandatoryField = config.mandatoryField; this.relationFields = config.relationFields; this.scopeUUID = config.scopeUUID; + this.submissionScope = config.submissionScope; const value = config.value || []; - this.valueUpdates.next(value) + this.valueUpdates.next(value); + } + + get value() { + if (isEmpty(this._value)) { + // If items is empty, last element has been removed + // so emit an empty value that allows to dispatch + // a remove JSON PATCH operation + const emptyItem = Object.create({}); + emptyItem[this.mandatoryField] = null; + this.relationFields + .forEach((field) => { + emptyItem[field] = null; + }); + return [emptyItem]; + } + return this._value + } + + set value(value) { + this._value = value; + } + + isEmpty() { + return (this.value.length === 1 && isNull(this.value[0][this.mandatoryField])); } } diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 2b0cddaaa4..b33018ea0c 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -162,12 +162,13 @@ export class FormBuilderService extends DynamicFormService { Object.keys(groupValue) .forEach((key) => { const normValue = normalizeValue(controlModel, groupValue[key], groupIndex); - if (iterateResult.hasOwnProperty(key)) { - iterateResult[key].push(normValue); - } else { - iterateResult[key] = [normValue]; + if (normValue.hasValue()) { + if (iterateResult.hasOwnProperty(key)) { + iterateResult[key].push(normValue); + } else { + iterateResult[key] = [normValue]; + } } - // newGroupValue[key] = normalizeValue(controlModel, groupValue[key], groupIndex); }); // controlArrayValue.push(newGroupValue); }) diff --git a/src/app/shared/form/builder/parsers/group-field-parser.ts b/src/app/shared/form/builder/parsers/group-field-parser.ts index cd897bb265..6c9ce6606a 100644 --- a/src/app/shared/form/builder/parsers/group-field-parser.ts +++ b/src/app/shared/form/builder/parsers/group-field-parser.ts @@ -13,7 +13,8 @@ export class GroupFieldParser extends FieldParser { constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean, - protected authorityUuid: string) { + protected submissionScope: string, + protected authorityUuid: string,) { super(configData, initFormValues, readOnly); } @@ -21,6 +22,7 @@ export class GroupFieldParser extends FieldParser { const modelConfiguration: DynamicGroupModelConfig = this.initModel(); modelConfiguration.scopeUUID = this.authorityUuid; + modelConfiguration.submissionScope = this.submissionScope; if (this.configData && this.configData.rows && this.configData.rows.length > 0) { modelConfiguration.formConfiguration = this.configData.rows; modelConfiguration.relationFields = []; diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index 833ccfb6bd..3f76c1c5c1 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -90,7 +90,7 @@ export class RowParser { break; case 'group': - fieldModel = new GroupFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse(); + fieldModel = new GroupFieldParser(fieldData, this.initFormValues, this.readOnly, this.submissionScope, this.authorityOptions.uuid).parse(); break; case 'twobox': diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 0539a57000..5dfaa4bec1 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -6,6 +6,7 @@ [formGroup]="formGroup" [formModel]="formModel" [formLayout]="formLayout" + (change)="$event.stopPropagation();" (dfBlur)="onBlur($event)" (dfChange)="onChange($event)" (dfFocus)="onFocus($event)"> diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 981aed083e..7a894ca3e5 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -8,6 +8,7 @@ import { DynamicFormGroupModel, DynamicFormLayout, } from '@ng-dynamic-forms/core'; import { Store } from '@ngrx/store'; +import { findIndex } from 'lodash'; import { AppState } from '../../app.reducer'; import { @@ -37,6 +38,7 @@ import { isEmpty } from 'lodash'; }) export class FormComponent implements OnDestroy, OnInit { + private formErrors: FormError[] = []; private formValid: boolean; /** @@ -44,6 +46,11 @@ export class FormComponent implements OnDestroy, OnInit { */ @Input() displaySubmit = true; + /** + * A boolean that indicate if to emit a form change event + */ + @Input() emitChange = true; + /** * The form unique ID */ @@ -154,7 +161,9 @@ export class FormComponent implements OnDestroy, OnInit { .subscribe((errors: FormError[]) => { const {formGroup, formModel} = this; - errors.forEach((error: FormError) => { + errors + .filter((error: FormError) => findIndex(this.formErrors, {fieldId: error.fieldId}) === -1) + .forEach((error: FormError) => { const {fieldId} = error; let field: AbstractControl; if (!!this.parentFormModel) { @@ -166,9 +175,29 @@ export class FormComponent implements OnDestroy, OnInit { if (field) { const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel); this.formService.addErrorToField(field, model, error.message); + // this.formService.validateAllFormFields(formGroup); + this.changeDetectorRef.detectChanges(); } }); + this.formErrors + .filter((error: FormError) => findIndex(errors, {fieldId: error.fieldId}) === -1) + .forEach((error: FormError) => { + const {fieldId} = error; + let field: AbstractControl; + if (!!this.parentFormModel) { + field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as FormGroup, formModel); + } else { + field = this.formBuilderService.getFormControlById(fieldId, formGroup, formModel); + } + + if (field) { + const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel); + this.formService.removeErrorFromField(field, model, error.message); + } + }); + + this.formErrors = errors; this.changeDetectorRef.detectChanges(); }) ); @@ -217,10 +246,11 @@ export class FormComponent implements OnDestroy, OnInit { this.store.dispatch(action); this.formGroup.markAsPristine(); - this.change.emit(event); - const control: FormControl = event.control; + if (this.emitChange) { + this.change.emit(event); + } - // control.setErrors(null); + const control: FormControl = event.control; if (control.valid) { this.store.dispatch(new FormRemoveErrorAction(this.formId, event.model.id)); } diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index f890d90012..f13bae1532 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -7,9 +7,9 @@ import { AppState } from '../../app.reducer'; import { formObjectFromIdSelector } from './selectors'; import { FormBuilderService } from './builder/form-builder.service'; import { DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core'; -import { isNotEmpty, isNotUndefined } from '../empty.util'; +import { isEmpty, isNotEmpty, isNotUndefined } from '../empty.util'; import { find, uniqueId } from 'lodash'; -import { FormChangeAction } from './form.actions'; +import { FormChangeAction, FormRemoveErrorAction } from './form.actions'; @Injectable() export class FormService { @@ -75,39 +75,46 @@ export class FormService { } public addErrorToField(field: AbstractControl, model: DynamicFormControlModel, message: string) { - const errorFound = !!(find(field.errors, (err) => err === message)); - // search for the same error in the formControl.errors property - if (!errorFound) { - const errorKey = uniqueId('error-'); // create a single key for the error - const error = {}; // create the error object + const error = {}; // create the error object - error[errorKey] = message; // assign message - - // if form control model has not errorMessages object, create it - if (!model.errorMessages) { - model.errorMessages = {}; - } - - // put the error in the form control model - model.errorMessages[errorKey] = message; - - // Use correct error messages from the model - const lastArray = message.split('.'); - if (lastArray && lastArray.length > 0) { - const last = lastArray[lastArray.length - 1]; - const modelMsg = model.errorMessages[last]; - if (modelMsg && modelMsg.length > 0) { - model.errorMessages[errorKey] = modelMsg; - } - } - - // add the error in the form control - field.setErrors(error); - - // formGroup.markAsDirty(); - field.markAsTouched(); + // if form control model has not errorMessages object, create it + if (!model.errorMessages) { + model.errorMessages = {}; } + + // Use correct error messages from the model + const lastArray = message.split('.'); + if (lastArray && lastArray.length > 0) { + // check if error code is already present in the set of model's validators + const last = lastArray[lastArray.length - 1]; + const modelMsg = model.errorMessages[last]; + if (isEmpty(modelMsg)) { + const errorKey = uniqueId('error-'); // create a single key for the error + error[errorKey] = true; + // put the error message in the form control model + model.errorMessages[errorKey] = message; + } else { + error[last] = modelMsg; + } + } + + // add the error in the form control + field.setErrors(error); + field.markAsTouched(); + } + + public removeErrorFromField(field: AbstractControl, model: DynamicFormControlModel, message) { + const error = {}; + + // Use correct error messages from the model + const lastArray = message.split('.'); + if (lastArray && lastArray.length > 0) { + const last = lastArray[lastArray.length - 1]; + error[last] = null; + } + field.setErrors(error); + field.markAsUntouched(); } public resetForm(formGroup: FormGroup, groupModel: DynamicFormControlModel[], formId: string) {