From 4c7387812cd7e3a2cc718b2371139ba679532000 Mon Sep 17 00:00:00 2001 From: kurilova Date: Thu, 15 Oct 2026 13:30:36 +0000 Subject: [PATCH 1/2] 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; } } From 408fb8d9196d0d0c7dc1a331b265bd71b780c0fe Mon Sep 17 00:00:00 2001 From: kurilova Date: Fri, 16 Oct 2026 10:23:36 +0000 Subject: [PATCH 2/2] Fix gemini suggestions --- .../dynamic-form.component.spec.ts | 12 ++++++ .../dynamic-form/dynamic-form.component.ts | 42 ++++++++++--------- 2 files changed, 35 insertions(+), 19 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 7a7bfd0c8..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 @@ -307,5 +307,17 @@ describe('DynamicFormComponent', () => { 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 49b5b439e..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 @@ -223,33 +223,37 @@ export class DynamicFormComponent implements OnInit, OnDestroy { } 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; - } + let prevValue = { ...control.value }; + + control.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(currValue => { + const changedKey = Object.keys(currValue).find( + key => currValue[key] === true && !prevValue[key] + ); - 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 }); + if (changedKey === noneKey) { + const newValue = { ...currValue }; + + Object.keys(newValue).forEach(key => { + if (key !== noneKey) { + newValue[key] = 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; - const index = 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; } }