@planning-inspectorate/dynamic-forms 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -199,7 +199,88 @@ These conditions combine with individual conditions and section conditions. In t
199
199
  | q6 | q1 = 'yes' | q5 = 90 | N/A |
200
200
  | q7 | q1 = 'yes' | q5 = 90 | N/A |
201
201
 
202
+ ### Email Validation
202
203
 
204
+ Email validation is provided through the `EmailValidator` class and `EmailQuestion` component. The validator uses express-validator's robust email validation with configurable options.
205
+
206
+ #### Basic Usage
207
+
208
+ ```javascript
209
+ import { COMPONENT_TYPES } from '@planning-inspectorate/dynamic-forms/src/index.js';
210
+ import { createQuestions } from '@planning-inspectorate/dynamic-forms/src/questions/create-questions.js';
211
+ import { questionClasses } from '@planning-inspectorate/dynamic-forms/src/questions/questions.js';
212
+ import EmailValidator from '@planning-inspectorate/dynamic-forms/src/validator/email-validator.js';
213
+
214
+ // Define question configuration
215
+ const questionProps = {
216
+ contactEmail: {
217
+ type: COMPONENT_TYPES.EMAIL,
218
+ title: 'Contact Information',
219
+ question: 'What is your email address?',
220
+ fieldName: 'contactEmail',
221
+ url: 'contact-email',
222
+ label: 'Email address',
223
+ validators: [
224
+ new EmailValidator({
225
+ errorMessage: 'Enter an email address in the correct format, like name@example.com'
226
+ })
227
+ ]
228
+ }
229
+ };
230
+
231
+ // Create questions using the factory function
232
+ const questions = createQuestions(questionProps, questionClasses, {});
233
+ const emailQuestion = questions.contactEmail;
234
+ ```
235
+
236
+ The EmailQuestion uses the following default input attributes:
237
+
238
+ - `type="email"`
239
+ - `spellcheck="false"`
240
+ - `autocomplete="email"`
241
+
242
+ #### Advanced Validation Options
243
+
244
+ For stricter email validation requirements:
245
+
246
+ ```javascript
247
+ const businessEmailValidator = new EmailValidator({
248
+ options: {
249
+ allowDisplayName: false, // Reject "Name <email@domain.com>" format
250
+ requireTld: true, // Require top-level domain (default: true)
251
+ allowUtf8LocalPart: false, // Only ASCII characters in local part
252
+ allowIpDomain: false // Don't allow IP addresses as domain
253
+ },
254
+ errorMessage: 'Enter a valid business email address'
255
+ });
256
+ ```
257
+
258
+ #### Alternative: Single Line Input with Email Attributes
259
+
260
+ You can also configure a single line input with email-specific attributes:
261
+
262
+ ```javascript
263
+ import { COMPONENT_TYPES } from '@planning-inspectorate/dynamic-forms/src/index.js';
264
+ import { createQuestions } from '@planning-inspectorate/dynamic-forms/src/questions/create-questions.js';
265
+ import { questionClasses } from '@planning-inspectorate/dynamic-forms/src/questions/questions.js';
266
+ import EmailValidator from '@planning-inspectorate/dynamic-forms/src/validator/email-validator.js';
267
+
268
+ const questionProps = {
269
+ contactEmail: {
270
+ type: COMPONENT_TYPES.SINGLE_LINE_INPUT,
271
+ title: 'Contact Information',
272
+ question: 'What is your email address?',
273
+ fieldName: 'contactEmail',
274
+ url: 'contact-email',
275
+ label: 'Email address',
276
+ inputAttributes: { type: 'email', spellcheck: 'false' },
277
+ autocomplete: 'email',
278
+ validators: [new EmailValidator()]
279
+ }
280
+ };
281
+
282
+ const questions = createQuestions(questionProps, questionClasses, {});
283
+ ```
203
284
 
204
285
  ## Contributing
