@planning-inspectorate/dynamic-forms 3.17.0 → 3.18.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 +70 -246
- package/docs/Email Validation.md +86 -0
- package/docs/Getting Started.md +299 -0
- package/package.json +1 -1
- package/src/components/checkbox/question.d.ts.map +1 -1
- package/src/components/checkbox/question.js +3 -2
- package/src/components/checkbox/question.test.js +8 -0
- package/src/components/email/question.d.ts +0 -9
- package/src/components/email/question.d.ts.map +1 -1
- package/src/components/email/question.js +2 -13
- package/src/components/email/question.test.js +19 -0
- package/src/components/multi-field-input/question.d.ts +4 -19
- package/src/components/multi-field-input/question.d.ts.map +1 -1
- package/src/components/multi-field-input/question.js +89 -23
- package/src/components/multi-field-input/question.test.js +441 -3
- package/src/components/number-entry/question.d.ts.map +1 -1
- package/src/components/number-entry/question.js +3 -2
- package/src/components/number-entry/question.test.js +17 -0
- package/src/components/unit-option-entry/question.d.ts.map +1 -1
- package/src/components/unit-option-entry/question.js +3 -1
- package/src/components/unit-option-entry/question.test.js +15 -0
- package/src/questions/question.d.ts +2 -2
- package/src/questions/question.js +2 -2
- package/test/snapshots/check-your-answers-with-answers.html +1 -1
- package/test/utils/question-test-utils.js +2 -1
- package/typedefs/question-props.d.ts +33 -0
- package/typedefs/question-types.d.ts +0 -23
package/README.md
CHANGED
|
@@ -1,142 +1,86 @@
|
|
|
1
1
|
# Dynamics Forms
|
|
2
2
|
|
|
3
|
-
This package is for building [
|
|
3
|
+
This package is for building [GOV.UK Frontend](https://design-system.service.gov.uk/) forms using a configuration-based approach. It allows you to define a set of questions, and combine them into journeys. Questions can be configured to be included conditionally, based on the answers to other questions.
|
|
4
4
|
|
|
5
|
-
The functionality for 'check-your-answers' pages can also be useful for generating pages outside the context of a journey or form, such as for managing data. Each row shown on the page can include a link to edit that data.
|
|
5
|
+
The functionality for ['check-your-answers'](https://design-system.service.gov.uk/patterns/check-answers/) pages can also be useful for generating pages outside the context of a journey or form, such as for managing data. Each row shown on the page can include a link to edit that data.
|
|
6
6
|
|
|
7
7
|
> Note this is ported from Appeals: [dynamic forms](https://github.com/Planning-Inspectorate/appeal-planning-decision/tree/a46f945047dc1f13d523a0853b4fcbb4bd0f6d6e/packages/forms-web-app/src/dynamic-forms), but transformed for ES6 modules and Node Test Runner. Not all functionality has been brought across, such as 'add more'.
|
|
8
8
|
> It is hoped that this version can be developed over time so Appeals can move to this version.
|
|
9
9
|
|
|
10
|
+
## Getting started
|
|
11
|
+
|
|
12
|
+
In summary:
|
|
13
|
+
|
|
14
|
+
1. Install the package: `npm install --save @planning-inspectorate/dynamic-forms`
|
|
15
|
+
2. Configure nunjucks
|
|
16
|
+
3. (optional) Configure accessible-autocomplete
|
|
17
|
+
4. Add the question and check-your-answers templates
|
|
18
|
+
5. Configure questions, journeys, and routes
|
|
19
|
+
|
|
20
|
+
For more details, see the [Getting started](./docs/Getting%20Started.md) guide.
|
|
21
|
+
|
|
10
22
|
## Terminology
|
|
11
23
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
| Term | Description |
|
|
25
|
+
|------------|----------------------------------------------------------------------------------------------------------------------|
|
|
26
|
+
| Component | A reusable part of a user interface. Usually forming the main content of a page in the context of a user journey. |
|
|
27
|
+
| Question | An instantiated component with associated configuration, a specific question within a journey |
|
|
28
|
+
| Section | A group of questions, configured with any conditional logic |
|
|
29
|
+
| Journey | An entire set of questions and sections which make up a user journey |
|
|
30
|
+
| Answer | Data input by a user against a specific Question |
|
|
31
|
+
| Response | A collection of answers submitted as part of a Journey |
|
|
32
|
+
| Validation | Verification that an individual Answer meets the criteria of that Question. i.e. String is greater than 3 characters |
|
|
21
33
|
|
|
22
|
-
##
|
|
34
|
+
## Usage
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
The test directory includes a very basic journey in `test/journey.js`. This may be a useful guide for setting up a journey and associated questions. Also in `test/questions.test.js` there is a function `createAppWithQuestions` which may be a useful guide for setting up the appropriate controllers and routes for a journey.
|
|
25
37
|
|
|
26
|
-
|
|
38
|
+
All exports are named exports from the root module:
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
`import {COMPONENT_TYPES} from '@planning-inspectorate/dynamic-forms`
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
import { createRequire } from 'node:module';
|
|
32
|
-
import path from 'node:path';
|
|
33
|
-
import nunjucks from 'nunjucks';
|
|
34
|
-
|
|
35
|
-
export function configureNunjucks() {
|
|
36
|
-
// get the require function, see https://nodejs.org/api/module.html#modulecreaterequirefilename
|
|
37
|
-
const require = createRequire(import.meta.url);
|
|
38
|
-
// path to dynamic forms folder
|
|
39
|
-
const dynamicFormsRoot = path.resolve(require.resolve('@planning-inspectorate/dynamic-forms'), '..');
|
|
40
|
-
// get the path to the govuk-frontend folder, in node_modules, using the node require resolution
|
|
41
|
-
const govukFrontendRoot = path.resolve(require.resolve('govuk-frontend'), '../..');
|
|
42
|
-
const appDir = path.join('some/path/to/the', 'app');
|
|
43
|
-
|
|
44
|
-
// configure nunjucks
|
|
45
|
-
return nunjucks.configure(
|
|
46
|
-
// ensure nunjucks templates can use govuk-frontend components, and templates we've defined in `web/src/app`
|
|
47
|
-
[dynamicFormsRoot, govukFrontendRoot, appDir],
|
|
48
|
-
{
|
|
49
|
-
// output with dangerous characters are escaped automatically
|
|
50
|
-
autoescape: true,
|
|
51
|
-
// automatically remove trailing newlines from a block/tag
|
|
52
|
-
trimBlocks: true,
|
|
53
|
-
// automatically remove leading whitespace from a block/tag
|
|
54
|
-
lstripBlocks: true
|
|
55
|
-
}
|
|
56
|
-
);
|
|
57
|
-
}
|
|
42
|
+
### Components
|
|
58
43
|
|
|
59
|
-
|
|
44
|
+
Components available are exported via the `COMPONENT_TYPES` constant.
|
|
60
45
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
There are two layout files that need to be configured for dynamic-forms: journey and task list.
|
|
71
|
-
|
|
72
|
-
**Journey template**
|
|
73
|
-
|
|
74
|
-
The journey template is used for question pages. It must include a block called `dynQuestionContent`, and a block called `head` (which the default gov.uk template has).
|
|
75
|
-
It should also include the rendering of the errorSummary component. The path to this template is configured via the `journeyTemplate` property on a Journey.
|
|
76
|
-
|
|
77
|
-
e.g.
|
|
78
|
-
|
|
79
|
-
```nunjucks
|
|
80
|
-
{% extends "govuk/template.njk" %}
|
|
81
|
-
|
|
82
|
-
{% block beforeContent %}
|
|
83
|
-
{% if errorSummary %}
|
|
84
|
-
<div class="govuk-grid-row">
|
|
85
|
-
<div class="govuk-grid-column-two-thirds">
|
|
86
|
-
{{ govukErrorSummary({
|
|
87
|
-
titleText: "There is a problem",
|
|
88
|
-
errorList: errorSummary,
|
|
89
|
-
attributes: {"data-cy": "error-wrapper"}
|
|
90
|
-
}) }}
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
{% endif %}
|
|
94
|
-
{% endblock %}
|
|
95
|
-
|
|
96
|
-
{% block content %}
|
|
97
|
-
<div class="govuk-grid-row">
|
|
98
|
-
<div class="govuk-grid-column-full">
|
|
99
|
-
<h1 class="govuk-heading-xl">
|
|
100
|
-
{% if pageCaption %}
|
|
101
|
-
<span class="govuk-caption-xl">{{ pageCaption }}</span>
|
|
102
|
-
{% endif %}
|
|
103
|
-
{{ pageHeading }}
|
|
104
|
-
</h1>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
{% block dynQuestionContent %}{% endblock %}
|
|
109
|
-
{% endblock %}
|
|
110
|
-
```
|
|
46
|
+
#### Custom components
|
|
47
|
+
|
|
48
|
+
Components can be created and used with Dynamic Forms with some configuration. Extend the base `Question` class.
|
|
49
|
+
|
|
50
|
+
1. Ensure nunjucks is configured with the folder containing your custom components
|
|
51
|
+
2. Export the custom component type names and classes
|
|
52
|
+
3. Merge the exported question classes object with the custom classes
|
|
53
|
+
|
|
54
|
+
See also [Contributing > new components](#new-components)
|
|
111
55
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
{% block dynTaskList %}{% endblock %}
|
|
134
|
-
{% endblock %}
|
|
56
|
+
### Controllers
|
|
57
|
+
|
|
58
|
+
Dynamic forms includes several Express handlers/controllers which implement the core logic. They all have prerequisites and expect particular properties have been added to the `res.locals` object. These are:
|
|
59
|
+
|
|
60
|
+
* `res.locals.journey` must be an instance of the `Journey` class
|
|
61
|
+
* `res.locals.journeyResponse` must be an instance of the `JourneyResponse` class
|
|
62
|
+
|
|
63
|
+
They can be added using middleware, and utility functions are available for this, for example:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// get the answers from the session, and add JourneyResponse to `res.locals`
|
|
67
|
+
const getJourneyResponse = buildGetJourneyResponseFromSession(JOURNEY_ID);
|
|
68
|
+
// create the journey with the given journeyResponse and questions, and add to `res.locals`
|
|
69
|
+
const getJourney = buildGetJourney((req, journeyResponse) => createJourney(req, journeyResponse, questions));
|
|
70
|
+
|
|
71
|
+
router.use(
|
|
72
|
+
getJourneyResponse,
|
|
73
|
+
getJourney
|
|
74
|
+
);
|
|
135
75
|
```
|
|
136
76
|
|
|
137
|
-
|
|
77
|
+
The controllers available are:
|
|
138
78
|
|
|
139
|
-
|
|
79
|
+
| Controller | Purpose |
|
|
80
|
+
|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
81
|
+
| question | To render a question page as part of a journey |
|
|
82
|
+
| buildSave | Returns a controller to handle saving the answer and either redirecting to the next question or show any validation errors if present. Accepts a save function. |
|
|
83
|
+
| list/buildList | Renders a check-your-answers page showing all the answers, links to change any answer, and a button to submit the answers. |
|
|
140
84
|
|
|
141
85
|
### Conditions
|
|
142
86
|
|
|
@@ -199,92 +143,9 @@ These conditions combine with individual conditions and section conditions. In t
|
|
|
199
143
|
| q6 | q1 = 'yes' | q5 = 90 | N/A |
|
|
200
144
|
| q7 | q1 = 'yes' | q5 = 90 | N/A |
|
|
201
145
|
|
|
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 {
|
|
210
|
-
COMPONENT_TYPES,
|
|
211
|
-
createQuestions,
|
|
212
|
-
questionClasses,
|
|
213
|
-
EmailValidator
|
|
214
|
-
} from '@planning-inspectorate/dynamic-forms';
|
|
215
|
-
|
|
216
|
-
// Define question configuration
|
|
217
|
-
const questionProps = {
|
|
218
|
-
contactEmail: {
|
|
219
|
-
type: COMPONENT_TYPES.EMAIL,
|
|
220
|
-
title: 'Contact Information',
|
|
221
|
-
question: 'What is your email address?',
|
|
222
|
-
fieldName: 'contactEmail',
|
|
223
|
-
url: 'contact-email',
|
|
224
|
-
label: 'Email address',
|
|
225
|
-
validators: [
|
|
226
|
-
new EmailValidator({
|
|
227
|
-
errorMessage: 'Enter an email address in the correct format, like name@example.com'
|
|
228
|
-
})
|
|
229
|
-
]
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// Create questions using the factory function
|
|
234
|
-
const questions = createQuestions(questionProps, questionClasses, {});
|
|
235
|
-
const emailQuestion = questions.contactEmail;
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
The EmailQuestion uses the following default input attributes:
|
|
146
|
+
### Validators
|
|
239
147
|
|
|
240
|
-
|
|
241
|
-
- `spellcheck="false"`
|
|
242
|
-
- `autocomplete="email"`
|
|
243
|
-
|
|
244
|
-
#### Advanced Validation Options
|
|
245
|
-
|
|
246
|
-
For stricter email validation requirements:
|
|
247
|
-
|
|
248
|
-
```javascript
|
|
249
|
-
const businessEmailValidator = new EmailValidator({
|
|
250
|
-
options: {
|
|
251
|
-
allowDisplayName: false, // Reject "Name <email@domain.com>" format
|
|
252
|
-
requireTld: true, // Require top-level domain (default: true)
|
|
253
|
-
allowUtf8LocalPart: false, // Only ASCII characters in local part
|
|
254
|
-
allowIpDomain: false // Don't allow IP addresses as domain
|
|
255
|
-
},
|
|
256
|
-
errorMessage: 'Enter a valid business email address'
|
|
257
|
-
});
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
#### Alternative: Single Line Input with Email Attributes
|
|
261
|
-
|
|
262
|
-
You can also configure a single line input with email-specific attributes:
|
|
263
|
-
|
|
264
|
-
```javascript
|
|
265
|
-
import {
|
|
266
|
-
COMPONENT_TYPES,
|
|
267
|
-
createQuestions,
|
|
268
|
-
questionClasses,
|
|
269
|
-
EmailValidator
|
|
270
|
-
} from '@planning-inspectorate/dynamic-forms';
|
|
271
|
-
|
|
272
|
-
const questionProps = {
|
|
273
|
-
contactEmail: {
|
|
274
|
-
type: COMPONENT_TYPES.SINGLE_LINE_INPUT,
|
|
275
|
-
title: 'Contact Information',
|
|
276
|
-
question: 'What is your email address?',
|
|
277
|
-
fieldName: 'contactEmail',
|
|
278
|
-
url: 'contact-email',
|
|
279
|
-
label: 'Email address',
|
|
280
|
-
inputAttributes: { type: 'email', spellcheck: 'false' },
|
|
281
|
-
autocomplete: 'email',
|
|
282
|
-
validators: [new EmailValidator()]
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const questions = createQuestions(questionProps, questionClasses, {});
|
|
287
|
-
```
|
|
148
|
+
Many validators are available such as `RequiredValidator` and `AddressValidator`. More detail on email validation is available in [docs/Email Validation](./docs/Email%20Validation.md).
|
|
288
149
|
|
|
289
150
|
### Custom Summary Formatting
|
|
290
151
|
|
|
@@ -320,20 +181,6 @@ The formatter function receives a context object with the following properties:
|
|
|
320
181
|
| `question` | `Question` | The question instance |
|
|
321
182
|
| `journey` | `Journey` | The journey instance |
|
|
322
183
|
| `sectionSegment` | `string` | The current section segment |
|
|
323
|
-
| `selectedOptions` | `Option[] \| undefined` | For OptionsQuestions (such as Checkbox or Radio), the matched option object(s) as an array |
|
|
324
|
-
|
|
325
|
-
#### Answer Formats
|
|
326
|
-
|
|
327
|
-
Different options-based questions store answers in different formats. The `selectedOptions` array normalises these formats for your formatter:
|
|
328
|
-
|
|
329
|
-
| Question Type | Answer Format | `selectedOptions` Result |
|
|
330
|
-
|---------------|---------------|--------------------------|
|
|
331
|
-
| **Radio / Select** | Single string value: `"approved"` | `[{ text: 'Approved', value: 'approved' }]` |
|
|
332
|
-
| **Radio / Select** | Conditional object: `{ value: "yes", conditional: "details" }` | `[{ text: 'Yes', value: 'yes', conditional: {...} }]` |
|
|
333
|
-
| **Checkbox** | Comma-separated string: `"option1,option2"` | `[{ text: 'Option 1', value: 'option1' }, { text: 'Option 2', value: 'option2' }]` |
|
|
334
|
-
| **Checkbox** | Single conditional object: `{ value: "other", conditional: "details" }` | `[{ text: 'Other', value: 'other', conditional: {...} }]` |
|
|
335
|
-
|
|
336
|
-
For single-select questions (Radio, Select), use `selectedOptions[0]` to access the selected option. For multi-select questions (Checkbox), iterate over the array.
|
|
337
184
|
|
|
338
185
|
#### Examples
|
|
339
186
|
|
|
@@ -346,10 +193,9 @@ formatSummaryValue: ({ formattedAnswer }) => `<strong>${escapeHtml(formattedAnsw
|
|
|
346
193
|
**Conditional styling based on answer:**
|
|
347
194
|
|
|
348
195
|
```javascript
|
|
349
|
-
formatSummaryValue: ({
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
return `<span style="color: ${color}">${escapeHtml(selectedOption?.text ?? '-')}</span>`;
|
|
196
|
+
formatSummaryValue: ({ answer, formattedAnswer }) => {
|
|
197
|
+
const color = answer === 'approved' ? 'green' : 'red';
|
|
198
|
+
return `<span style="color: ${color}">${escapeHtml(formattedAnswer)}</span>`;
|
|
353
199
|
}
|
|
354
200
|
```
|
|
355
201
|
|
|
@@ -373,29 +219,7 @@ formatSummaryValue: ({ formattedAnswer, journey }) => {
|
|
|
373
219
|
}
|
|
374
220
|
```
|
|
375
221
|
|
|
376
|
-
**
|
|
377
|
-
|
|
378
|
-
```javascript
|
|
379
|
-
formatSummaryValue: ({ selectedOptions }) => {
|
|
380
|
-
const selectedOption = selectedOptions[0];
|
|
381
|
-
// Access the full option object including any custom properties
|
|
382
|
-
const warning = selectedOption?.warning;
|
|
383
|
-
return warning ? `${escapeHtml(selectedOption?.text ?? '-')} ⚠️` : escapeHtml(selectedOption?.text ?? '-');
|
|
384
|
-
}
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
**Using selectedOptions for multi-select questions (Checkbox):**
|
|
388
|
-
|
|
389
|
-
```javascript
|
|
390
|
-
formatSummaryValue: ({ selectedOptions }) => {
|
|
391
|
-
// Join all selected option texts with custom formatting
|
|
392
|
-
return selectedOptions
|
|
393
|
-
.map(opt => `<span class="tag">${escapeHtml(opt.text)}</span>`)
|
|
394
|
-
.join(' ');
|
|
395
|
-
}
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
> **Note:** `formattedAnswer` is already escaped by dynamic-forms; you are responsible for escaping any additional user-controlled values you include (for example `answer`, option text from `selectedOptions`, or values read from `journey.response.answers`).
|
|
222
|
+
> **Note:** `formattedAnswer` is already escaped by dynamic-forms; you are responsible for escaping any additional user-controlled values you include (for example `answer` or values read from `journey.response.answers`).
|
|
399
223
|
|
|
400
224
|
## Contributing
|
|
401
225
|
|
|
@@ -428,7 +252,7 @@ When adding a new question type, be sure to add an example question into `test/q
|
|
|
428
252
|
|
|
429
253
|
To update any snapshots with rendering changes (or new questions), run `node --test --test-update-snapshots`.
|
|
430
254
|
|
|
431
|
-
###
|
|
255
|
+
### New components
|
|
432
256
|
|
|
433
257
|
When implement a new component, extend the base `Question` class. Common methods to override are:
|
|
434
258
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Email Validation
|
|
2
|
+
|
|
3
|
+
Email validation is provided through the `EmailValidator` class and `EmailQuestion` component. The validator uses express-validator's robust email validation with configurable options.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import {
|
|
9
|
+
COMPONENT_TYPES,
|
|
10
|
+
createQuestions,
|
|
11
|
+
questionClasses,
|
|
12
|
+
EmailValidator
|
|
13
|
+
} from '@planning-inspectorate/dynamic-forms';
|
|
14
|
+
|
|
15
|
+
// Define question configuration
|
|
16
|
+
const questionProps = {
|
|
17
|
+
contactEmail: {
|
|
18
|
+
type: COMPONENT_TYPES.EMAIL,
|
|
19
|
+
title: 'Contact Information',
|
|
20
|
+
question: 'What is your email address?',
|
|
21
|
+
fieldName: 'contactEmail',
|
|
22
|
+
url: 'contact-email',
|
|
23
|
+
label: 'Email address',
|
|
24
|
+
validators: [
|
|
25
|
+
new EmailValidator({
|
|
26
|
+
errorMessage: 'Enter an email address in the correct format, like name@example.com'
|
|
27
|
+
})
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Create questions using the factory function
|
|
33
|
+
const questions = createQuestions(questionProps, questionClasses, {});
|
|
34
|
+
const emailQuestion = questions.contactEmail;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The EmailQuestion uses the following default input attributes:
|
|
38
|
+
|
|
39
|
+
- `type="email"`
|
|
40
|
+
- `spellcheck="false"`
|
|
41
|
+
- `autocomplete="email"`
|
|
42
|
+
|
|
43
|
+
## Advanced Validation Options
|
|
44
|
+
|
|
45
|
+
For stricter email validation requirements:
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
const businessEmailValidator = new EmailValidator({
|
|
49
|
+
options: {
|
|
50
|
+
allowDisplayName: false, // Reject "Name <email@domain.com>" format
|
|
51
|
+
requireTld: true, // Require top-level domain (default: true)
|
|
52
|
+
allowUtf8LocalPart: false, // Only ASCII characters in local part
|
|
53
|
+
allowIpDomain: false // Don't allow IP addresses as domain
|
|
54
|
+
},
|
|
55
|
+
errorMessage: 'Enter a valid business email address'
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Alternative: Single Line Input with Email Attributes
|
|
60
|
+
|
|
61
|
+
You can also configure a single line input with email-specific attributes:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
import {
|
|
65
|
+
COMPONENT_TYPES,
|
|
66
|
+
createQuestions,
|
|
67
|
+
questionClasses,
|
|
68
|
+
EmailValidator
|
|
69
|
+
} from '@planning-inspectorate/dynamic-forms';
|
|
70
|
+
|
|
71
|
+
const questionProps = {
|
|
72
|
+
contactEmail: {
|
|
73
|
+
type: COMPONENT_TYPES.SINGLE_LINE_INPUT,
|
|
74
|
+
title: 'Contact Information',
|
|
75
|
+
question: 'What is your email address?',
|
|
76
|
+
fieldName: 'contactEmail',
|
|
77
|
+
url: 'contact-email',
|
|
78
|
+
label: 'Email address',
|
|
79
|
+
inputAttributes: { type: 'email', spellcheck: 'false' },
|
|
80
|
+
autocomplete: 'email',
|
|
81
|
+
validators: [new EmailValidator()]
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const questions = createQuestions(questionProps, questionClasses, {});
|
|
86
|
+
```
|