diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts index 890a6ea54..805e28964 100644 --- a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts @@ -254,4 +254,70 @@ describe('DynamicFormComponent', () => { expect(wrapper?.clientHeight).toEqual(hint.offsetHeight); }); }); + + describe('select multiple with "None of the above" option', () => { + const multiSelectIndex = '5'; + + beforeEach(() => { + component.createProfileForm([ + ...PROFILE_FORM, + { + question: 'Which statements are true?', + type: FormControlType.SELECT_MULTIPLE, + options: ['Option 1', 'Option 2', 'None of the above'], + validation: { + required: true, + }, + }, + ]); + fixture.detectChanges(); + }); + + it('should unselect "None of the above" when another option is selected after it', () => { + const multiSelect = component.getFormGroup(multiSelectIndex); + + multiSelect.get('2')?.setValue(true); + expect(multiSelect.value).toEqual({ 0: false, 1: false, 2: true }); + + multiSelect.get('1')?.setValue(true); + expect(multiSelect.value).toEqual({ 0: false, 1: true, 2: false }); + }); + + it('should unselect other options when "None of the above" is selected, and unselect "None of the above" when another option is selected again', () => { + const multiSelect = component.getFormGroup(multiSelectIndex); + + multiSelect.get('0')?.setValue(true); + multiSelect.get('1')?.setValue(true); + expect(multiSelect.value).toEqual({ 0: true, 1: true, 2: false }); + + multiSelect.get('2')?.setValue(true); + expect(multiSelect.value).toEqual({ 0: false, 1: false, 2: true }); + + multiSelect.get('1')?.setValue(true); + expect(multiSelect.value).toEqual({ 0: false, 1: true, 2: false }); + }); + + it('should unselect "None of the above" when another option is selected after form reset', () => { + const multiSelect = component.getFormGroup(multiSelectIndex); + + component.formGroup.reset(); + multiSelect.get('2')?.setValue(true); + multiSelect.get('1')?.setValue(true); + + expect(multiSelect.get('1')?.value).toBeTrue(); + expect(multiSelect.get('2')?.value).toBeFalse(); + }); + + it('should handle non-string option values without throwing an error', () => { + expect(() => + component.createProfileForm([ + { + question: 'Object options question', + type: FormControlType.SELECT_MULTIPLE, + options: [{ label: 'Option 1' }, { label: 'None of the above' }], + }, + ]) + ).not.toThrow(); + }); + }); }); diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts index ce1297f4c..e572b9572 100644 --- a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts @@ -56,8 +56,6 @@ import { ProfileValidators } from '../../pages/risk-assessment/profile-form/prof import { DomSanitizer } from '@angular/platform-browser'; import { Subject } from 'rxjs/internal/Subject'; import { takeUntil } from 'rxjs/internal/operators/takeUntil'; -import { startWith } from 'rxjs/internal/operators/startWith'; -import { pairwise } from 'rxjs/internal/operators/pairwise'; @Component({ selector: 'app-dynamic-form', @@ -141,7 +139,7 @@ export class DynamicFormComponent implements OnInit, OnDestroy { const multiSelect = this.getMultiSelectGroup(question); this.formGroup.addControl(index.toString(), multiSelect); const noneKey = this.getNoneOptionIndex(question.options); - if (noneKey) { + if (noneKey !== null) { this.applyNoneLogic(multiSelect, noneKey.toString()); } } else { @@ -224,38 +222,38 @@ export class DynamicFormComponent implements OnInit, OnDestroy { }); } - private applyNoneLogic(control: AbstractControl, noneKey: string) { - control.valueChanges - .pipe(takeUntil(this.destroy$), startWith(control.value), pairwise()) - .subscribe(([prev, curr]) => { - const changedKey = Object.keys(curr).find( - key => curr[key] === true && prev[key] === false - ); + private applyNoneLogic(control: FormGroup, noneKey: string) { + let prevValue = { ...control.value }; - if (!changedKey) { - return; - } + control.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(currValue => { + const changedKey = Object.keys(currValue).find( + key => currValue[key] === true && !prevValue[key] + ); - if (changedKey == noneKey) { - const newValue = { ...curr }; + if (changedKey === noneKey) { + const newValue = { ...currValue }; - Object.keys(newValue).forEach(key => { - if (key !== noneKey) { - newValue[key] = false; - } - }); + Object.keys(newValue).forEach(key => { + if (key !== noneKey) { + newValue[key] = false; + } + }); - control.setValue(newValue, { emitEvent: false }); - } else if (curr[noneKey] === true) { - control.patchValue({ [noneKey]: false }, { emitEvent: false }); - } - }); + control.setValue(newValue, { emitEvent: false }); + } else if (changedKey && currValue[noneKey] === true) { + control.patchValue({ [noneKey]: false }, { emitEvent: false }); + } + + prevValue = { ...control.value }; + }); } private getNoneOptionIndex(options: OptionType[] | undefined): number | null { if (!options) return null; - return options.findIndex(option => - this.getOptionValue(option)?.toLowerCase().includes('none') - ); + const index = options.findIndex(option => { + const value = this.getOptionValue(option); + return typeof value === 'string' && value.toLowerCase().includes('none'); + }); + return index !== -1 ? index : null; } }