205
286
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/dynamic-forms",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "This package is for building GDS forms using a configuration-based approach.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,55 @@
1
+ import SingleLineInputQuestion from '../single-line-input/question.js';
2
+
3
+ /**
4
+ * @typedef {import('../../questions/question.js').QuestionViewModel} QuestionViewModel
5
+ * @typedef {import('../../journey/journey.js').Journey} Journey
6
+ * @typedef {import('../../journey/journey-response.js').JourneyResponse} JourneyResponse
7
+ * @typedef {import('../../section').Section} Section
8
+ * @typedef {import('../../validator/base-validator')} BaseValidator
9
+ */
10
+
11
+ /**
12
+ * Email input question that extends SingleLineInputQuestion
13
+ * Automatically sets the input type to "email" and adds appropriate attributes
14
+ * @class
15
+ */
16
+ export default class EmailQuestion extends SingleLineInputQuestion {
17
+ /**
18
+ * @param {import('#question-types').QuestionParameters} params
19
+ * @param {string|undefined} [params.label] if defined this show as a label for the input and the question will just be a standard h1
20
+ * @param {Record<string, string>} [params.inputAttributes] html attributes to add to the input (will be merged with email defaults)
21
+ * @param {string} [params.classes] html classes to add to the input
22
+ * @param {string} [params.autocomplete] autocomplete attribute value (defaults to 'email')
23
+ */
24
+ constructor(params) {
25
+ // Set default input attributes for email
26
+ const emailInputAttributes = {
27
+ type: 'email',
28
+ spellcheck: 'false',
29
+ ...params.inputAttributes
30
+ };
31
+
32
+ // Set default autocomplete for email if not provided
33
+ const autocomplete = params.autocomplete || 'email';
34
+
35
+ super({
36
+ ...params,
37
+ viewFolder: 'single-line-input', // Reuse single-line-input template
38
+ inputAttributes: emailInputAttributes,
39
+ autocomplete: autocomplete
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Override formatAnswerForSummary to prevent capitalization of email addresses
45
+ * Email addresses should remain in their original case (typically lowercase)
46
+ * @param {string} sectionSegment
47
+ * @param {import('../../journey/journey.js').Journey} journey
48
+ * @param {string} answer
49
+ * @returns {Array}
50
+ */
51
+ formatAnswerForSummary(sectionSegment, journey, answer) {
52
+ // Call parent method with capitals=false to prevent email capitalization
53
+ return super.formatAnswerForSummary(sectionSegment, journey, answer, false);
54
+ }
55
+ }
@@ -0,0 +1,146 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import EmailQuestion from './question.js';
4
+
5
+ describe('src/dynamic-forms/components/email/question.js', () => {
6
+ it('should create an email question with default attributes', () => {
7
+ const emailQuestion = new EmailQuestion({
8
+ title: 'Email Address',
9
+ question: 'What is your email address?',
10
+ fieldName: 'email'
11
+ });
12
+
13
+ assert.strictEqual(emailQuestion.viewFolder, 'single-line-input');
14
+ assert.strictEqual(emailQuestion.inputAttributes.type, 'email');
15
+ assert.strictEqual(emailQuestion.inputAttributes.spellcheck, 'false');
16
+ assert.strictEqual(emailQuestion.autocomplete, 'email');
17
+ });
18
+
19
+ it('should allow custom input attributes while preserving email defaults', () => {
20
+ const customAttributes = {
21
+ placeholder: 'Enter your email',
22
+ 'data-testid': 'email-input'
23
+ };
24
+
25
+ const emailQuestion = new EmailQuestion({
26
+ title: 'Email Address',
27
+ question: 'What is your email address?',
28
+ fieldName: 'email',
29
+ inputAttributes: customAttributes
30
+ });
31
+
32
+ // Should preserve email-specific attributes
33
+ assert.strictEqual(emailQuestion.inputAttributes.type, 'email');
34
+ assert.strictEqual(emailQuestion.inputAttributes.spellcheck, 'false');
35
+
36
+ // Should include custom attributes
37
+ assert.strictEqual(emailQuestion.inputAttributes.placeholder, 'Enter your email');
38
+ assert.strictEqual(emailQuestion.inputAttributes['data-testid'], 'email-input');
39
+ });
40
+
41
+ it('should allow overriding default attributes', () => {
42
+ const overrideAttributes = {
43
+ type: 'text', // Override the default email type
44
+ spellcheck: 'true' // Override the default spellcheck
45
+ };
46
+
47
+ const emailQuestion = new EmailQuestion({
48
+ title: 'Email Address',
49
+ question: 'What is your email address?',
50
+ fieldName: 'email',
51
+ inputAttributes: overrideAttributes
52
+ });
53
+
54
+ // Should use overridden values
55
+ assert.strictEqual(emailQuestion.inputAttributes.type, 'text');
56
+ assert.strictEqual(emailQuestion.inputAttributes.spellcheck, 'true');
57
+ });
58
+
59
+ it('should set custom autocomplete attribute', () => {
60
+ const emailQuestion = new EmailQuestion({
61
+ title: 'Work Email',
62
+ question: 'What is your work email address?',
63
+ fieldName: 'workEmail',
64
+ autocomplete: 'work email'
65
+ });
66
+
67
+ assert.strictEqual(emailQuestion.autocomplete, 'work email');
68
+ });
69
+
70
+ it('should include label when provided', () => {
71
+ const emailQuestion = new EmailQuestion({
72
+ title: 'Contact Email',
73
+ question: 'Provide your contact information',
74
+ fieldName: 'contactEmail',
75
+ label: 'Email Address'
76
+ });
77
+
78
+ assert.strictEqual(emailQuestion.label, 'Email Address');
79
+ });
80
+
81
+ it('should include custom classes when provided', () => {
82
+ const emailQuestion = new EmailQuestion({
83
+ title: 'Email Address',
84
+ question: 'What is your email address?',
85
+ fieldName: 'email',
86
+ classes: 'govuk-input--width-20'
87
+ });
88
+
89
+ assert.strictEqual(emailQuestion.classes, 'govuk-input--width-20');
90
+ });
91
+
92
+ it('should prepare question for rendering correctly', () => {
93
+ const emailQuestion = new EmailQuestion({
94
+ title: 'Email Address',
95
+ question: 'What is your email address?',
96
+ fieldName: 'email',
97
+ label: 'Your Email',
98
+ hint: 'We will use this to contact you'
99
+ });
100
+
101
+ // Mock section and journey objects with proper structure
102
+ const mockSection = {
103
+ name: 'contact',
104
+ segment: 'contact-section'
105
+ };
106
+ const mockJourney = {
107
+ response: {
108
+ answers: {
109
+ email: 'existing@example.com'
110
+ }
111
+ },
112
+ getBackLink: () => '/back'
113
+ };
114
+ const mockCustomViewData = {};
115
+ const mockPayload = { email: 'test@example.com' };
116
+
117
+ const viewModel = emailQuestion.prepQuestionForRendering(mockSection, mockJourney, mockCustomViewData, mockPayload);
118
+
119
+ assert.strictEqual(viewModel.question.fieldName, 'email');
120
+ assert.strictEqual(viewModel.question.label, 'Your Email');
121
+ assert.strictEqual(viewModel.question.value, 'test@example.com');
122
+ assert.strictEqual(viewModel.question.type, 'email');
123
+ assert.strictEqual(viewModel.question.attributes.spellcheck, 'false');
124
+ });
125
+
126
+ it('should not capitalize email addresses in summary', () => {
127
+ const emailQuestion = new EmailQuestion({
128
+ title: 'Email Address',
129
+ question: 'What is your email address?',
130
+ fieldName: 'email'
131
+ });
132
+
133
+ const mockSection = { segment: 'contact' };
134
+ const mockJourney = {
135
+ getCurrentQuestionUrl: () => '/questions/email'
136
+ };
137
+ const emailAddress = 'test@example.com';
138
+
139
+ const summary = emailQuestion.formatAnswerForSummary(mockSection.segment, mockJourney, emailAddress);
140
+
141
+ // Should preserve the original case of the email address
142
+ assert.strictEqual(summary.length, 1);
143
+ assert.match(summary[0].value, /test@example\.com/);
144
+ assert.doesNotMatch(summary[0].value, /Test@example\.com/);
145
+ });
146
+ });
@@ -40,7 +40,7 @@
40
40
  },
41
41
  classes: question.classes,
42
42
  attributes: question.attributes,
43
- type: question.attributes.type,
43
+ type: question.type,
44
44
  autocomplete: question.autocomplete
45
45
  }) }}
