From e909b97a738b42ff6f8450e3a9898920a0b8d991 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Wed, 31 Jul 2024 10:49:06 +0100 Subject: [PATCH 01/27] Add storage service --- .../carbon-estimator-form.component.ts | 7 ++++++- src/app/services/storage.service.spec.ts | 16 ++++++++++++++ src/app/services/storage.service.ts | 21 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/app/services/storage.service.spec.ts create mode 100644 src/app/services/storage.service.ts diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index a4fb042..9b0b663 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -8,6 +8,7 @@ import { CarbonEstimationService } from '../services/carbon-estimation.service'; import { ExpansionPanelComponent } from '../expansion-panel/expansion-panel.component'; import { FormatCostRangePipe } from '../pipes/format-cost-range.pipe'; import { InvalidatedPipe } from '../pipes/invalidated.pipe'; +import { StorageService } from '../services/storage.service'; const locationDescriptions: Record = { WORLD: 'Globally', @@ -72,10 +73,14 @@ export class CarbonEstimatorFormComponent implements OnInit { constructor( private formBuilder: FormBuilder, private changeDetector: ChangeDetectorRef, - private estimationService: CarbonEstimationService + private estimationService: CarbonEstimationService, + private storageService: StorageService ) {} public ngOnInit() { + this.storageService.sayHello(); + this.storageService.set('foo', 'foo value'); + this.estimatorForm = this.formBuilder.nonNullable.group({ upstream: this.formBuilder.nonNullable.group({ headCount: [defaultValues.upstream.headCount, [Validators.required, Validators.min(1)]], diff --git a/src/app/services/storage.service.spec.ts b/src/app/services/storage.service.spec.ts new file mode 100644 index 0000000..e7fe5b5 --- /dev/null +++ b/src/app/services/storage.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { StorageService } from './storage.service'; + +describe('StorageService', () => { + let service: StorageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(StorageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/storage.service.ts b/src/app/services/storage.service.ts new file mode 100644 index 0000000..0dfe660 --- /dev/null +++ b/src/app/services/storage.service.ts @@ -0,0 +1,21 @@ +import { Inject, Injectable, InjectionToken } from '@angular/core'; + +export const BROWSER_STORAGE = new InjectionToken('Browser Storage', { + providedIn: 'root', + factory: () => sessionStorage, +}); + +@Injectable({ + providedIn: 'root', +}) +export class StorageService { + constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {} + + get(key: string) { + return this.storage.getItem(key); + } + + set(key: string, value: string) { + return this.storage.setItem(key, value); + } +} From f34c3cbfc3973f2655ace1ac0cc71ee972f2076c Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Wed, 31 Jul 2024 12:35:23 +0100 Subject: [PATCH 02/27] Store form data on value change --- .../carbon-estimator-form.component.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 9b0b663..bd743c2 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -78,9 +78,6 @@ export class CarbonEstimatorFormComponent implements OnInit { ) {} public ngOnInit() { - this.storageService.sayHello(); - this.storageService.set('foo', 'foo value'); - this.estimatorForm = this.formBuilder.nonNullable.group({ upstream: this.formBuilder.nonNullable.group({ headCount: [defaultValues.upstream.headCount, [Validators.required, Validators.min(1)]], @@ -107,6 +104,16 @@ export class CarbonEstimatorFormComponent implements OnInit { }), }); + const storedFormData = this.storageService.get('formData'); + + if (storedFormData) { + this.estimatorForm.setValue(JSON.parse(storedFormData)); + } + + this.estimatorForm.valueChanges.subscribe(value => { + this.storageService.set('formData', JSON.stringify(value)); + }); + this.estimatorForm.get('upstream.headCount')?.valueChanges.subscribe(() => { this.refreshPreviewServerCount(); }); From 802b31ae80736ebf97587a923b8ce689cf2704f3 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Wed, 31 Jul 2024 15:02:12 +0100 Subject: [PATCH 03/27] Reduce number of form saves --- .../carbon-estimator-form.component.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index bd743c2..27f6bab 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -1,5 +1,14 @@ import { CommonModule, JsonPipe } from '@angular/common'; -import { ChangeDetectorRef, Component, EventEmitter, OnInit, Output, input } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + HostListener, + OnDestroy, + OnInit, + Output, + input, +} from '@angular/core'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { EstimatorFormValues, EstimatorValues, WorldLocation, locationArray } from '../types/carbon-estimator'; import { costRanges, defaultValues, formContext, questionPanelConfig } from './carbon-estimator-form.constants'; @@ -37,7 +46,7 @@ const locationDescriptions: Record = { InvalidatedPipe, ], }) -export class CarbonEstimatorFormComponent implements OnInit { +export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { public formValue = input(); @Output() public formSubmit: EventEmitter = new EventEmitter(); @@ -110,10 +119,6 @@ export class CarbonEstimatorFormComponent implements OnInit { this.estimatorForm.setValue(JSON.parse(storedFormData)); } - this.estimatorForm.valueChanges.subscribe(value => { - this.storageService.set('formData', JSON.stringify(value)); - }); - this.estimatorForm.get('upstream.headCount')?.valueChanges.subscribe(() => { this.refreshPreviewServerCount(); }); @@ -168,6 +173,10 @@ export class CarbonEstimatorFormComponent implements OnInit { } } + ngOnDestroy(): void { + this.saveFormData(); + } + public handleSubmit() { if (!this.estimatorForm.valid) { return; @@ -187,6 +196,7 @@ export class CarbonEstimatorFormComponent implements OnInit { public resetForm() { this.estimatorForm.reset(); + this.saveFormData(); this.formReset.emit(); } @@ -209,4 +219,13 @@ export class CarbonEstimatorFormComponent implements OnInit { ); } } + + private saveFormData() { + this.storageService.set('formData', JSON.stringify(this.estimatorForm.value)); + } + + @HostListener('window:beforeunload', ['$event']) + onBeforeUnload(): void { + this.saveFormData(); + } } From 6fc2043fd66cf31b508e1b10cb352b41269d9189 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Thu, 1 Aug 2024 10:58:21 +0100 Subject: [PATCH 04/27] Restore stored data after input sets data --- .../carbon-estimator-form.component.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 27f6bab..1207281 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -113,12 +113,6 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { }), }); - const storedFormData = this.storageService.get('formData'); - - if (storedFormData) { - this.estimatorForm.setValue(JSON.parse(storedFormData)); - } - this.estimatorForm.get('upstream.headCount')?.valueChanges.subscribe(() => { this.refreshPreviewServerCount(); }); @@ -171,6 +165,12 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { if (formValue !== undefined) { this.estimatorForm.setValue(formValue); } + + const storedFormData = this.storageService.get('formData'); + + if (storedFormData) { + this.estimatorForm.setValue(JSON.parse(storedFormData)); + } } ngOnDestroy(): void { From 1c45e04cee84ea94846c6f3284378fd5d5ce9f6c Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Thu, 1 Aug 2024 13:16:32 +0100 Subject: [PATCH 05/27] Save raw form value --- .../carbon-estimator-form/carbon-estimator-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 1207281..ff3f5a6 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -221,7 +221,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { } private saveFormData() { - this.storageService.set('formData', JSON.stringify(this.estimatorForm.value)); + this.storageService.set('formData', JSON.stringify(this.estimatorForm.getRawValue())); } @HostListener('window:beforeunload', ['$event']) From 5aa6fc5ed3b62572a586831bc20c7138626482e1 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Thu, 1 Aug 2024 14:21:59 +0100 Subject: [PATCH 06/27] Add comparator for CostRange --- .../carbon-estimator-form.component.html | 3 +- .../carbon-estimator-form.component.ts | 3 ++ src/app/utils/cost-range.spec.ts | 43 +++++++++++++++++++ src/app/utils/cost-range.ts | 5 +++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/app/utils/cost-range.spec.ts create mode 100644 src/app/utils/cost-range.ts diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.html b/src/app/carbon-estimator-form/carbon-estimator-form.component.html index 416abb0..22ef1f7 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.html +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.html @@ -141,7 +141,8 @@ class="tce-border tce-border-slate-400 tce-rounded disabled:tce-bg-gray-200 disabled:tce-text-slate-400 disabled:tce-cursor-not-allowed tce-px-3 tce-py-2" formControlName="monthlyCloudBill" name="monthlyCloudBill" - required> + required + [compareWith]="compareCostRanges"> @for (range of costRanges; track $index) { } diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index ff3f5a6..e432e24 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -18,6 +18,7 @@ import { ExpansionPanelComponent } from '../expansion-panel/expansion-panel.comp import { FormatCostRangePipe } from '../pipes/format-cost-range.pipe'; import { InvalidatedPipe } from '../pipes/invalidated.pipe'; import { StorageService } from '../services/storage.service'; +import { compareCostRanges } from '../utils/cost-range'; const locationDescriptions: Record = { WORLD: 'Globally', @@ -79,6 +80,8 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { public questionPanelConfig = questionPanelConfig; + public compareCostRanges = compareCostRanges; + constructor( private formBuilder: FormBuilder, private changeDetector: ChangeDetectorRef, diff --git a/src/app/utils/cost-range.spec.ts b/src/app/utils/cost-range.spec.ts new file mode 100644 index 0000000..a7f0fd0 --- /dev/null +++ b/src/app/utils/cost-range.spec.ts @@ -0,0 +1,43 @@ +import { CostRange } from '../types/carbon-estimator'; +import { compareCostRanges } from './cost-range'; + +describe('compareCostRanges', () => { + it('should return true when the cost ranges are the same', () => { + const range1: CostRange = { + min: 10, + max: 20, + }; + const range2: CostRange = { + min: 10, + max: 20, + }; + + expect(compareCostRanges(range1, range2)).toBeTrue(); + }); + + it('should return false when the cost ranges have different min values', () => { + const range1: CostRange = { + min: 5, + max: 20, + }; + const range2: CostRange = { + min: 10, + max: 20, + }; + + expect(compareCostRanges(range1, range2)).toBeFalse(); + }); + + it('should return false when the cost ranges have different max values', () => { + const range1: CostRange = { + min: 10, + max: 20, + }; + const range2: CostRange = { + min: 10, + max: 30, + }; + + expect(compareCostRanges(range1, range2)).toBeFalse(); + }); +}); diff --git a/src/app/utils/cost-range.ts b/src/app/utils/cost-range.ts new file mode 100644 index 0000000..a5d9923 --- /dev/null +++ b/src/app/utils/cost-range.ts @@ -0,0 +1,5 @@ +import { CostRange } from '../types/carbon-estimator'; + +export const compareCostRanges = (r1: CostRange, r2: CostRange) => { + return r1.min === r2.min && r1.max === r2.max; +}; From 5c2b588fc922c368366e0b9e39db5c20bb8a7c89 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Thu, 1 Aug 2024 14:35:31 +0100 Subject: [PATCH 07/27] Add method to get saved data --- .../carbon-estimator-form.component.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index e432e24..0f82a93 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -169,10 +169,10 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { this.estimatorForm.setValue(formValue); } - const storedFormData = this.storageService.get('formData'); + const storedFormData = this.getSavedFormData(); if (storedFormData) { - this.estimatorForm.setValue(JSON.parse(storedFormData)); + this.estimatorForm.setValue(storedFormData); } } @@ -227,6 +227,11 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { this.storageService.set('formData', JSON.stringify(this.estimatorForm.getRawValue())); } + private getSavedFormData() { + const storedFormData = this.storageService.get('formData'); + return storedFormData ? (JSON.parse(storedFormData) as EstimatorValues) : null; + } + @HostListener('window:beforeunload', ['$event']) onBeforeUnload(): void { this.saveFormData(); From dbd8d293b991ed9b07ebd867f941ab20d444d268 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Fri, 2 Aug 2024 10:51:29 +0100 Subject: [PATCH 08/27] Save control states --- .../carbon-estimator-form.component.ts | 36 ++++++++++++++++--- .../carbon-estimator-form.constants.ts | 5 +++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 0f82a93..94cd1ed 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -11,7 +11,13 @@ import { } from '@angular/core'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { EstimatorFormValues, EstimatorValues, WorldLocation, locationArray } from '../types/carbon-estimator'; -import { costRanges, defaultValues, formContext, questionPanelConfig } from './carbon-estimator-form.constants'; +import { + ControlState, + costRanges, + defaultValues, + formContext, + questionPanelConfig, +} from './carbon-estimator-form.constants'; import { NoteComponent } from '../note/note.component'; import { CarbonEstimationService } from '../services/carbon-estimation.service'; import { ExpansionPanelComponent } from '../expansion-panel/expansion-panel.component'; @@ -177,7 +183,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.saveFormData(); + this.saveFormState(); } public handleSubmit() { @@ -199,7 +205,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { public resetForm() { this.estimatorForm.reset(); - this.saveFormData(); + this.saveFormState(); this.formReset.emit(); } @@ -232,8 +238,30 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { return storedFormData ? (JSON.parse(storedFormData) as EstimatorValues) : null; } + private saveControlStates() { + const controlStates: Record = {}; + + for (const [groupName, formGroup] of Object.entries(this.estimatorForm.controls)) { + for (const [controlName, control] of Object.entries(formGroup.controls)) { + if (control.dirty || control.touched) { + controlStates[`${groupName}.${controlName}`] = { + dirty: control.dirty, + touched: control.touched, + }; + } + } + } + + this.storageService.set('controlStates', JSON.stringify(controlStates)); + } + + private saveFormState() { + this.saveFormData(); + this.saveControlStates(); + } + @HostListener('window:beforeunload', ['$event']) onBeforeUnload(): void { - this.saveFormData(); + this.saveFormState(); } } diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts b/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts index c314d2a..aae318d 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts @@ -97,3 +97,8 @@ export const questionPanelConfig: ExpansionPanelConfig = { buttonStyles: 'material-icons-outlined tce-text-base hover:tce-bg-slate-200 hover:tce-rounded', contentContainerStyles: 'tce-px-3 tce-py-2 tce-bg-slate-100 tce-border tce-border-slate-400 tce-rounded tce-text-sm', }; + +export type ControlState = { + dirty: boolean; + touched: boolean; +}; From 72f66220263d7295e7d93cce3c6508c312f20d49 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Fri, 2 Aug 2024 11:13:46 +0100 Subject: [PATCH 09/27] Restore stored control states on init --- .../carbon-estimator-form.component.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 94cd1ed..1fdcdce 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -180,6 +180,20 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { if (storedFormData) { this.estimatorForm.setValue(storedFormData); } + + const storedControlStates = this.getSavedControlStates(); + + if (storedControlStates) { + for (const [controlKey, controlState] of Object.entries(storedControlStates)) { + const control = this.estimatorForm.get(controlKey)!; + if (controlState.dirty) { + control.markAsDirty(); + } + if (controlState.touched) { + control.markAsTouched(); + } + } + } } ngOnDestroy(): void { @@ -255,6 +269,11 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { this.storageService.set('controlStates', JSON.stringify(controlStates)); } + private getSavedControlStates() { + const storedControlStates = this.storageService.get('controlStates'); + return storedControlStates ? (JSON.parse(storedControlStates) as Record) : null; + } + private saveFormState() { this.saveFormData(); this.saveControlStates(); From 11cb1bea8764e6d27773f8f1c298ca3773dcea79 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Fri, 2 Aug 2024 11:22:13 +0100 Subject: [PATCH 10/27] Rename storage methods --- .../carbon-estimator-form.component.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 1fdcdce..ae95a78 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -175,13 +175,13 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { this.estimatorForm.setValue(formValue); } - const storedFormData = this.getSavedFormData(); + const storedFormData = this.getStoredFormData(); if (storedFormData) { this.estimatorForm.setValue(storedFormData); } - const storedControlStates = this.getSavedControlStates(); + const storedControlStates = this.getStoredControlStates(); if (storedControlStates) { for (const [controlKey, controlState] of Object.entries(storedControlStates)) { @@ -197,7 +197,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.saveFormState(); + this.storeFormState(); } public handleSubmit() { @@ -219,7 +219,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { public resetForm() { this.estimatorForm.reset(); - this.saveFormState(); + this.storeFormState(); this.formReset.emit(); } @@ -243,16 +243,16 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { } } - private saveFormData() { + private storeFormData() { this.storageService.set('formData', JSON.stringify(this.estimatorForm.getRawValue())); } - private getSavedFormData() { + private getStoredFormData() { const storedFormData = this.storageService.get('formData'); return storedFormData ? (JSON.parse(storedFormData) as EstimatorValues) : null; } - private saveControlStates() { + private storeControlStates() { const controlStates: Record = {}; for (const [groupName, formGroup] of Object.entries(this.estimatorForm.controls)) { @@ -269,18 +269,18 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { this.storageService.set('controlStates', JSON.stringify(controlStates)); } - private getSavedControlStates() { + private getStoredControlStates() { const storedControlStates = this.storageService.get('controlStates'); return storedControlStates ? (JSON.parse(storedControlStates) as Record) : null; } - private saveFormState() { - this.saveFormData(); - this.saveControlStates(); + private storeFormState() { + this.storeFormData(); + this.storeControlStates(); } @HostListener('window:beforeunload', ['$event']) onBeforeUnload(): void { - this.saveFormState(); + this.storeFormState(); } } From 8748ee769b549bcc25ecaba6138ca5cf1c87ea25 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Fri, 2 Aug 2024 12:54:50 +0100 Subject: [PATCH 11/27] Remove onInit calls from tests --- .../carbon-estimator-form.component.spec.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts index 921de7c..727a189 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts @@ -18,7 +18,6 @@ describe('CarbonEstimatorFormComponent', () => { it('should create component form in a valid state', () => { expect(component).toBeTruthy(); - component.ngOnInit(); expect(component.estimatorForm.valid).toBeTruthy(); }); @@ -32,14 +31,12 @@ describe('CarbonEstimatorFormComponent', () => { describe('Downstream', () => { it('should invalidate form when monthly active users are zero', () => { - component.ngOnInit(); component.estimatorForm.get('downstream.monthlyActiveUsers')?.setValue(0); fixture.detectChanges(); expect(component.estimatorForm.valid).toBeFalsy(); }); it('should validate form when downstream is excluded and monthly active users are zero', () => { - component.ngOnInit(); component.estimatorForm.get('downstream.monthlyActiveUsers')?.setValue(0); component.estimatorForm.get('downstream.noDownstream')?.setValue(true); fixture.detectChanges(); @@ -49,21 +46,18 @@ describe('CarbonEstimatorFormComponent', () => { describe('headCount()', () => { it('should not return null once the component is initialized', () => { - component.ngOnInit(); expect(component.headCount).not.toBeNull(); }); }); describe('numberOfServers()', () => { it('should not return null once the component is initialized', () => { - component.ngOnInit(); expect(component.numberOfServers).not.toBeNull(); }); }); describe('monthlyActiveUsers()', () => { it('should not return null once the component is initialized', () => { - component.ngOnInit(); expect(component.monthlyActiveUsers).not.toBeNull(); }); }); From 7645c27a6556a31ec64b21aabaa5428c5a2f85e1 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Mon, 5 Aug 2024 14:45:07 +0100 Subject: [PATCH 12/27] Mock storage service in tests --- .../carbon-estimator-form.component.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts index 727a189..3702389 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts @@ -1,6 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CarbonEstimatorFormComponent } from './carbon-estimator-form.component'; +import { StorageService } from '../services/storage.service'; + +class MockStorageService { + storage: Record = {}; + + get(key: string): string | null { + return this.storage[key] || null; + } + + set(key: string, value: string): void { + this.storage[key] = value; + } +} describe('CarbonEstimatorFormComponent', () => { let component: CarbonEstimatorFormComponent; @@ -9,6 +22,7 @@ describe('CarbonEstimatorFormComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [CarbonEstimatorFormComponent], + providers: [{ provide: StorageService, useClass: MockStorageService }], }).compileComponents(); fixture = TestBed.createComponent(CarbonEstimatorFormComponent); From 9911d9d8d292f357ee2642b815161c1085f4157d Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Mon, 5 Aug 2024 14:48:34 +0100 Subject: [PATCH 13/27] Add explicit return types to storage service methods --- src/app/services/storage.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/services/storage.service.ts b/src/app/services/storage.service.ts index 0dfe660..bfcb013 100644 --- a/src/app/services/storage.service.ts +++ b/src/app/services/storage.service.ts @@ -11,11 +11,11 @@ export const BROWSER_STORAGE = new InjectionToken('Browser Storage', { export class StorageService { constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {} - get(key: string) { + get(key: string): string | null { return this.storage.getItem(key); } - set(key: string, value: string) { + set(key: string, value: string): void { return this.storage.setItem(key, value); } } From 44e66d822340b50391cd22c672378962379ea9d3 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Mon, 5 Aug 2024 15:03:04 +0100 Subject: [PATCH 14/27] Move HostListener to top of class --- .../carbon-estimator-form.component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index e7d1bbd..a9c5223 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -58,6 +58,11 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { @ViewChild(ErrorSummaryComponent) errorSummary?: ErrorSummaryComponent; + @HostListener('window:beforeunload', ['$event']) + onBeforeUnload(): void { + this.storeFormState(); + } + public estimatorForm!: FormGroup; public formContext = formContext; @@ -301,9 +306,4 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { this.storeFormData(); this.storeControlStates(); } - - @HostListener('window:beforeunload', ['$event']) - onBeforeUnload(): void { - this.storeFormState(); - } } From 56da1dffaacbe7c37d113229b1718479c9dd73b3 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Mon, 5 Aug 2024 16:11:51 +0100 Subject: [PATCH 15/27] Persist error summary state --- .../carbon-estimator-form.component.ts | 22 +++++++++++++++++++ .../carbon-estimator-form.constants.ts | 5 +++++ 2 files changed, 27 insertions(+) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index a9c5223..1605496 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -21,6 +21,7 @@ import { ValidationError, errorConfig, ControlState, + ErrorSummaryState, } from './carbon-estimator-form.constants'; import { NoteComponent } from '../note/note.component'; import { CarbonEstimationService } from '../services/carbon-estimation.service'; @@ -202,6 +203,13 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { } } } + + const storedErrorSummaryState = this.getStoredErrorSummaryState(); + + if (storedErrorSummaryState) { + this.showErrorSummary = storedErrorSummaryState.showErrorSummary; + this.validationErrors = storedErrorSummaryState.validationErrors; + } } ngOnDestroy(): void { @@ -302,8 +310,22 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { return storedControlStates ? (JSON.parse(storedControlStates) as Record) : null; } + private storeErrorSummaryState() { + const errorSummaryState: ErrorSummaryState = { + showErrorSummary: this.showErrorSummary, + validationErrors: this.validationErrors, + }; + this.storageService.set('errorSummaryState', JSON.stringify(errorSummaryState)); + } + + private getStoredErrorSummaryState() { + const storedErrorSummaryState = this.storageService.get('errorSummaryState'); + return storedErrorSummaryState ? (JSON.parse(storedErrorSummaryState) as ErrorSummaryState) : null; + } + private storeFormState() { this.storeFormData(); this.storeControlStates(); + this.storeErrorSummaryState(); } } diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts b/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts index 45c5601..4db2fbf 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts @@ -133,3 +133,8 @@ export type ControlState = { dirty: boolean; touched: boolean; }; + +export type ErrorSummaryState = { + showErrorSummary: boolean; + validationErrors: ValidationError[]; +}; From 67c1f792b0b5493d915f67f7ad7dcce544f44d70 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Tue, 6 Aug 2024 10:27:46 +0100 Subject: [PATCH 16/27] Store state in single storage variable --- .../carbon-estimator-form.component.ts | 62 +++++++------------ .../carbon-estimator-form.constants.ts | 11 +++- 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 1605496..5463e28 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -22,6 +22,7 @@ import { errorConfig, ControlState, ErrorSummaryState, + FormState, } from './carbon-estimator-form.constants'; import { NoteComponent } from '../note/note.component'; import { CarbonEstimationService } from '../services/carbon-estimation.service'; @@ -184,16 +185,12 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { this.estimatorForm.setValue(formValue); } - const storedFormData = this.getStoredFormData(); + const storedFormState = this.getStoredFormState(); - if (storedFormData) { - this.estimatorForm.setValue(storedFormData); - } - - const storedControlStates = this.getStoredControlStates(); + if (storedFormState) { + this.estimatorForm.setValue(storedFormState.formValue); - if (storedControlStates) { - for (const [controlKey, controlState] of Object.entries(storedControlStates)) { + for (const [controlKey, controlState] of Object.entries(storedFormState.controlStates)) { const control = this.estimatorForm.get(controlKey)!; if (controlState.dirty) { control.markAsDirty(); @@ -202,13 +199,9 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { control.markAsTouched(); } } - } - - const storedErrorSummaryState = this.getStoredErrorSummaryState(); - if (storedErrorSummaryState) { - this.showErrorSummary = storedErrorSummaryState.showErrorSummary; - this.validationErrors = storedErrorSummaryState.validationErrors; + this.showErrorSummary = storedFormState.errorSummaryState.showErrorSummary; + this.validationErrors = storedFormState.errorSummaryState.validationErrors; } } @@ -279,16 +272,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { return validationErrors; } - private storeFormData() { - this.storageService.set('formData', JSON.stringify(this.estimatorForm.getRawValue())); - } - - private getStoredFormData() { - const storedFormData = this.storageService.get('formData'); - return storedFormData ? (JSON.parse(storedFormData) as EstimatorValues) : null; - } - - private storeControlStates() { + private getControlStates() { const controlStates: Record = {}; for (const [groupName, formGroup] of Object.entries(this.estimatorForm.controls)) { @@ -302,30 +286,26 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { } } - this.storageService.set('controlStates', JSON.stringify(controlStates)); + return controlStates; } - private getStoredControlStates() { - const storedControlStates = this.storageService.get('controlStates'); - return storedControlStates ? (JSON.parse(storedControlStates) as Record) : null; - } - - private storeErrorSummaryState() { + private storeFormState() { + const formValue = this.estimatorForm.getRawValue(); + const controlStates = this.getControlStates(); const errorSummaryState: ErrorSummaryState = { showErrorSummary: this.showErrorSummary, validationErrors: this.validationErrors, }; - this.storageService.set('errorSummaryState', JSON.stringify(errorSummaryState)); - } - - private getStoredErrorSummaryState() { - const storedErrorSummaryState = this.storageService.get('errorSummaryState'); - return storedErrorSummaryState ? (JSON.parse(storedErrorSummaryState) as ErrorSummaryState) : null; + const formState: FormState = { + formValue, + controlStates, + errorSummaryState, + }; + this.storageService.set('formState', JSON.stringify(formState)); } - private storeFormState() { - this.storeFormData(); - this.storeControlStates(); - this.storeErrorSummaryState(); + private getStoredFormState() { + const storedFormState = this.storageService.get('formState'); + return storedFormState ? (JSON.parse(storedFormState) as FormState) : null; } } diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts b/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts index 4db2fbf..b656055 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.constants.ts @@ -1,5 +1,6 @@ +import { FormGroup } from '@angular/forms'; import { ExpansionPanelConfig } from '../expansion-panel/expansion-panel.constants'; -import { CostRange, EstimatorValues, WorldLocation } from '../types/carbon-estimator'; +import { CostRange, EstimatorFormValues, EstimatorValues, WorldLocation } from '../types/carbon-estimator'; export const costRanges: CostRange[] = [ { min: 0, max: 1000 }, @@ -129,6 +130,8 @@ export const errorConfig = { }, }; +export type EstimatorFormRawValue = ReturnType['getRawValue']>; + export type ControlState = { dirty: boolean; touched: boolean; @@ -138,3 +141,9 @@ export type ErrorSummaryState = { showErrorSummary: boolean; validationErrors: ValidationError[]; }; + +export type FormState = { + formValue: EstimatorFormRawValue; + controlStates: Record; + errorSummaryState: ErrorSummaryState; +}; From 9cfdf2dde8342f43e98388cd8d73187d754e4ac1 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Tue, 6 Aug 2024 10:47:28 +0100 Subject: [PATCH 17/27] Add setControlStates method --- .../carbon-estimator-form.component.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 5463e28..fe6c845 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -189,17 +189,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { if (storedFormState) { this.estimatorForm.setValue(storedFormState.formValue); - - for (const [controlKey, controlState] of Object.entries(storedFormState.controlStates)) { - const control = this.estimatorForm.get(controlKey)!; - if (controlState.dirty) { - control.markAsDirty(); - } - if (controlState.touched) { - control.markAsTouched(); - } - } - + this.setControlStates(storedFormState.controlStates); this.showErrorSummary = storedFormState.errorSummaryState.showErrorSummary; this.validationErrors = storedFormState.errorSummaryState.validationErrors; } @@ -289,6 +279,18 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { return controlStates; } + private setControlStates(controlStates: Record) { + for (const [controlKey, controlState] of Object.entries(controlStates)) { + const control = this.estimatorForm.get(controlKey); + if (controlState.dirty) { + control?.markAsDirty(); + } + if (controlState.touched) { + control?.markAsTouched(); + } + } + } + private storeFormState() { const formValue = this.estimatorForm.getRawValue(); const controlStates = this.getControlStates(); From 31b33bc1a1a756a2c39f84f424b7cd57e3132851 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Tue, 6 Aug 2024 11:04:06 +0100 Subject: [PATCH 18/27] Clear stored state on form reset --- .../carbon-estimator-form.component.spec.ts | 4 ++++ .../carbon-estimator-form.component.ts | 6 +++++- src/app/services/storage.service.ts | 6 +++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts index 3702389..625d71b 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts @@ -13,6 +13,10 @@ class MockStorageService { set(key: string, value: string): void { this.storage[key] = value; } + + removeItem(key: string): void { + delete this.storage[key]; + } } describe('CarbonEstimatorFormComponent', () => { diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index fe6c845..f32a3f3 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -223,7 +223,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { public resetForm() { this.estimatorForm.reset(); - this.storeFormState(); + this.clearStoredFormState(); this.formReset.emit(); } @@ -310,4 +310,8 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { const storedFormState = this.storageService.get('formState'); return storedFormState ? (JSON.parse(storedFormState) as FormState) : null; } + + private clearStoredFormState() { + this.storageService.removeItem('formState'); + } } diff --git a/src/app/services/storage.service.ts b/src/app/services/storage.service.ts index bfcb013..394596a 100644 --- a/src/app/services/storage.service.ts +++ b/src/app/services/storage.service.ts @@ -16,6 +16,10 @@ export class StorageService { } set(key: string, value: string): void { - return this.storage.setItem(key, value); + this.storage.setItem(key, value); + } + + removeItem(key: string): void { + this.storage.removeItem(key); } } From 83a80e370e83d9ab31a0e88e49b50f95199fadf2 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Tue, 6 Aug 2024 11:29:49 +0100 Subject: [PATCH 19/27] Use Map in MockStorageService --- .../carbon-estimator-form.component.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts index 625d71b..4b68b1d 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts @@ -4,18 +4,18 @@ import { CarbonEstimatorFormComponent } from './carbon-estimator-form.component' import { StorageService } from '../services/storage.service'; class MockStorageService { - storage: Record = {}; + storage = new Map(); get(key: string): string | null { - return this.storage[key] || null; + return this.storage.get(key) || null; } set(key: string, value: string): void { - this.storage[key] = value; + this.storage.set(key, value); } removeItem(key: string): void { - delete this.storage[key]; + this.storage.delete(key); } } From 1cb72f581a0add4bbf1dbb8c9f0d9ef5d68cf806 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Tue, 6 Aug 2024 12:12:00 +0100 Subject: [PATCH 20/27] Refactor storeFormState --- .../carbon-estimator-form.component.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index f32a3f3..0005a25 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -21,7 +21,6 @@ import { ValidationError, errorConfig, ControlState, - ErrorSummaryState, FormState, } from './carbon-estimator-form.constants'; import { NoteComponent } from '../note/note.component'; @@ -292,16 +291,13 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { } private storeFormState() { - const formValue = this.estimatorForm.getRawValue(); - const controlStates = this.getControlStates(); - const errorSummaryState: ErrorSummaryState = { - showErrorSummary: this.showErrorSummary, - validationErrors: this.validationErrors, - }; const formState: FormState = { - formValue, - controlStates, - errorSummaryState, + formValue: this.estimatorForm.getRawValue(), + controlStates: this.getControlStates(), + errorSummaryState: { + showErrorSummary: this.showErrorSummary, + validationErrors: this.validationErrors, + }, }; this.storageService.set('formState', JSON.stringify(formState)); } From 5c27f3d9245e4d9b8dda90aa07340a7bb6ee5a63 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Tue, 6 Aug 2024 12:39:29 +0100 Subject: [PATCH 21/27] Refactor error summary state --- .../carbon-estimator-form.component.html | 4 +-- .../carbon-estimator-form.component.ts | 26 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.html b/src/app/carbon-estimator-form/carbon-estimator-form.component.html index 9f2659d..aa957e7 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.html +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.html @@ -1,7 +1,7 @@
- @if (showErrorSummary) { - + @if (errorSummaryState.showErrorSummary) { + } diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 0005a25..da8c443 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -22,6 +22,7 @@ import { errorConfig, ControlState, FormState, + ErrorSummaryState, } from './carbon-estimator-form.constants'; import { NoteComponent } from '../note/note.component'; import { CarbonEstimationService } from '../services/carbon-estimation.service'; @@ -92,8 +93,10 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { public questionPanelConfig = questionPanelConfig; public errorConfig = errorConfig; - public showErrorSummary = false; - public validationErrors: ValidationError[] = []; + public errorSummaryState: ErrorSummaryState = { + showErrorSummary: false, + validationErrors: [], + }; public compareCostRanges = compareCostRanges; @@ -189,8 +192,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { if (storedFormState) { this.estimatorForm.setValue(storedFormState.formValue); this.setControlStates(storedFormState.controlStates); - this.showErrorSummary = storedFormState.errorSummaryState.showErrorSummary; - this.validationErrors = storedFormState.errorSummaryState.validationErrors; + this.errorSummaryState = storedFormState.errorSummaryState; } } @@ -200,13 +202,18 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { public handleSubmit() { if (this.estimatorForm.invalid) { - this.validationErrors = this.getValidationErrors(); - this.showErrorSummary = true; + this.errorSummaryState = { + showErrorSummary: true, + validationErrors: this.getValidationErrors(), + }; this.changeDetector.detectChanges(); this.errorSummary?.summary.nativeElement.focus(); return; } - this.showErrorSummary = false; + this.errorSummaryState = { + showErrorSummary: false, + validationErrors: [], + }; const formValue = this.estimatorForm.getRawValue(); if (formValue.onPremise.serverLocation === 'unknown') { formValue.onPremise.serverLocation = 'WORLD'; @@ -294,10 +301,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { const formState: FormState = { formValue: this.estimatorForm.getRawValue(), controlStates: this.getControlStates(), - errorSummaryState: { - showErrorSummary: this.showErrorSummary, - validationErrors: this.validationErrors, - }, + errorSummaryState: this.errorSummaryState, }; this.storageService.set('formState', JSON.stringify(formState)); } From 54572afe8617d20d06110bacfc2762a442e999b5 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Wed, 7 Aug 2024 09:25:24 +0100 Subject: [PATCH 22/27] Add form state service --- .../carbon-estimator-form.component.ts | 44 ++------------ src/app/services/form-state.service.spec.ts | 16 +++++ src/app/services/form-state.service.ts | 59 +++++++++++++++++++ 3 files changed, 81 insertions(+), 38 deletions(-) create mode 100644 src/app/services/form-state.service.spec.ts create mode 100644 src/app/services/form-state.service.ts diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index da8c443..1495d3d 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -21,7 +21,6 @@ import { ValidationError, errorConfig, ControlState, - FormState, ErrorSummaryState, } from './carbon-estimator-form.constants'; import { NoteComponent } from '../note/note.component'; @@ -31,8 +30,8 @@ import { FormatCostRangePipe } from '../pipes/format-cost-range.pipe'; import { InvalidatedPipe } from '../pipes/invalidated.pipe'; import { ErrorSummaryComponent } from '../error-summary/error-summary.component'; import { ExternalLinkDirective } from '../directives/external-link.directive'; -import { StorageService } from '../services/storage.service'; import { compareCostRanges } from '../utils/cost-range'; +import { FormStateService } from '../services/form-state.service'; @Component({ selector: 'carbon-estimator-form', @@ -104,7 +103,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { private formBuilder: FormBuilder, private changeDetector: ChangeDetectorRef, private estimationService: CarbonEstimationService, - private storageService: StorageService + private formStateService: FormStateService ) {} public ngOnInit() { @@ -268,50 +267,19 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { return validationErrors; } - private getControlStates() { - const controlStates: Record = {}; - - for (const [groupName, formGroup] of Object.entries(this.estimatorForm.controls)) { - for (const [controlName, control] of Object.entries(formGroup.controls)) { - if (control.dirty || control.touched) { - controlStates[`${groupName}.${controlName}`] = { - dirty: control.dirty, - touched: control.touched, - }; - } - } - } - - return controlStates; - } - private setControlStates(controlStates: Record) { - for (const [controlKey, controlState] of Object.entries(controlStates)) { - const control = this.estimatorForm.get(controlKey); - if (controlState.dirty) { - control?.markAsDirty(); - } - if (controlState.touched) { - control?.markAsTouched(); - } - } + this.formStateService.setControlStates(this.estimatorForm, controlStates); } private storeFormState() { - const formState: FormState = { - formValue: this.estimatorForm.getRawValue(), - controlStates: this.getControlStates(), - errorSummaryState: this.errorSummaryState, - }; - this.storageService.set('formState', JSON.stringify(formState)); + this.formStateService.storeFormState(this.estimatorForm, this.errorSummaryState); } private getStoredFormState() { - const storedFormState = this.storageService.get('formState'); - return storedFormState ? (JSON.parse(storedFormState) as FormState) : null; + return this.formStateService.getStoredFormState(); } private clearStoredFormState() { - this.storageService.removeItem('formState'); + this.formStateService.clearStoredFormState(); } } diff --git a/src/app/services/form-state.service.spec.ts b/src/app/services/form-state.service.spec.ts new file mode 100644 index 0000000..d523bdb --- /dev/null +++ b/src/app/services/form-state.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FormStateService } from './form-state.service'; + +describe('FormStateService', () => { + let service: FormStateService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FormStateService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/form-state.service.ts b/src/app/services/form-state.service.ts new file mode 100644 index 0000000..0139d5f --- /dev/null +++ b/src/app/services/form-state.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { StorageService } from './storage.service'; +import { FormGroup } from '@angular/forms'; +import { ControlState, ErrorSummaryState, FormState } from '../carbon-estimator-form/carbon-estimator-form.constants'; +import { EstimatorFormValues } from '../types/carbon-estimator'; + +@Injectable({ + providedIn: 'root', +}) +export class FormStateService { + constructor(private storageService: StorageService) {} + + getControlStates(estimatorForm: FormGroup) { + const controlStates: Record = {}; + + for (const [groupName, formGroup] of Object.entries(estimatorForm.controls)) { + for (const [controlName, control] of Object.entries(formGroup.controls)) { + if (control.dirty || control.touched) { + controlStates[`${groupName}.${controlName}`] = { + dirty: control.dirty, + touched: control.touched, + }; + } + } + } + + return controlStates; + } + + setControlStates(estimatorForm: FormGroup, controlStates: Record) { + for (const [controlKey, controlState] of Object.entries(controlStates)) { + const control = estimatorForm.get(controlKey); + if (controlState.dirty) { + control?.markAsDirty(); + } + if (controlState.touched) { + control?.markAsTouched(); + } + } + } + + storeFormState(estimatorForm: FormGroup, errorSummaryState: ErrorSummaryState) { + const formState: FormState = { + formValue: estimatorForm.getRawValue(), + controlStates: this.getControlStates(estimatorForm), + errorSummaryState, + }; + this.storageService.set('formState', JSON.stringify(formState)); + } + + getStoredFormState() { + const storedFormState = this.storageService.get('formState'); + return storedFormState ? (JSON.parse(storedFormState) as FormState) : null; + } + + clearStoredFormState() { + this.storageService.removeItem('formState'); + } +} From bdb72043ec7e3f16dc9bacd85d79647b1e2ce316 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Wed, 7 Aug 2024 10:51:32 +0100 Subject: [PATCH 23/27] Add tests for control state methods --- src/app/services/form-state.service.spec.ts | 166 ++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/src/app/services/form-state.service.spec.ts b/src/app/services/form-state.service.spec.ts index d523bdb..c237179 100644 --- a/src/app/services/form-state.service.spec.ts +++ b/src/app/services/form-state.service.spec.ts @@ -1,16 +1,182 @@ import { TestBed } from '@angular/core/testing'; import { FormStateService } from './form-state.service'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { EstimatorFormValues, EstimatorValues, WorldLocation } from '../types/carbon-estimator'; +import { costRanges } from '../carbon-estimator-form/carbon-estimator-form.constants'; + +const formValues: EstimatorValues = { + upstream: { + headCount: 100, + desktopPercentage: 50, + employeeLocation: 'WORLD', + }, + onPremise: { + estimateServerCount: false, + serverLocation: 'WORLD', + numberOfServers: 10, + }, + cloud: { + noCloudServices: false, + cloudLocation: 'WORLD', + cloudPercentage: 50, + monthlyCloudBill: costRanges[0], + }, + downstream: { + noDownstream: false, + customerLocation: 'WORLD', + monthlyActiveUsers: 100, + mobilePercentage: 50, + purposeOfSite: 'average', + }, +}; + +const formBuilder = new FormBuilder(); describe('FormStateService', () => { let service: FormStateService; + let estimatorForm: FormGroup; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(FormStateService); + estimatorForm = formBuilder.nonNullable.group({ + upstream: formBuilder.nonNullable.group({ + headCount: [formValues.upstream.headCount], + desktopPercentage: [formValues.upstream.desktopPercentage], + employeeLocation: [formValues.upstream.employeeLocation], + }), + onPremise: formBuilder.nonNullable.group({ + estimateServerCount: [formValues.onPremise.estimateServerCount], + serverLocation: [formValues.onPremise.serverLocation as WorldLocation | 'unknown'], + numberOfServers: [formValues.onPremise.numberOfServers], + }), + cloud: formBuilder.nonNullable.group({ + noCloudServices: [false], + cloudLocation: [formValues.cloud.cloudLocation as WorldLocation | 'unknown'], + cloudPercentage: [formValues.cloud.cloudPercentage], + monthlyCloudBill: [formValues.cloud.monthlyCloudBill], + }), + downstream: formBuilder.nonNullable.group({ + noDownstream: [false], + customerLocation: [formValues.downstream.customerLocation], + monthlyActiveUsers: [formValues.downstream.monthlyActiveUsers], + mobilePercentage: [formValues.downstream.mobilePercentage], + purposeOfSite: [formValues.downstream.purposeOfSite], + }), + }); }); it('should be created', () => { expect(service).toBeTruthy(); }); + + describe('getControlStates()', () => { + it('should return the states of controls that are marked as dirty or touched', () => { + estimatorForm.get('upstream.headCount')?.markAsDirty(); + estimatorForm.get('upstream.headCount')?.markAsTouched(); + estimatorForm.get('onPremise.serverLocation')?.markAsDirty(); + estimatorForm.get('cloud.cloudLocation')?.markAsTouched(); + + const controlStates = service.getControlStates(estimatorForm); + const expectedControlStates = { + 'upstream.headCount': { + dirty: true, + touched: true, + }, + 'onPremise.serverLocation': { + dirty: true, + touched: false, + }, + 'cloud.cloudLocation': { + dirty: false, + touched: true, + }, + }; + + expect(controlStates).toEqual(expectedControlStates); + }); + }); + + describe('setControlStates()', () => { + const singleControlTestCases = [ + { + controlName: 'upstream.headCount', + dirty: true, + touched: true, + }, + { + controlName: 'upstream.headCount', + dirty: true, + touched: false, + }, + { + controlName: 'upstream.headCount', + dirty: false, + touched: true, + }, + ]; + + singleControlTestCases.forEach(testCase => { + it(`should set the control state on the form when dirty = ${testCase.dirty} and touched = ${testCase.touched}`, () => { + const controlStates = { + [testCase.controlName]: { + dirty: testCase.dirty, + touched: testCase.touched, + }, + }; + + service.setControlStates(estimatorForm, controlStates); + const control = estimatorForm.get(testCase.controlName); + const controlState = { + dirty: control?.dirty, + touched: control?.touched, + }; + const expectedControlState = { + dirty: testCase.dirty, + touched: testCase.touched, + }; + + expect(controlState).toEqual(expectedControlState); + }); + }); + + it('should set all the control states when passed multiple control states', () => { + const controlStatesArg = { + 'upstream.headCount': { + dirty: true, + touched: true, + }, + 'onPremise.serverLocation': { + dirty: true, + touched: false, + }, + 'cloud.cloudLocation': { + dirty: false, + touched: true, + }, + }; + + service.setControlStates(estimatorForm, controlStatesArg); + + const controlStates = { + 'upstream.headCount': { + dirty: estimatorForm.get('upstream.headCount')?.dirty, + touched: estimatorForm.get('upstream.headCount')?.touched, + }, + 'onPremise.serverLocation': { + dirty: estimatorForm.get('onPremise.serverLocation')?.dirty, + touched: estimatorForm.get('onPremise.serverLocation')?.touched, + }, + 'cloud.cloudLocation': { + dirty: estimatorForm.get('cloud.cloudLocation')?.dirty, + touched: estimatorForm.get('cloud.cloudLocation')?.touched, + }, + }; + + const expectedControlStates = controlStatesArg; + + expect(controlStates).toEqual(expectedControlStates); + }); + }); }); From 4b2cdaf33c4b075ea20f3ecc09cfdb43d592d330 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Wed, 7 Aug 2024 15:26:26 +0100 Subject: [PATCH 24/27] Use ?? operator in mock storage service --- .../carbon-estimator-form.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts index 4b68b1d..3405bd8 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts @@ -7,7 +7,7 @@ class MockStorageService { storage = new Map(); get(key: string): string | null { - return this.storage.get(key) || null; + return this.storage.get(key) ?? null; } set(key: string, value: string): void { From 0171e48184d0ae8eeaf1decea676b8014fe73f82 Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Thu, 8 Aug 2024 09:02:10 +0100 Subject: [PATCH 25/27] Add tests for form storage --- .../carbon-estimator-form.component.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts index 3405bd8..5e2b625 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts @@ -22,6 +22,7 @@ class MockStorageService { describe('CarbonEstimatorFormComponent', () => { let component: CarbonEstimatorFormComponent; let fixture: ComponentFixture; + let storageService: StorageService; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -31,6 +32,7 @@ describe('CarbonEstimatorFormComponent', () => { fixture = TestBed.createComponent(CarbonEstimatorFormComponent); component = fixture.componentInstance; + storageService = TestBed.inject(StorageService); fixture.detectChanges(); }); @@ -79,4 +81,20 @@ describe('CarbonEstimatorFormComponent', () => { expect(component.monthlyActiveUsers).not.toBeNull(); }); }); + + describe('form state', () => { + it('should store the form state when the component is destroyed', () => { + spyOn(storageService, 'set'); + component.ngOnDestroy(); + + expect(storageService.set).toHaveBeenCalled(); + }); + + it('should store the state when the user leaves the page', () => { + spyOn(storageService, 'set'); + window.dispatchEvent(new Event('beforeunload')); + + expect(storageService.set).toHaveBeenCalled(); + }); + }); }); From 69679efaf164705ae177892ab8d565619f8c556a Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Thu, 8 Aug 2024 09:16:57 +0100 Subject: [PATCH 26/27] Remove '' from beforeunload listener --- .../carbon-estimator-form/carbon-estimator-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 1495d3d..7d6bf6a 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -59,7 +59,7 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { @ViewChild(ErrorSummaryComponent) errorSummary?: ErrorSummaryComponent; - @HostListener('window:beforeunload', ['$event']) + @HostListener('window:beforeunload') onBeforeUnload(): void { this.storeFormState(); } From 6a5ab6df2743210fa36ffc80b32da584162e72ab Mon Sep 17 00:00:00 2001 From: jantoun-scottlogic Date: Thu, 8 Aug 2024 09:48:17 +0100 Subject: [PATCH 27/27] Use visibilitychange instead of beforeunload --- .../carbon-estimator-form.component.spec.ts | 4 ++-- .../carbon-estimator-form.component.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts index 5e2b625..6133f1b 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.spec.ts @@ -90,9 +90,9 @@ describe('CarbonEstimatorFormComponent', () => { expect(storageService.set).toHaveBeenCalled(); }); - it('should store the state when the user leaves the page', () => { + it('should store the state when the page visibility changes', () => { spyOn(storageService, 'set'); - window.dispatchEvent(new Event('beforeunload')); + document.dispatchEvent(new Event('visibilitychange')); expect(storageService.set).toHaveBeenCalled(); }); diff --git a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts index 7d6bf6a..3dd25e3 100644 --- a/src/app/carbon-estimator-form/carbon-estimator-form.component.ts +++ b/src/app/carbon-estimator-form/carbon-estimator-form.component.ts @@ -59,8 +59,10 @@ export class CarbonEstimatorFormComponent implements OnInit, OnDestroy { @ViewChild(ErrorSummaryComponent) errorSummary?: ErrorSummaryComponent; - @HostListener('window:beforeunload') - onBeforeUnload(): void { + // The visibilitychange event is fired in several scenarios including when the + // user navigates away from the page or switches app on mobile. + @HostListener('document:visibilitychange') + onVisibilityChange(): void { this.storeFormState(); }