From 3d429d3b35d23bd2da439e412922725904118448 Mon Sep 17 00:00:00 2001 From: kurilova Date: Thu, 15 Oct 2026 13:30:36 +0000 Subject: [PATCH] Fix "None of the above" option selection --- .../dynamic-form.component.spec.ts | 54 +++++++++++++++++++ .../dynamic-form/dynamic-form.component.ts | 54 +++++++++---------- 2 files changed, 78 insertions(+), 30 deletions(-) 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..7a7bfd0c8 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,58 @@ 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(); + }); + }); }); 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..49b5b439e 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,34 @@ 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 - ); - - if (!changedKey) { - return; - } - - if (changedKey == noneKey) { - const newValue = { ...curr }; - - 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 }); - } - }); + private applyNoneLogic(control: FormGroup, noneKey: string) { + Object.keys(control.controls).forEach(key => { + control + .get(key) + ?.valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe(checked => { + if (!checked) { + return; + } + + if (key === noneKey) { + Object.keys(control.controls).forEach(k => { + if (k !== noneKey) { + control.get(k)?.setValue(false, { emitEvent: false }); + } + }); + } else if (control.get(noneKey)?.value === true) { + control.get(noneKey)?.setValue(false, { emitEvent: false }); + } + }); + }); } private getNoneOptionIndex(options: OptionType[] | undefined): number | null { if (!options) return null; - return options.findIndex(option => + const index = options.findIndex(option => this.getOptionValue(option)?.toLowerCase().includes('none') ); + return index !== -1 ? index : null; } }