46
46
  {% else %}
@@ -64,7 +64,7 @@
64
64
  },
65
65
  classes: question.classes,
66
66
  attributes: question.attributes,
67
- type: question.attributes.type,
67
+ type: question.type,
68
68
  autocomplete: question.autocomplete
69
69
  }) }}
70
70
 
@@ -36,7 +36,12 @@ export default class SingleLineInputQuestion extends Question {
36
36
  let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
37
37
  viewModel.question.label = this.label;
38
38
  viewModel.question.value = payload ? payload[viewModel.question.fieldName] : viewModel.question.value;
39
- viewModel.question.attributes = this.inputAttributes;
39
+
40
+ // Extract type from attributes to pass separately to avoid duplication
41
+ const { type, ...otherAttributes } = this.inputAttributes;
42
+ viewModel.question.attributes = otherAttributes;
43
+ viewModel.question.type = type;
44
+
40
45
  viewModel.question.classes = this.classes;
41
46
  return viewModel;
42
47
  }
package/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  /**
6
- * @type {Readonly<{CHECKBOX: QuestionTypes, BOOLEAN: QuestionTypes, RADIO: QuestionTypes, DATE: QuestionTypes, DATE_PERIOD: QuestionTypes, TEXT_ENTRY: QuestionTypes, TEXT_ENTRY_REDACT: QuestionTypes, SELECT: QuestionTypes, SINGLE_LINE_INPUT: QuestionTypes, MULTI_FIELD_INPUT: QuestionTypes, NUMBER: QuestionTypes, ADDRESS: QuestionTypes, UNIT_OPTION: QuestionTypes}>}
6
+ * @type {Readonly<{CHECKBOX: QuestionTypes, BOOLEAN: QuestionTypes, RADIO: QuestionTypes, DATE: QuestionTypes, DATE_PERIOD: QuestionTypes, TEXT_ENTRY: QuestionTypes, TEXT_ENTRY_REDACT: QuestionTypes, SELECT: QuestionTypes, SINGLE_LINE_INPUT: QuestionTypes, MULTI_FIELD_INPUT: QuestionTypes, NUMBER: QuestionTypes, ADDRESS: QuestionTypes, UNIT_OPTION: QuestionTypes, EMAIL: QuestionTypes}>}
7
7
  */
