@planning-inspectorate/dynamic-forms 3.16.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.
Files changed (77) hide show
  1. package/README.md +124 -189
  2. package/docs/Email Validation.md +86 -0
  3. package/docs/Getting Started.md +299 -0
  4. package/package.json +1 -1
  5. package/src/components/address/question.d.ts +1 -9
  6. package/src/components/address/question.d.ts.map +1 -1
  7. package/src/components/address/question.js +6 -17
  8. package/src/components/checkbox/question.d.ts +1 -21
  9. package/src/components/checkbox/question.d.ts.map +1 -1
  10. package/src/components/checkbox/question.js +38 -50
  11. package/src/components/checkbox/question.test.js +177 -26
  12. package/src/components/date/question.d.ts +0 -8
  13. package/src/components/date/question.d.ts.map +1 -1
  14. package/src/components/date/question.js +6 -14
  15. package/src/components/date-period/question.d.ts +1 -5
  16. package/src/components/date-period/question.d.ts.map +1 -1
  17. package/src/components/date-period/question.js +1 -1
  18. package/src/components/date-time/question.d.ts +0 -8
  19. package/src/components/date-time/question.d.ts.map +1 -1
  20. package/src/components/date-time/question.js +4 -12
  21. package/src/components/email/question.d.ts +0 -9
  22. package/src/components/email/question.d.ts.map +1 -1
  23. package/src/components/email/question.js +2 -13
  24. package/src/components/email/question.test.js +19 -0
  25. package/src/components/manage-list/answer-summary-list.njk +1 -1
  26. package/src/components/manage-list/question.d.ts +1 -5
  27. package/src/components/manage-list/question.d.ts.map +1 -1
  28. package/src/components/manage-list/question.js +1 -1
  29. package/src/components/multi-field-input/question.d.ts +4 -19
  30. package/src/components/multi-field-input/question.d.ts.map +1 -1
  31. package/src/components/multi-field-input/question.js +89 -23
  32. package/src/components/multi-field-input/question.test.js +441 -3
  33. package/src/components/number-entry/question.d.ts +0 -28
  34. package/src/components/number-entry/question.d.ts.map +1 -1
  35. package/src/components/number-entry/question.js +3 -20
  36. package/src/components/number-entry/question.test.js +17 -0
  37. package/src/components/radio/question.d.ts +1 -9
  38. package/src/components/radio/question.d.ts.map +1 -1
  39. package/src/components/radio/question.js +29 -46
  40. package/src/components/radio/question.test.js +139 -60
  41. package/src/components/select/question.d.ts +1 -15
  42. package/src/components/select/question.d.ts.map +1 -1
  43. package/src/components/select/question.js +4 -47
  44. package/src/components/single-line-input/question.d.ts +0 -4
  45. package/src/components/single-line-input/question.d.ts.map +1 -1
  46. package/src/components/single-line-input/question.js +1 -1
  47. package/src/components/text-entry/question.d.ts +0 -4
  48. package/src/components/text-entry/question.d.ts.map +1 -1
  49. package/src/components/text-entry/question.js +1 -1
  50. package/src/components/text-entry-redact/question.d.ts +2 -6
  51. package/src/components/text-entry-redact/question.d.ts.map +1 -1
  52. package/src/components/text-entry-redact/question.js +1 -1
  53. package/src/components/unit-option-entry/question.d.ts.map +1 -1
  54. package/src/components/unit-option-entry/question.js +3 -1
  55. package/src/components/unit-option-entry/question.test.js +15 -0
  56. package/src/components/utils/question-has-answer.js +1 -1
  57. package/src/components/utils/question-utils.d.ts +19 -1
  58. package/src/components/utils/question-utils.d.ts.map +1 -1
  59. package/src/components/utils/question-utils.js +31 -2
  60. package/src/components/utils/question-utils.test.js +89 -2
  61. package/src/questions/options-question.d.ts +4 -4
  62. package/src/questions/options-question.d.ts.map +1 -1
  63. package/src/questions/options-question.js +27 -2
  64. package/src/questions/options-question.test.js +106 -0
  65. package/src/questions/question.d.ts +16 -1
  66. package/src/questions/question.d.ts.map +1 -1
  67. package/src/questions/question.js +64 -9
  68. package/src/questions/question.test.js +86 -0
  69. package/test/journey.js +2 -0
  70. package/test/questions.js +50 -0
  71. package/test/snapshots/check-your-answers-not-started.html +22 -0
  72. package/test/snapshots/check-your-answers-with-answers.html +23 -1
  73. package/test/snapshots/favourite-activity-reason.html +54 -0
  74. package/test/snapshots/travel-requirements.html +48 -0
  75. package/test/utils/question-test-utils.js +38 -7
  76. package/typedefs/question-props.d.ts +33 -0
  77. package/typedefs/question-types.d.ts +4 -0
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)
55
+
56
+ ### Controllers
111
57
 
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 %}
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,93 +143,84 @@ 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
146
+ ### Validators
203
147
 
