diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts index ec441a1b58..c1cdf06939 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.spec.ts @@ -115,11 +115,66 @@ describe('FormFieldModel', () => { it('should parse and leave dropdown value as is', () => { const field = new FormFieldModel(new FormModel(), { type: FormFieldTypes.DROPDOWN, - options: [], - value: 'deferred' + options: [{ id: 'one', name: 'One' }], + value: { id: 'one', name: 'One' } }); - expect(field.value).toBe('deferred'); + expect(field.value).toEqual({ id: 'one', name: 'One' }); + expect(field.options).toEqual([{ id: 'one', name: 'One' }]); + }); + + it('should filter out invalid options on field initialization', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DROPDOWN, + options: [{ id: 'valid', name: 'Valid' }, { id: 'invalid' }, { name: 'invalid' }, [], {}, 'invalid'], + value: null + }); + + expect(field.options).toEqual([{ id: 'valid', name: 'Valid' }]); + }); + + it('should add value to field options if NOT present', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DROPDOWN, + options: [], + value: { id: 'one', name: 'One' } + }); + + expect(field.value).toEqual({ id: 'one', name: 'One' }); + expect(field.options).toEqual([{ id: 'one', name: 'One' }]); + }); + + it('should assign "empty" option as value if value is null and "empty" option is present in options', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DROPDOWN, + options: [ + { id: 'empty', name: 'Chose option...' }, + { id: 'one', name: 'One' } + ], + value: null + }); + + expect(field.value).toEqual({ id: 'empty', name: 'Chose option...' }); + }); + + it('should assign null to value when value has invalid option object', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DROPDOWN, + options: [{ id: 'one', name: 'One' }], + value: { id: 'one' } + }); + + expect(field.value).toBe(null); + }); + + it('should set hasEmptyValue to true if "empty" option is present in options', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DROPDOWN, + options: [{ id: 'empty', name: 'Chose option...' }], + value: null + }); + + expect(field.hasEmptyValue).toBe(true); }); it('should add value to field options if NOT present', () => { @@ -144,7 +199,7 @@ describe('FormFieldModel', () => { }); expect(field.hasEmptyValue).toBe(true); - expect(field.emptyOption).toEqual({ id: 'empty', name: 'Chose one...' }); + expect(field.emptyValueOption).toEqual({ id: 'empty', name: 'Chose one...' }); expect(field.value).toEqual('empty'); }); @@ -156,7 +211,7 @@ describe('FormFieldModel', () => { }); expect(field.hasEmptyValue).toBe(true); - expect(field.emptyOption).toEqual({ id: 'empty', name: 'Choose one...' }); + expect(field.emptyValueOption).toEqual({ id: 'empty', name: 'Choose one...' }); }); it('should add default "empty" option to the options if hasEmptyValue is true but "empty" option is not present', () => { @@ -168,7 +223,7 @@ describe('FormFieldModel', () => { }); expect(field.hasEmptyValue).toBe(true); - expect(field.emptyOption).toEqual({ id: 'empty', name: 'Choose one...' }); + expect(field.emptyValueOption).toEqual({ id: 'empty', name: 'Choose one...' }); expect(field.options).toEqual([ { id: 'empty', name: 'Choose one...' }, { id: 'one', name: 'One' } @@ -495,7 +550,7 @@ describe('FormFieldModel', () => { { id: 'fake-option-2', name: 'fake label 2' }, { id: 'fake-option-3', name: 'fake label 3' } ], - value: 'fake-option-2' + value: { id: 'fake-option-2', name: 'fake label 2' } }); expect(field.getOptionName()).toBe('fake label 2'); expect(field.hasEmptyValue).toBe(true); @@ -562,20 +617,17 @@ describe('FormFieldModel', () => { expect(field.value).toBe(false); }); - it('should set the value as null for a dropdown field that has the None value selected', () => { + it('should set the form value as null for a dropdown field that has the "empty" option selected', () => { const form = new FormModel(); const field = new FormFieldModel(form, { id: 'dropdown-1', - type: FormFieldTypes.DROPDOWN + type: FormFieldTypes.DROPDOWN, + options: [{ id: 'empty', name: 'Chose option...' }], + value: null }); - field.value = 'empty'; - expect(form.values['dropdown-1']).toBe(null); - - field.value = ''; - expect(form.values['dropdown-1']).toBe(null); - - field.value = undefined; + expect(field.hasEmptyValue).toBe(true); + expect(field.value).toEqual({ id: 'empty', name: 'Chose option...' }); expect(form.values['dropdown-1']).toBe(null); }); @@ -590,8 +642,12 @@ describe('FormFieldModel', () => { ] }); - field.value = 'opt2'; - expect(form.values['dropdown-2']).toEqual(field.options[1]); + const valueBeforeSelection = form.values['dropdown-2']; + field.value = field.options[1]; + const valueAfterSelection = form.values['dropdown-2']; + + expect(valueBeforeSelection).toEqual(null); + expect(valueAfterSelection).toEqual({ id: 'opt2', name: 'Option 2' }); }); it('should update form with radio button value', () => { @@ -728,7 +784,7 @@ describe('FormFieldModel', () => { id: 'dropdown_field', name: 'header', type: FormFieldTypes.DROPDOWN, - value: 'opt1', + value: { id: 'opt1', name: 'Option 1' }, required: false, readOnly: true, options: [ @@ -747,7 +803,7 @@ describe('FormFieldModel', () => { id: 'dropdown_field', name: 'header', type: FormFieldTypes.DROPDOWN, - value: 'opt1', + value: { id: 'opt1', name: 'Option 1' }, required: false, readOnly: true, restUrl: 'fake-url-just-to-show', @@ -771,7 +827,7 @@ describe('FormFieldModel', () => { id: 'dropdown_field', name: 'header', type: FormFieldTypes.DROPDOWN, - value: 'opt1', + value: { id: 'opt1', name: 'Option 1' }, required: false, readOnly: true, restUrl: 'fake-url-just-to-show', diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts index aae980098a..84f0647723 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts @@ -38,6 +38,7 @@ export class FormFieldModel extends FormWidgetModel { private _isValid: boolean = true; private _required: boolean = false; + private readonly emptyValueOptionId = 'empty'; readonly defaultDateFormat: string = 'D-M-YYYY'; readonly defaultDateTimeFormat: string = 'D-M-YYYY hh:mm A'; private readonly defaultEmptyOptionId = 'empty'; @@ -96,7 +97,7 @@ export class FormFieldModel extends FormWidgetModel { columns: ContainerColumnModel[] = []; // util members - emptyOption: FormFieldOption; + emptyValueOption: FormFieldOption; validationSummary: ErrorMessageModel; get value(): any { @@ -189,9 +190,9 @@ export class FormFieldModel extends FormWidgetModel { this.maxDateRangeValue = json.maxDateRangeValue; this.dynamicDateRangeSelection = json.dynamicDateRangeSelection; this.regexPattern = json.regexPattern; - this.options = this.parseValidOptions(json.options); - this.emptyOption = this.getEmptyOption(this.options); - this.hasEmptyValue = json?.hasEmptyValue ?? !!this.emptyOption; + this.options = Array.isArray(json.options) ? json.options.filter((option) => this.isValidOption(option)) : []; + this.hasEmptyValue = json?.hasEmptyValue ?? this.hasEmptyValueOption(this.options); + this.emptyValueOption = this.hasEmptyValue ? this.getEmptyValueOption(this.options) : undefined; this.className = json.className; this.optionType = json.optionType; this.params = json.params || {}; @@ -236,8 +237,12 @@ export class FormFieldModel extends FormWidgetModel { this.updateForm(); } - private getEmptyOption(options: FormFieldOption[]): FormFieldOption { - return options.find((option) => option?.id === this.defaultEmptyOptionId); + private getEmptyValueOption(options: FormFieldOption[]): FormFieldOption { + return options.find((option) => option?.id === this.emptyValueOptionId); + } + + private hasEmptyValueOption(options: FormFieldOption[]): boolean { + return options.some((option) => option?.id === this.emptyValueOptionId); } private setValueForReadonlyType(form: any) { @@ -304,41 +309,42 @@ export class FormFieldModel extends FormWidgetModel { /* This is needed due to Activiti issue related to reading dropdown values as value string but saving back as object: { id: , name: } + Side note: Probably not valid anymore */ if (json.type === FormFieldTypes.DROPDOWN) { - if (this.hasEmptyValue) { - if (!this.emptyOption) { - this.emptyOption = { + if (this.hasEmptyValue && value === null) { + if (!this.emptyValueOption) { + this.emptyValueOption = { id: this.defaultEmptyOptionId, name: this.defaultEmptyOptionName }; - this.options.unshift(this.emptyOption); + this.options.unshift(this.emptyValueOption); } - const isEmptyValue = !value || [this.emptyOption.id, this.emptyOption.name].includes(value); - if (isEmptyValue) { - return this.emptyOption.id; - } + value = this.emptyValueOption; + return value; } if (this.isValidOption(value)) { this.addOption(value); - return value.id; + return value; } if (this.hasMultipleValues) { - const validSelectedOptions = (Array.isArray(json.value) ? json.value : []).filter((option) => this.isValidOption(option)); + let arrayOfSelectedOptions = Array.isArray(json.value) ? json.value : []; + arrayOfSelectedOptions = arrayOfSelectedOptions.filter((option) => this.isValidOption(option)); - this.addOptions(validSelectedOptions); - return validSelectedOptions; + this.addOptions(arrayOfSelectedOptions); + value = arrayOfSelectedOptions; + return value; } - - return value; + return null; } /* This is needed due to Activiti issue related to reading radio button values as value string but saving back as object: { id: , name: } + Side note: Probably not valid anymore */ if (json.type === FormFieldTypes.RADIO_BUTTONS) { // Activiti has a bug with default radio button value where initial selection passed as `name` value @@ -385,31 +391,7 @@ export class FormFieldModel extends FormWidgetModel { switch (this.type) { case FormFieldTypes.DROPDOWN: { - if (!this.value) { - this.form.values[this.id] = null; - break; - } - - /* - This is needed due to Activiti reading dropdown values as string - but saving back as object: { id: , name: } - */ - if (Array.isArray(this.value)) { - this.form.values[this.id] = this.value; - break; - } - - if (typeof this.value === 'string') { - if (this.value === 'empty' || this.value === '') { - this.form.values[this.id] = null; - break; - } - - const entry: FormFieldOption[] = this.options.filter((opt) => opt.id === this.value); - if (entry.length > 0) { - this.form.values[this.id] = entry[0]; - } - } + this.form.values[this.id] = this.isEmptyValueOption(this.value) ? null : this.value; break; } case FormFieldTypes.RADIO_BUTTONS: { @@ -509,9 +491,8 @@ export class FormFieldModel extends FormWidgetModel { return type === 'container'; } - getOptionName(): string { - const option: FormFieldOption = this.options.find((opt) => opt.id === this.value); - return option ? option.name : null; + getOptionName(): null | string { + return this.value ? this.value?.name : null; } hasOptions() { @@ -519,7 +500,7 @@ export class FormFieldModel extends FormWidgetModel { } isEmptyValueOption(option: FormFieldOption): boolean { - return this.hasEmptyValue && option?.id === this.defaultEmptyOptionId; + return this.hasEmptyValueOption && option?.id === this.emptyValueOptionId; } private addOptions(options: FormFieldOption[]) {