8
8
  export const COMPONENT_TYPES = Object.freeze({
9
9
  CHECKBOX: 'checkbox',
@@ -19,5 +19,6 @@ export const COMPONENT_TYPES = Object.freeze({
19
19
  MULTI_FIELD_INPUT: 'multi-field-input',
20
20
  NUMBER: 'number',
21
21
  ADDRESS: 'site-address',
22
- UNIT_OPTION: 'unit-option'
22
+ UNIT_OPTION: 'unit-option',
23
+ EMAIL: 'email'
23
24
  });
@@ -10,6 +10,7 @@ import SingleLineInputQuestion from '../components/single-line-input/question.js
10
10
  import MultiFieldInputQuestion from '../components/multi-field-input/question.js';
11
11
  import NumberEntryQuestion from '../components/number-entry/question.js';
12
12
  import UnitOptionEntryQuestion from '../components/unit-option-entry/question.js';
13
+ import EmailQuestion from '../components/email/question.js';
13
14
 
14
15
  import { COMPONENT_TYPES } from '../index.js';
15
16
  import TextEntryRedactQuestion from '../components/text-entry-redact/question.js';
@@ -33,5 +34,6 @@ export const questionClasses = Object.freeze({
33
34
  [COMPONENT_TYPES.SINGLE_LINE_INPUT]: SingleLineInputQuestion,
34
35
  [COMPONENT_TYPES.MULTI_FIELD_INPUT]: MultiFieldInputQuestion,
35
36
  [COMPONENT_TYPES.NUMBER]: NumberEntryQuestion,
36
- [COMPONENT_TYPES.UNIT_OPTION]: UnitOptionEntryQuestion
37
+ [COMPONENT_TYPES.UNIT_OPTION]: UnitOptionEntryQuestion,
38
+ [COMPONENT_TYPES.EMAIL]: EmailQuestion
37
39
  });
@@ -0,0 +1,41 @@
1
+ import { body } from 'express-validator';
2
+
3
+ import BaseValidator from './base-validator.js';
4
+
5
+ /**
6
+ * @typedef {Object} EmailValidationOptions
7
+ * @property {boolean} [allowDisplayName] - Allow display names (e.g., "John Doe <john@example.com>")
8
+ * @property {boolean} [requireTld] - Require top-level domain
9
+ * @property {boolean} [allowUtf8LocalPart] - Allow UTF8 characters in local part
10
+ * @property {boolean} [allowIpDomain] - Allow IP addresses as domain
11
+ * @property {string} [errorMessage] - Custom error message
12
+ */
13
+
14
+ export default class EmailValidator extends BaseValidator {
15
+ /**
16
+ * @param {Object} params
17
+ * @param {EmailValidationOptions} [params.options] - Email validation options
18
+ * @param {string} [params.errorMessage] - Custom error message
19
+ * @param {string} [params.fieldName] - Field name to validate
20
+ */
21
+ constructor({ options = {}, errorMessage, fieldName } = {}) {
22
+ super();
23
+
24
+ this.options = {
25
+ allow_display_name: options.allowDisplayName || false,
26
+ require_tld: options.requireTld !== false, // Default to true
27
+ allow_utf8_local_part: options.allowUtf8LocalPart !== undefined ? options.allowUtf8LocalPart : true,
28
+ allow_ip_domain: options.allowIpDomain || false,
29
+ ...options
30
+ };
31
+
32
+ this.errorMessage = errorMessage || 'Enter an email address in the correct format, like name@example.com';
33
+ this.fieldName = fieldName;
34
+ }
35
+
36
+ validate(questionObj) {
37
+ const fieldName = this.fieldName || questionObj.fieldName;
38
+
39
+ return body(fieldName).isEmail(this.options).withMessage(this.errorMessage);
40
+ }
41
+ }
@@ -0,0 +1,166 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import EmailValidator from './email-validator.js';
4
+
5
+ describe('src/dynamic-forms/validator/email-validator.js', () => {
6
+ // Helper function to create validation test
7
+ const createValidationTest = async (email, fieldName = 'email') => {
8
+ const req = {
9
+ body: {
10
+ [fieldName]: email
11
+ }
12
+ };
13
+ const question = {
14
+ fieldName
15
+ };
16
+ return await new EmailValidator().validate(question).run(req);
17
+ };
18
+
19
+ // Valid email addresses
20
+ const validEmails = [
21
+ 'test@example.com',
22
+ 'test+tag@example.com',
23
+ 'first.last@example.com',
24
+ 'user@mail.example.com',
25
+ 'test@example.org',
26
+ 'verylongusernamethatisusuallynotrecommended@verylongdomainnamethatisalsounusual.com'
27
+ ];
28
+
29
+ validEmails.forEach((email) => {
30
+ it(`should validate "${email}" as a correct email address`, async () => {
31
+ const validationResult = await createValidationTest(email);
32
+ assert.strictEqual(validationResult.errors.length, 0);
33
+ });
34
+ });
35
+
36
+ // Invalid email addresses
37
+ const invalidEmails = ['invalid-email', 'test@', 'testexample.com', '', 'test @example.com'];
38
+
39
+ invalidEmails.forEach((email) => {
40
+ it(`should invalidate "${email}" as an incorrect email format`, async () => {
41
+ const validationResult = await createValidationTest(email);
42
+ assert.strictEqual(validationResult.errors.length, 1);
43
+ assert.strictEqual(
44
+ validationResult.errors[0].msg,
45
+ 'Enter an email address in the correct format, like name@example.com'
46
+ );
47
+ });
48
+ });
49
+
50
+ it('should handle undefined email field as invalid', async () => {
51
+ const req = {
52
+ body: {}
53
+ };
54
+ const question = {
55
+ fieldName: 'email'
56
+ };
57
+ const validationResult = await new EmailValidator().validate(question).run(req);
58
+ assert.strictEqual(validationResult.errors.length, 1);
59
+ });
60
+
61
+ it('should use custom error message when provided', async () => {
62
+ const req = {
63
+ body: {
64
+ email: 'invalid-email'
65
+ }
66
+ };
67
+ const question = {
68
+ fieldName: 'email'
69
+ };
70
+ const customMessage = 'Please provide a valid email address';
71
+ const validationResult = await new EmailValidator({ errorMessage: customMessage }).validate(question).run(req);
72
+ assert.strictEqual(validationResult.errors.length, 1);
73
+ assert.strictEqual(validationResult.errors[0].msg, customMessage);
74
+ });
75
+
76
+ it('should handle display names when option is enabled', async () => {
77
+ const req = {
78
+ body: {
79
+ email: 'John Doe <john@example.com>'
80
+ }
81
+ };
82
+ const question = {
83
+ fieldName: 'email'
84
+ };
85
+ const validationResult = await new EmailValidator({
86
+ options: { allowDisplayName: true }
87
+ })
88
+ .validate(question)
89
+ .run(req);
90
+ assert.strictEqual(validationResult.errors.length, 0);
91
+ });
92
+
93
+ it('should reject display names when option is disabled', async () => {
94
+ const req = {
95
+ body: {
96
+ email: 'John Doe <john@example.com>'
97
+ }
98
+ };
99
+ const question = {
100
+ fieldName: 'email'
101
+ };
102
+ const validationResult = await new EmailValidator({
103
+ options: { allowDisplayName: false }
104
+ })
105
+ .validate(question)
106
+ .run(req);
107
+ assert.strictEqual(validationResult.errors.length, 1);
108
+ });
109
+
110
+ it('should validate email with custom fieldName', async () => {
111
+ const req = {
112
+ body: {
113
+ contactEmail: 'test@example.com'
114
+ }
115
+ };
116
+ const question = {
117
+ fieldName: 'someOtherField'
118
+ };
119
+ const validationResult = await new EmailValidator({
120
+ fieldName: 'contactEmail'
121
+ })
122
+ .validate(question)
123
+ .run(req);
124
+ assert.strictEqual(validationResult.errors.length, 0);
125
+ });
126
+
127
+ it('should reject UTF8 characters when allowUtf8LocalPart is false', async () => {
128
+ const req = {
129
+ body: {
130
+ email: 'tëst@example.com'
131
+ }
132
+ };
133
+ const question = {
134
+ fieldName: 'email'
135
+ };
136
+ const validationResult = await new EmailValidator({
137
+ options: { allowUtf8LocalPart: false }
138
+ })
139
+ .validate(question)
140
+ .run(req);
141
+
142
+ assert.strictEqual(validationResult.errors.length, 1);
143
+ assert.strictEqual(
144
+ validationResult.errors[0].msg,
145
+ 'Enter an email address in the correct format, like name@example.com'
146
+ );
147
+ });
148
+
149
+ it('should accept UTF8 characters when allowUtf8LocalPart is true (default)', async () => {
150
+ const req = {
151
+ body: {
152
+ email: 'tëst@example.com'
153
+ }
154
+ };
155
+ const question = {
156
+ fieldName: 'email'
157
+ };
158
+ const validationResult = await new EmailValidator({
159
+ options: { allowUtf8LocalPart: true }
160
+ })
161
+ .validate(question)
162
+ .run(req);
163
+
164
+ assert.strictEqual(validationResult.errors.length, 0);
165
+ });
166
+ });
package/test/journey.js CHANGED
@@ -28,6 +28,7 @@ export function createJourney(questions, response) {
28
28
  .addQuestion(questions.nights)
29
29
  .addQuestion(questions.hotelAddress)
30
30
  .addQuestion(questions.luggageWeight)
31
+ .addQuestion(questions.contactEmail)
31
32
  ],
32
33
  taskListUrl: 'check-your-answers',
33
34
  journeyTemplate: 'views/layout-journey.njk',
package/test/questions.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { COMPONENT_TYPES } from '../src/index.js';
2
2
  import { createQuestions } from '../src/questions/create-questions.js';
3
3
  import { questionClasses } from '../src/questions/questions.js';
4
+ import EmailValidator from '../src/validator/email-validator.js';
4
5
 
5
6
  /**
6
7
  * @typedef {import('../src/questions/question-props.js').QuestionProps} Props
@@ -164,6 +165,15 @@ export const questionProps = {
164
165
  conditional: { label: 'Weight in pounds', fieldName: 'luggageWeightValue_lbs', suffix: 'lbs' }
165
166
  }
166
167
  ]
168
+ },
169
+ contactEmail: {
170
+ type: COMPONENT_TYPES.EMAIL,
171
+ title: 'Contact Email',
172
+ question: 'What is your email address for booking confirmations?',
173
+ fieldName: 'contactEmail',
174
+ url: 'contact-email',
175
+ label: 'Email address',
176
+ validators: [new EmailValidator()]
167
177
  }
168
178
  };
169
179
 
@@ -183,7 +193,8 @@ export const questionsInOrder = [
183
193
  questionProps.companions,
184
194
  questionProps.nights,
185
195
  questionProps.hotelAddress,
186
- questionProps.luggageWeight
196
+ questionProps.luggageWeight,
197
+ questionProps.contactEmail
187
198
  ];
188
199
 
189
200
  export const getQuestions = () => createQuestions(questionProps, questionClasses, {});
@@ -193,6 +193,8 @@ export function mockAnswerBody(q) {
193
193
  [q.fieldName]: 'kg',
194
194
  [q.options[0].conditional.fieldName]: 10
195
195
  };
196
+ case COMPONENT_TYPES.EMAIL:
197
+ return { [q.fieldName]: 'test@example.com' };
196
198
  default:
197
199
  return { [q.fieldName]: 'test' };
198
200
  }
@@ -240,6 +242,8 @@ export function mockAnswer(q) {
240
242
  }
241
243
  case COMPONENT_TYPES.UNIT_OPTION:
242
244
  return '10 kg';
245
+ case COMPONENT_TYPES.EMAIL:
246
+ return 'test@example.com';
243
247
  default:
244
248
  return 'test';
245
249
  }
@@ -185,6 +185,17 @@
185
185
  <a class="govuk-link" href="questions/luggage-weight-units">Answer<span class="govuk-visually-hidden"> Select your luggage weight and unit.</span></a>
186
186
  </dd>
187
187
  </div>
188
+ <div class="govuk-summary-list__row">
189
+ <dt class="govuk-summary-list__key">
190
+ Contact Email
191
+ </dt>
192
+ <dd class="govuk-summary-list__value">
193
+ Not started
194
+ </dd>
195
+ <dd class="govuk-summary-list__actions">
196
+ <a class="govuk-link" href="questions/contact-email">Answer<span class="govuk-visually-hidden"> What is your email address for booking confirmations?</span></a>
197
+ </dd>
198
+ </div>
188
199
  </dl>
189
200
 
190
201
  <form action method="post">
@@ -185,6 +185,17 @@
185
185
  <a class="govuk-link" href="questions/luggage-weight-units">Change<span class="govuk-visually-hidden"> Select your luggage weight and unit.</span></a>
186
186
  </dd>
187
187
  </div>
188
+ <div class="govuk-summary-list__row">
189
+ <dt class="govuk-summary-list__key">
190
+ Contact Email
191
+ </dt>
192
+ <dd class="govuk-summary-list__value">
193
+ test@example.com
194
+ </dd>
195
+ <dd class="govuk-summary-list__actions">
196
+ <a class="govuk-link" href="questions/contact-email">Change<span class="govuk-visually-hidden"> What is your email address for booking confirmations?</span></a>
197
+ </dd>
198
+ </div>
188
199
  </dl>
189
200
 
190
201
  <form action method="post">
@@ -0,0 +1,23 @@
1
+ <div class="govuk-grid-row">
2
+ <div class="govuk-grid-column-two-thirds">
3
+ <form action="" method="post" novalidate>
4
+ <h1 class="govuk-heading-l">
5
+ What is your email address for booking confirmations?
6
+ </h1>
7
+
8
+
9
+
10
+ <div class="govuk-form-group">
11
+ <label class="govuk-label govuk-label" for="contactEmail">
12
+ Email address
13
+ </label>
14
+ <input class="govuk-input" id="contactEmail" name="contactEmail" type="email" value="" autocomplete="email" spellcheck="false">
15
+ </div>
16
+
17
+
18
+ <button type="submit" class="govuk-button" data-module="govuk-button" data-cy="button-save-and-continue">
19
+ Continue
20
+ </button>
21
+ </form>
22
+ </div>
23
+ </div>