@@ -14,9 +18,9 @@
required
[attr.aria-describedby]="(headCount | invalidated) ? 'headCountError' : null" />
@if (headCount | invalidated) {
-
+
error
-
The number of employees must be greater than 0.
+
{{ errorConfig.headCount.errorMessage }}
}
@@ -81,9 +85,9 @@
required
[attr.aria-describedby]="(numberOfServers | invalidated) ? 'numberOfServersError' : null" />
@if (numberOfServers | invalidated) {
-
+
error
-
The number of servers must be greater than or equal to 0.
+
{{ errorConfig.numberOfServers.errorMessage }}
}
@@ -210,12 +214,12 @@
required
[attr.aria-describedby]="(monthlyActiveUsers | invalidated) ? 'monthlyActiveUsersError' : null" />
@if (monthlyActiveUsers | invalidated) {
-
+
error
-
- Monthly active users must be greater than 0. To specify no external users, use the
- checkbox above.
-
+
{{ errorConfig.monthlyActiveUsers.errorMessage }}
}
@@ -258,35 +262,8 @@
-
+
- @if (estimatorForm | invalidated) {
-
- }
= {
- WORLD: 'Globally',
- 'NORTH AMERICA': 'in North America',
- EUROPE: 'in Europe',
- GBR: 'in the UK',
- ASIA: 'in Asia',
- AFRICA: 'in Africa',
- OCEANIA: 'in Oceania',
- 'LATIN AMERICA AND CARIBBEAN': 'in Latin America or the Caribbean',
-};
+import { ErrorSummaryComponent } from '../error-summary/error-summary.component';
@Component({
selector: 'carbon-estimator-form',
@@ -34,6 +32,7 @@ const locationDescriptions: Record = {
ExpansionPanelComponent,
FormatCostRangePipe,
InvalidatedPipe,
+ ErrorSummaryComponent,
],
})
export class CarbonEstimatorFormComponent implements OnInit {
@@ -42,6 +41,8 @@ export class CarbonEstimatorFormComponent implements OnInit {
@Output() public formSubmit: EventEmitter = new EventEmitter();
@Output() public formReset: EventEmitter = new EventEmitter();
+ @ViewChild(ErrorSummaryComponent) errorSummary?: ErrorSummaryComponent;
+
public estimatorForm!: FormGroup;
public formContext = formContext;
@@ -69,6 +70,10 @@ export class CarbonEstimatorFormComponent implements OnInit {
public questionPanelConfig = questionPanelConfig;
+ public errorConfig = errorConfig;
+ public showErrorSummary = false;
+ public validationErrors: ValidationError[] = [];
+
constructor(
private formBuilder: FormBuilder,
private changeDetector: ChangeDetectorRef,
@@ -157,9 +162,14 @@ export class CarbonEstimatorFormComponent implements OnInit {
}
public handleSubmit() {
- if (!this.estimatorForm.valid) {
+ if (this.estimatorForm.invalid) {
+ this.validationErrors = this.getValidationErrors();
+ this.showErrorSummary = true;
+ this.changeDetector.detectChanges();
+ this.errorSummary?.summary.nativeElement.focus();
return;
}
+ this.showErrorSummary = false;
const formValue = this.estimatorForm.getRawValue();
if (formValue.onPremise.serverLocation === 'unknown') {
formValue.onPremise.serverLocation = 'WORLD';
@@ -197,4 +207,19 @@ export class CarbonEstimatorFormComponent implements OnInit {
);
}
}
+
+ private getValidationErrors() {
+ const validationErrors: ValidationError[] = [];
+ if (this.headCount?.invalid) {
+ validationErrors.push(this.errorConfig.headCount);
+ }
+ if (this.numberOfServers?.invalid) {
+ validationErrors.push(this.errorConfig.numberOfServers);
+ }
+ if (this.monthlyActiveUsers?.invalid) {
+ validationErrors.push(this.errorConfig.monthlyActiveUsers);
+ }
+
+ return validationErrors;
+ }
}
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..3669403 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 { ExpansionPanelConfig } from '../expansion-panel/expansion-panel.constants';
import { CostRange, EstimatorValues } from '../types/carbon-estimator';
+import { WorldLocation } from '../types/carbon-estimator';
export const costRanges: CostRange[] = [
{ min: 0, max: 1000 },
@@ -97,3 +98,34 @@ 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 const locationDescriptions: Record = {
+ WORLD: 'Globally',
+ 'NORTH AMERICA': 'in North America',
+ EUROPE: 'in Europe',
+ GBR: 'in the UK',
+ ASIA: 'in Asia',
+ AFRICA: 'in Africa',
+ OCEANIA: 'in Oceania',
+ 'LATIN AMERICA AND CARIBBEAN': 'in Latin America or the Caribbean',
+};
+
+export type ValidationError = {
+ inputId: string;
+ errorMessage: string;
+};
+
+export const errorConfig = {
+ headCount: {
+ inputId: 'headCount',
+ errorMessage: 'The number of employees must be greater than 0',
+ },
+ numberOfServers: {
+ inputId: 'numberOfServers',
+ errorMessage: 'The number of servers must be greater than or equal to 0',
+ },
+ monthlyActiveUsers: {
+ inputId: 'monthlyActiveUsers',
+ errorMessage: 'The number of monthly active users must be greater than 0',
+ },
+};
diff --git a/src/app/error-summary/error-summary.component.html b/src/app/error-summary/error-summary.component.html
new file mode 100644
index 0000000..bc60abe
--- /dev/null
+++ b/src/app/error-summary/error-summary.component.html
@@ -0,0 +1,10 @@
+
diff --git a/src/app/error-summary/error-summary.component.spec.ts b/src/app/error-summary/error-summary.component.spec.ts
new file mode 100644
index 0000000..79de3b3
--- /dev/null
+++ b/src/app/error-summary/error-summary.component.spec.ts
@@ -0,0 +1,42 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ErrorSummaryComponent } from './error-summary.component';
+import { ValidationError } from '../carbon-estimator-form/carbon-estimator-form.constants';
+
+describe('ErrorSummaryComponent', () => {
+ let component: ErrorSummaryComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ErrorSummaryComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ErrorSummaryComponent);
+ component = fixture.componentInstance;
+
+ const validationErrors: ValidationError[] = [
+ {
+ inputId: 'input1',
+ errorMessage: 'Input 1 must be greater than 0',
+ },
+ {
+ inputId: 'input2',
+ errorMessage: 'Input 2 must be greater than 0',
+ },
+ ];
+
+ fixture.componentRef.setInput('validationErrors', validationErrors);
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should display validation error messages', () => {
+ expect(fixture.nativeElement.textContent).toContain('Input 1 must be greater than 0');
+ expect(fixture.nativeElement.textContent).toContain('Input 2 must be greater than 0');
+ });
+});
diff --git a/src/app/error-summary/error-summary.component.ts b/src/app/error-summary/error-summary.component.ts
new file mode 100644
index 0000000..ab4436e
--- /dev/null
+++ b/src/app/error-summary/error-summary.component.ts
@@ -0,0 +1,13 @@
+import { Component, ElementRef, input, ViewChild } from '@angular/core';
+import { ValidationError } from '../carbon-estimator-form/carbon-estimator-form.constants';
+
+@Component({
+ selector: 'error-summary',
+ standalone: true,
+ imports: [],
+ templateUrl: './error-summary.component.html',
+})
+export class ErrorSummaryComponent {
+ validationErrors = input.required();
+ @ViewChild('errorSummary') summary!: ElementRef;
+}
diff --git a/src/app/tech-carbon-estimator/tech-carbon-estimator.component.html b/src/app/tech-carbon-estimator/tech-carbon-estimator.component.html
index 36861c9..f494824 100644
--- a/src/app/tech-carbon-estimator/tech-carbon-estimator.component.html
+++ b/src/app/tech-carbon-estimator/tech-carbon-estimator.component.html
@@ -9,7 +9,7 @@ Technology Carbon Estimator
aria-live="polite"
(closeEvent)="closeAssumptionsAndLimitation($event)">
} @else {
-