@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 CHANGED
@@ -1,142 +1,86 @@
1
1
  # Dynamics Forms
2
2
 
3
- This package is for building [GDS](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.
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
- - Component - A "blueprint" of a type of question. i.e. input, radio button, checkbox etc.
13
- - Question - A specific question within a journey which is made up of one (usually) or many (sometimes) components and
14
- their required content.
15
- - Section - A group of Questions
16
- - Journey - An entire set of questions required for a completion of a submission
17
- - Answer - Data input by a user against a specific Question.
18
- - Response - a collection of answers submitted as part of a Journey.
19
- - Validation - Verification that an individual Answer meets the criteria of that Question. i.e. String is greater than 3
20
- characters.
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
- ## Requirements
34
+ ## Usage
23
35
 
24
- To use this package nunjucks must be configured so that the component view files are available, and static assets must be made available for certain components and styling. Some nunjucks templates are required to correctly render the question pages and task list views.
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
- ### Nunjucks
38
+ All exports are named exports from the root module:
27
39
 
28
- Nunjucks template paths needs configuring with the govuk-frontend and the dynamic forms folder, e.g.
40
+ `import {COMPONENT_TYPES} from '@planning-inspectorate/dynamic-forms`
29
41
 
30
- ```javascript
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
- ### Assets
62
-
63
- The select component can use the [accessible-autocomplete](https://www.npmjs.com/package/accessible-autocomplete) package. This requires configuring, the component assumes the following paths are available:
64
-
65
- * `/assets/css/accessible-autocomplete.min.css`
66
- * `/assets/js/accessible-autocomplete.min.js`
67
-
68
- ### Layouts
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
- **Task list template**
113
-
114
- The task list template is used for the task list or "check your answers" page. It must include a block called `dynTaskList`. The path to this template is configured via the `taskListTemplate` property on a Journey.
115
-
116
- e.g.
117
-
118
- ```nunjucks
119
- {% extends "govuk/template.njk" %}
120
-
121
- {% block content %}
122
- <div class="govuk-grid-row">
123
- <div class="govuk-grid-column-full">
124
- <h1 class="govuk-heading-xl">
125
- {% if pageCaption %}
126
- <span class="govuk-caption-xl">{{ pageCaption }}</span>
127
- {% endif %}
128
- {{ pageHeading }}
129
- </h1>
130
- </div>
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
- ## Usage
77
+ The controllers available are:
138
78
 
139
- 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.
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
- ### Email Validation
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
- - `type="email"`
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: ({ selectedOptions }) => {
350
- const selectedOption = selectedOptions[0];
351
- const color = selectedOption?.value === 'approved' ? 'green' : 'red';
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
- **Using selectedOptions for single-select questions (Radio/Select):**
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
- ### Components
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
+ ```