204
- Email validation is provided through the `EmailValidator` class and `EmailQuestion` component. The validator uses express-validator's robust email validation with configurable options.
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).
149
+
150
+ ### Custom Summary Formatting
151
+
152
+ The `formatSummaryValue` option allows you to customise how answers are displayed on check-your-answers pages without needing to create a custom question class.
205
153
 
206
154
  #### Basic Usage
207
155
 
156
+ Pass a `formatSummaryValue` function when creating a question:
157
+
208
158
  ```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;
159
+ import { RadioQuestion } from '@planning-inspectorate/dynamic-forms';
160
+
161
+ new RadioQuestion({
162
+ fieldName: 'status',
163
+ title: 'Application status',
164
+ question: 'What is the application status?',
165
+ options: [
166
+ { text: 'Approved', value: 'approved' },
167
+ { text: 'Rejected', value: 'rejected' }
168
+ ],
169
+ formatSummaryValue: ({ formattedAnswer }) => `<strong>${formattedAnswer}</strong>`
170
+ });
236
171
  ```
237
172
 
238
- The EmailQuestion uses the following default input attributes:
173
+ #### Formatter Context
174
+
175
+ The formatter function receives a context object with the following properties:
239
176
 
240
- - `type="email"`
241
- - `spellcheck="false"`
242
- - `autocomplete="email"`
177
+ | Property | Type | Description |
178
+ |----------|------|------------------------------------------------|
179
+ | `answer` | `unknown` | The raw answer value |
180
+ | `formattedAnswer` | `string` | The default formatted display value |
181
+ | `question` | `Question` | The question instance |
182
+ | `journey` | `Journey` | The journey instance |
183
+ | `sectionSegment` | `string` | The current section segment |
243
184
 
244
- #### Advanced Validation Options
185
+ #### Examples
245
186
 
246
- For stricter email validation requirements:
187
+ **Bold formatting:**
247
188
 
248
189
  ```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
- });
190
+ formatSummaryValue: ({ formattedAnswer }) => `<strong>${escapeHtml(formattedAnswer)}</strong>`
258
191
  ```
259
192
 
260
- #### Alternative: Single Line Input with Email Attributes
193
+ **Conditional styling based on answer:**
194
+
195
+ ```javascript
196
+ formatSummaryValue: ({ answer, formattedAnswer }) => {
197
+ const color = answer === 'approved' ? 'green' : 'red';
198
+ return `<span style="color: ${color}">${escapeHtml(formattedAnswer)}</span>`;
199
+ }
200
+ ```
261
201
 
262
- You can also configure a single line input with email-specific attributes:
202
+ **Adding a suffix from another answer:**
263
203
 
264
204
  ```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, {});
205
+ const suffixes = { solid: ' (tonnes)', liquid: ' (litres)' };
206
+
207
+ formatSummaryValue: ({ answer, formattedAnswer }) => {
208
+ const suffix = suffixes[answer] ?? '';
209
+ return `${formattedAnswer}${suffix}`;
210
+ }
287
211
  ```
288
212
 
213
+ **Accessing other journey answers:**
214
+
215
+ ```javascript
216
+ formatSummaryValue: ({ formattedAnswer, journey }) => {
217
+ const capacity = journey.response.answers.wasteCapacity;
218
+ return capacity ? `${formattedAnswer}: ${capacity}` : formattedAnswer;
219
+ }
220
+ ```
221
+
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`).
223
+
289
224
  ## Contributing
290
225
 
291
226
  When contributing to this package, ensure changes are generic and not service-specific. Speak to the R&D devs if you are not sure. Prefer configuration over hardcoding values, and ensure the code is well documented.
@@ -317,11 +252,11 @@ When adding a new question type, be sure to add an example question into `test/q
317
252
 
318
253
  To update any snapshots with rendering changes (or new questions), run `node --test --test-update-snapshots`.
319
254
 
320
- ### Components
255
+ ### New components
321
256
 
322
257
  When implement a new component, extend the base `Question` class. Common methods to override are:
323
258
 
324
- * `formatAnswerForSummary` - used for check-your-answers display
259
+ * `formatAnswer` - used for check-your-answers display
325
260
  * `getDataToSave` - answer data to save
326
261
  * `addCustomDataToViewModel` - customise the view model with extra data/configuration
327
262
  * `answerForViewModel` - customise the view model answer value
@@ -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
+ ```