@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.
- package/README.md +124 -189
- package/docs/Email Validation.md +86 -0
- package/docs/Getting Started.md +299 -0
- package/package.json +1 -1
- package/src/components/address/question.d.ts +1 -9
- package/src/components/address/question.d.ts.map +1 -1
- package/src/components/address/question.js +6 -17
- package/src/components/checkbox/question.d.ts +1 -21
- package/src/components/checkbox/question.d.ts.map +1 -1
- package/src/components/checkbox/question.js +38 -50
- package/src/components/checkbox/question.test.js +177 -26
- package/src/components/date/question.d.ts +0 -8
- package/src/components/date/question.d.ts.map +1 -1
- package/src/components/date/question.js +6 -14
- package/src/components/date-period/question.d.ts +1 -5
- package/src/components/date-period/question.d.ts.map +1 -1
- package/src/components/date-period/question.js +1 -1
- package/src/components/date-time/question.d.ts +0 -8
- package/src/components/date-time/question.d.ts.map +1 -1
- package/src/components/date-time/question.js +4 -12
- 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/manage-list/answer-summary-list.njk +1 -1
- package/src/components/manage-list/question.d.ts +1 -5
- package/src/components/manage-list/question.d.ts.map +1 -1
- package/src/components/manage-list/question.js +1 -1
- 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 +0 -28
- package/src/components/number-entry/question.d.ts.map +1 -1
- package/src/components/number-entry/question.js +3 -20
- package/src/components/number-entry/question.test.js +17 -0
- package/src/components/radio/question.d.ts +1 -9
- package/src/components/radio/question.d.ts.map +1 -1
- package/src/components/radio/question.js +29 -46
- package/src/components/radio/question.test.js +139 -60
- package/src/components/select/question.d.ts +1 -15
- package/src/components/select/question.d.ts.map +1 -1
- package/src/components/select/question.js +4 -47
- package/src/components/single-line-input/question.d.ts +0 -4
- package/src/components/single-line-input/question.d.ts.map +1 -1
- package/src/components/single-line-input/question.js +1 -1
- package/src/components/text-entry/question.d.ts +0 -4
- package/src/components/text-entry/question.d.ts.map +1 -1
- package/src/components/text-entry/question.js +1 -1
- package/src/components/text-entry-redact/question.d.ts +2 -6
- package/src/components/text-entry-redact/question.d.ts.map +1 -1
- package/src/components/text-entry-redact/question.js +1 -1
- 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/components/utils/question-has-answer.js +1 -1
- package/src/components/utils/question-utils.d.ts +19 -1
- package/src/components/utils/question-utils.d.ts.map +1 -1
- package/src/components/utils/question-utils.js +31 -2
- package/src/components/utils/question-utils.test.js +89 -2
- package/src/questions/options-question.d.ts +4 -4
- package/src/questions/options-question.d.ts.map +1 -1
- package/src/questions/options-question.js +27 -2
- package/src/questions/options-question.test.js +106 -0
- package/src/questions/question.d.ts +16 -1
- package/src/questions/question.d.ts.map +1 -1
- package/src/questions/question.js +64 -9
- package/src/questions/question.test.js +86 -0
- package/test/journey.js +2 -0
- package/test/questions.js +50 -0
- package/test/snapshots/check-your-answers-not-started.html +22 -0
- package/test/snapshots/check-your-answers-with-answers.html +23 -1
- package/test/snapshots/favourite-activity-reason.html +54 -0
- package/test/snapshots/travel-requirements.html +48 -0
- package/test/utils/question-test-utils.js +38 -7
- package/typedefs/question-props.d.ts +33 -0
- package/typedefs/question-types.d.ts +4 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
Follow these steps to get started using Dynamic Forms. This guide assumes you have a [Node.js project](https://nodejs.org/) setup with [npm](https://www.npmjs.com/), [expressjs](https://expressjs.com/), [Nunjucks](https://mozilla.github.io/nunjucks/), and the [GOV.UK frontend](https://design-system.service.gov.uk/) already.
|
|
4
|
+
|
|
5
|
+
## Step 1 - add the package dependency
|
|
6
|
+
|
|
7
|
+
Add dynamic-forms to your project with npm:
|
|
8
|
+
|
|
9
|
+
`npm install --save @planning-inspectorate/dynamic-forms`
|
|
10
|
+
|
|
11
|
+
If you are using the select component with [accessible-autocomplete](https://www.npmjs.com/package/accessible-autocomplete), also install this:
|
|
12
|
+
|
|
13
|
+
`npm install --save accessible-autocomplete`
|
|
14
|
+
|
|
15
|
+
## Step 2 - configure the dynamic forms root for Nunjucks
|
|
16
|
+
|
|
17
|
+
So that the nunjucks rendered can resolve the dynamic forms components, add the root to the nunjucks configuration. For example:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import nunjucks from 'nunjucks';
|
|
23
|
+
|
|
24
|
+
export function configureNunjucks() {
|
|
25
|
+
// get the require function, see https://nodejs.org/api/module.html#modulecreaterequirefilename
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
// path to dynamic forms folder
|
|
28
|
+
const dynamicFormsRoot = path.resolve(require.resolve('@planning-inspectorate/dynamic-forms'), '..');
|
|
29
|
+
// get the path to the govuk-frontend folder, in node_modules, using the node require resolution
|
|
30
|
+
const govukFrontendRoot = path.resolve(require.resolve('govuk-frontend'), '../..');
|
|
31
|
+
const appDir = path.join('some/path/to/the', 'app');
|
|
32
|
+
|
|
33
|
+
// configure nunjucks
|
|
34
|
+
return nunjucks.configure(
|
|
35
|
+
// ensure nunjucks templates can use govuk-frontend components, and templates we've defined
|
|
36
|
+
[dynamicFormsRoot, govukFrontendRoot, appDir],
|
|
37
|
+
{
|
|
38
|
+
// output with dangerous characters are escaped automatically
|
|
39
|
+
autoescape: true,
|
|
40
|
+
// automatically remove trailing newlines from a block/tag
|
|
41
|
+
trimBlocks: true,
|
|
42
|
+
// automatically remove leading whitespace from a block/tag
|
|
43
|
+
lstripBlocks: true
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Step 3 - add required assets
|
|
50
|
+
|
|
51
|
+
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:
|
|
52
|
+
|
|
53
|
+
- `/assets/css/accessible-autocomplete.min.css`
|
|
54
|
+
- `/assets/js/accessible-autocomplete.min.js`
|
|
55
|
+
|
|
56
|
+
If you have a build step for static assets, copy the accessible-autocomplete assets into the static folder. For example:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
/**
|
|
60
|
+
* Copy accessible-autocomplete assets into the static folder
|
|
61
|
+
*/
|
|
62
|
+
async function copyAutocompleteAssets({ staticDir, root }: AutocompleteOptions): Promise<void> {
|
|
63
|
+
const js = path.join(root, 'accessible-autocomplete.min.js');
|
|
64
|
+
const css = path.join(root, 'accessible-autocomplete.min.css');
|
|
65
|
+
|
|
66
|
+
const staticJs = path.join(staticDir, 'assets', 'js', 'accessible-autocomplete.min.js');
|
|
67
|
+
const staticCss = path.join(staticDir, 'assets', 'css', 'accessible-autocomplete.min.css');
|
|
68
|
+
|
|
69
|
+
await copyFile(js, staticJs);
|
|
70
|
+
await copyFile(css, staticCss);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Step 4 - add the question and check-your-answers templates
|
|
75
|
+
|
|
76
|
+
Dynamic forms requires two templates to work for rendering questions and rendering check-your-answers page. Different journeys can use different templates if required, but usually they can be shared.
|
|
77
|
+
|
|
78
|
+
### Journey (or question) template
|
|
79
|
+
|
|
80
|
+
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).
|
|
81
|
+
It should also include the rendering of the errorSummary component. The path to this template is configured via the `journeyTemplate` property on a Journey.
|
|
82
|
+
|
|
83
|
+
For example:
|
|
84
|
+
|
|
85
|
+
e.g.
|
|
86
|
+
|
|
87
|
+
```nunjucks
|
|
88
|
+
{% extends "govuk/template.njk" %}
|
|
89
|
+
|
|
90
|
+
{% block beforeContent %}
|
|
91
|
+
{% if errorSummary %}
|
|
92
|
+
<div class="govuk-grid-row">
|
|
93
|
+
<div class="govuk-grid-column-two-thirds">
|
|
94
|
+
{{ govukErrorSummary({
|
|
95
|
+
titleText: "There is a problem",
|
|
96
|
+
errorList: errorSummary,
|
|
97
|
+
attributes: {"data-cy": "error-wrapper"}
|
|
98
|
+
}) }}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
{% endif %}
|
|
102
|
+
{% endblock %}
|
|
103
|
+
|
|
104
|
+
{% block content %}
|
|
105
|
+
<div class="govuk-grid-row">
|
|
106
|
+
<div class="govuk-grid-column-full">
|
|
107
|
+
<h1 class="govuk-heading-xl">
|
|
108
|
+
{% if pageCaption %}
|
|
109
|
+
<span class="govuk-caption-xl">{{ pageCaption }}</span>
|
|
110
|
+
{% endif %}
|
|
111
|
+
{{ pageHeading }}
|
|
112
|
+
</h1>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{% block dynQuestionContent %}{% endblock %}
|
|
117
|
+
{% endblock %}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Task list (or check your answers) template
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
e.g.
|
|
125
|
+
|
|
126
|
+
```nunjucks
|
|
127
|
+
{% extends "govuk/template.njk" %}
|
|
128
|
+
|
|
129
|
+
{% block content %}
|
|
130
|
+
<div class="govuk-grid-row">
|
|
131
|
+
<div class="govuk-grid-column-full">
|
|
132
|
+
<h1 class="govuk-heading-xl">
|
|
133
|
+
{% if pageCaption %}
|
|
134
|
+
<span class="govuk-caption-xl">{{ pageCaption }}</span>
|
|
135
|
+
{% endif %}
|
|
136
|
+
{{ pageHeading }}
|
|
137
|
+
</h1>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{% block dynTaskList %}{% endblock %}
|
|
142
|
+
{% endblock %}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Step 5 - add questions, journeys, and routes
|
|
146
|
+
|
|
147
|
+
Now you are ready to implement any user journeys for your service. Add questions, journeys, and routes for each of them.
|
|
148
|
+
|
|
149
|
+
For example:
|
|
150
|
+
|
|
151
|
+
`questions.ts`
|
|
152
|
+
|
|
153
|
+
Use `satisfies Record<string, QuestionProps>` to get autocomplete for available question props specific for each component type.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import {
|
|
157
|
+
COMPONENT_TYPES,
|
|
158
|
+
createQuestions,
|
|
159
|
+
questionClasses,
|
|
160
|
+
RequiredValidator
|
|
161
|
+
} from '@planning-inspectorate/dynamic-forms';
|
|
162
|
+
import type { QuestionProps } from '@planning-inspectorate/dynamic-forms';
|
|
163
|
+
|
|
164
|
+
export const questionProps = {
|
|
165
|
+
destinationType: {
|
|
166
|
+
type: COMPONENT_TYPES.RADIO,
|
|
167
|
+
title: 'Destination type',
|
|
168
|
+
question: 'Which sort of place would you like to visit?',
|
|
169
|
+
fieldName: 'destinationType',
|
|
170
|
+
url: 'destination-type',
|
|
171
|
+
options: [
|
|
172
|
+
{ value: 'beach', text: 'Beach' },
|
|
173
|
+
{ value: 'mountains', text: 'Mountains' },
|
|
174
|
+
{ value: 'city', text: 'City' }
|
|
175
|
+
],
|
|
176
|
+
validators: [new RequiredValidator('Select a destination type')]
|
|
177
|
+
},
|
|
178
|
+
nights: {
|
|
179
|
+
type: COMPONENT_TYPES.NUMBER,
|
|
180
|
+
title: 'Number of Nights',
|
|
181
|
+
question: 'How many nights will you stay on holiday?',
|
|
182
|
+
fieldName: 'nights',
|
|
183
|
+
url: 'nights',
|
|
184
|
+
label: 'Nights'
|
|
185
|
+
}
|
|
186
|
+
} satisfies Record<string, QuestionProps>;
|
|
187
|
+
|
|
188
|
+
export function getQuestions() {
|
|
189
|
+
return createQuestions(questionProps, questionClasses, {}, {});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type Questions = ReturnType<typeof getQuestions>;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`journey.ts`
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import {
|
|
199
|
+
BOOLEAN_OPTIONS,
|
|
200
|
+
Journey,
|
|
201
|
+
type JourneyResponse,
|
|
202
|
+
Section,
|
|
203
|
+
whenQuestionHasAnswer
|
|
204
|
+
} from '@planning-inspectorate/dynamic-forms';
|
|
205
|
+
import type { Request } from 'express';
|
|
206
|
+
import type { Questions } from './questions.ts';
|
|
207
|
+
|
|
208
|
+
export const JOURNEY_ID = 'holidays';
|
|
209
|
+
|
|
210
|
+
export function createJourney(req: Request, response: JourneyResponse, questions: Questions) {
|
|
211
|
+
return new Journey({
|
|
212
|
+
journeyId: JOURNEY_ID,
|
|
213
|
+
sections: [
|
|
214
|
+
new Section('Questions', 'questions').addQuestion(questions.destinationType).addQuestion(questions.nights)
|
|
215
|
+
],
|
|
216
|
+
taskListUrl: 'check-your-answers',
|
|
217
|
+
journeyTemplate: 'views/layouts/question.njk',
|
|
218
|
+
taskListTemplate: 'views/layouts/check-your-answers.njk',
|
|
219
|
+
journeyTitle: 'Holidays',
|
|
220
|
+
returnToListing: false,
|
|
221
|
+
makeBaseUrl: () => req.baseUrl,
|
|
222
|
+
initialBackLink: '/',
|
|
223
|
+
response
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`index.ts`
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { Router as createRouter } from 'express';
|
|
232
|
+
import type { IRouter } from 'express';
|
|
233
|
+
import {
|
|
234
|
+
buildGetJourney,
|
|
235
|
+
buildGetJourneyResponseFromSession,
|
|
236
|
+
buildList,
|
|
237
|
+
buildSave,
|
|
238
|
+
question,
|
|
239
|
+
saveDataToSession,
|
|
240
|
+
validate,
|
|
241
|
+
validationErrorHandler
|
|
242
|
+
} from '@planning-inspectorate/dynamic-forms';
|
|
243
|
+
import { createJourney, JOURNEY_ID } from './journey.ts';
|
|
244
|
+
import { getQuestions } from './questions.ts';
|
|
245
|
+
|
|
246
|
+
export function createRoutes(): IRouter {
|
|
247
|
+
const router = createRouter({ mergeParams: true });
|
|
248
|
+
|
|
249
|
+
const questions = getQuestions();
|
|
250
|
+
const getJourneyResponse = buildGetJourneyResponseFromSession(JOURNEY_ID);
|
|
251
|
+
const getJourney = buildGetJourney((req, journeyResponse) => createJourney(req, journeyResponse, questions));
|
|
252
|
+
|
|
253
|
+
router.use(getJourneyResponse, getJourney);
|
|
254
|
+
|
|
255
|
+
// route for rendering each question
|
|
256
|
+
router.get('/:section/:question', question);
|
|
257
|
+
// route for saving each answer (to session)
|
|
258
|
+
router.post('/:section/:question', validate, validationErrorHandler, buildSave(saveDataToSession));
|
|
259
|
+
|
|
260
|
+
// route for check-your-answers page
|
|
261
|
+
router.get('/check-your-answers', buildList());
|
|
262
|
+
// route for saving all answers
|
|
263
|
+
router.post('/check-your-answers', (req, res) => {
|
|
264
|
+
const answers = res.locals?.journeyResponse?.answers as JourneyAnswers;
|
|
265
|
+
if (typeof answers !== 'object' || answers === null) {
|
|
266
|
+
throw new Error('answers should be an object');
|
|
267
|
+
}
|
|
268
|
+
console.log('answers', answers);
|
|
269
|
+
// TODO: save to a database or similar!
|
|
270
|
+
res.redirect('/');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return router;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface JourneyAnswers {
|
|
277
|
+
destinationType: string;
|
|
278
|
+
nights: number;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`router.ts`
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import type { IRouter } from 'express';
|
|
286
|
+
import { Router as createRouter } from 'express';
|
|
287
|
+
import { viewHomepage } from './views/home/controller.ts';
|
|
288
|
+
import { createRoutes as createHolidayRoutes } from './views/holidays/index.ts';
|
|
289
|
+
|
|
290
|
+
export function buildRouter(): IRouter {
|
|
291
|
+
const router = createRouter();
|
|
292
|
+
|
|
293
|
+
router.route('/').get(viewHomepage);
|
|
294
|
+
|
|
295
|
+
router.use('/holidays', createHolidayRoutes());
|
|
296
|
+
|
|
297
|
+
return router;
|
|
298
|
+
}
|
|
299
|
+
```
|
package/package.json
CHANGED
|
@@ -22,10 +22,6 @@ export class AddressQuestion extends Question {
|
|
|
22
22
|
* @returns {*|string}
|
|
23
23
|
*/
|
|
24
24
|
answerForViewModel(answers: Record<string, any>): any | string;
|
|
25
|
-
/**
|
|
26
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
27
|
-
*/
|
|
28
|
-
addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
|
|
29
25
|
/**
|
|
30
26
|
* @param {Object<string, any>} answer
|
|
31
27
|
* @returns The formatted address to be presented in the UI
|
|
@@ -36,11 +32,7 @@ export class AddressQuestion extends Question {
|
|
|
36
32
|
/**
|
|
37
33
|
* returns the formatted answers values to be used to build task list elements
|
|
38
34
|
*/
|
|
39
|
-
|
|
40
|
-
key: string;
|
|
41
|
-
value: string;
|
|
42
|
-
action: import("../../index.js").ActionView | import("../../index.js").ActionView[] | undefined;
|
|
43
|
-
}[];
|
|
35
|
+
formatAnswer(answer: any): string;
|
|
44
36
|
formatLabelFromRequiredFields(fieldName: any): "" | " (optional)";
|
|
45
37
|
}
|
|
46
38
|
export default AddressQuestion;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;IACC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,yBAAyB,EAqB3E;IAXE;;;;;;kBAA8C;IAIhD;;;;;;MAMC;IAiBF;;;OAGG;IACH,4BAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjB,GAAC,GAAC,MAAM,CAqBpB;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;IACC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,yBAAyB,EAqB3E;IAXE;;;;;;kBAA8C;IAIhD;;;;;;MAMC;IAiBF;;;OAGG;IACH,4BAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjB,GAAC,GAAC,MAAM,CAqBpB;IAqCD;;;OAGG;IACH;;eAUC;IAED;;OAEG;IACH,kCAIC;IAED,kEAMC;CACD;;yBA1IwB,WAAW"}
|
|
@@ -70,7 +70,7 @@ export class AddressQuestion extends Question {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
73
|
+
* @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
|
|
74
74
|
*/
|
|
75
75
|
addCustomDataToViewModel(viewModel) {
|
|
76
76
|
viewModel.question.labels = this.addressLabels;
|
|
@@ -123,23 +123,12 @@ export class AddressQuestion extends Question {
|
|
|
123
123
|
/**
|
|
124
124
|
* returns the formatted answers values to be used to build task list elements
|
|
125
125
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
formattedAnswer = nl2br(escape(this.format(answer)));
|
|
131
|
-
} else if (answer === null) {
|
|
132
|
-
formattedAnswer = '';
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return [
|
|
136
|
-
{
|
|
137
|
-
key: `${this.title}`,
|
|
138
|
-
value: formattedAnswer,
|
|
139
|
-
action: this.getAction(sectionSegment, journey, answer)
|
|
140
|
-
}
|
|
141
|
-
];
|
|
126
|
+
formatAnswer(answer) {
|
|
127
|
+
if (answer === null) return '';
|
|
128
|
+
if (!answer) return this.notStartedText;
|
|
129
|
+
return nl2br(escape(this.format(answer)));
|
|
142
130
|
}
|
|
131
|
+
|
|
143
132
|
formatLabelFromRequiredFields(fieldName) {
|
|
144
133
|
if (this.requiredFields && this.requiredFields[fieldName]) {
|
|
145
134
|
return '';
|
|
@@ -1,29 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef ConditionalAnswerObject
|
|
3
|
-
* @type {object}
|
|
4
|
-
* @property {string} value the checkbox answer
|
|
5
|
-
* @property {string} conditional the conditional text input
|
|
6
|
-
*/
|
|
7
1
|
export class CheckboxQuestion extends OptionsQuestion {
|
|
8
2
|
/**
|
|
9
3
|
* @param {import('#typedefs/question-props.d.ts').CheckboxQuestionParams} params
|
|
10
4
|
*/
|
|
11
|
-
constructor({
|
|
12
|
-
/**
|
|
13
|
-
* returns the formatted answers values to be used to build task list elements
|
|
14
|
-
*/
|
|
15
|
-
formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): import("../../index.js").SummaryRow[];
|
|
5
|
+
constructor({ ...parentParams }: import("#typedefs/question-props.d.ts").CheckboxQuestionParams);
|
|
16
6
|
}
|
|
17
7
|
export default CheckboxQuestion;
|
|
18
|
-
export type ConditionalAnswerObject = {
|
|
19
|
-
/**
|
|
20
|
-
* the checkbox answer
|
|
21
|
-
*/
|
|
22
|
-
value: string;
|
|
23
|
-
/**
|
|
24
|
-
* the conditional text input
|
|
25
|
-
*/
|
|
26
|
-
conditional: string;
|
|
27
|
-
};
|
|
28
8
|
import OptionsQuestion from '../../questions/options-question.js';
|
|
29
9
|
//# sourceMappingURL=question.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;IACC;;OAEG;IACH,iCAFW,OAAO,+BAA+B,EAAE,sBAAsB,EAWxE;CA4CD;;4BA7D2B,qCAAqC"}
|
|
@@ -1,76 +1,64 @@
|
|
|
1
|
+
import escape from 'escape-html';
|
|
1
2
|
import OptionsQuestion from '../../questions/options-question.js';
|
|
2
|
-
import questionUtils from '../utils/question-utils.js';
|
|
3
3
|
|
|
4
4
|
const defaultOptionJoinString = ',';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* @typedef ConditionalAnswerObject
|
|
8
|
-
* @type {object}
|
|
9
|
-
* @property {string} value the checkbox answer
|
|
10
|
-
* @property {string} conditional the conditional text input
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
6
|
export class CheckboxQuestion extends OptionsQuestion {
|
|
14
7
|
/**
|
|
15
8
|
* @param {import('#typedefs/question-props.d.ts').CheckboxQuestionParams} params
|
|
16
9
|
*/
|
|
17
|
-
constructor({
|
|
10
|
+
constructor({ ...parentParams }) {
|
|
18
11
|
super({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
url,
|
|
24
|
-
pageTitle,
|
|
25
|
-
description,
|
|
26
|
-
options,
|
|
27
|
-
validators,
|
|
28
|
-
viewData
|
|
12
|
+
// default but allow overrides
|
|
13
|
+
capitaliseAnswer: false,
|
|
14
|
+
...parentParams,
|
|
15
|
+
viewFolder: 'checkbox'
|
|
29
16
|
});
|
|
30
17
|
|
|
31
18
|
this.optionJoinString = defaultOptionJoinString;
|
|
32
19
|
}
|
|
33
20
|
|
|
34
21
|
/**
|
|
35
|
-
*
|
|
22
|
+
* Formats an answer value for display in the summary.
|
|
23
|
+
* Handles object answers with conditional fields.
|
|
24
|
+
* - Single value with conditional: { value: 'yes', conditional: { yes: 'details' } }
|
|
25
|
+
* - Multiple values with conditionals: { value: 'yes,no', conditional: { yes: 'details1', no: 'details2' } }
|
|
26
|
+
*
|
|
27
|
+
* @param {unknown} answer - the raw answer value
|
|
28
|
+
* @returns {string} the formatted answer for display
|
|
36
29
|
*/
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
30
|
+
formatAnswer(answer) {
|
|
31
|
+
if (answer === null || answer === undefined || answer === '') {
|
|
32
|
+
return this.notStartedText;
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
//
|
|
43
|
-
if (answer
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const conditionalAnswerText = selectedOption.conditional?.label
|
|
47
|
-
? `${selectedOption.conditional.label} ${answer.conditional}`
|
|
48
|
-
: answer.conditional;
|
|
49
|
-
|
|
50
|
-
const formattedConditionalText = [selectedOption.text, conditionalAnswerText].join('\n');
|
|
51
|
-
|
|
52
|
-
return super.formatAnswerForSummary(sectionSegment, journey, formattedConditionalText, false);
|
|
35
|
+
// Handle simple string answers (e.g. '1,2')
|
|
36
|
+
if (typeof answer !== 'object' || answer.value === undefined) {
|
|
37
|
+
return super.formatAnswer(answer);
|
|
53
38
|
}
|
|
54
39
|
|
|
55
|
-
//
|
|
56
|
-
|
|
40
|
+
// Handle object answers with conditional fields
|
|
41
|
+
// TODO DF-51 treat as an array
|
|
42
|
+
const answerValues = String(answer.value)
|
|
43
|
+
.split(this.optionJoinString)
|
|
44
|
+
.map((v) => v.trim());
|
|
45
|
+
const conditionals = answer.conditional || {};
|
|
46
|
+
|
|
47
|
+
const formattedParts = answerValues.map((value) => {
|
|
48
|
+
const option = this.options.find((opt) => opt.value === value);
|
|
49
|
+
const optionText = escape(option ? option.text : value);
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
questionUtils.getConditionalFieldName(this.fieldName, option.conditional.fieldName)
|
|
65
|
-
];
|
|
66
|
-
return [option.text, conditionalAnswer].join('\n');
|
|
67
|
-
}
|
|
51
|
+
// Check if this option has a conditional answer
|
|
52
|
+
const conditionalValue = conditionals[value];
|
|
53
|
+
if (conditionalValue) {
|
|
54
|
+
const label = option?.conditional?.label ? `${escape(option.conditional.label)} ` : '';
|
|
55
|
+
return `${optionText}<br>${label}${escape(conditionalValue)}`;
|
|
56
|
+
}
|
|
68
57
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.join('\n');
|
|
58
|
+
return optionText;
|
|
59
|
+
});
|
|
72
60
|
|
|
73
|
-
return
|
|
61
|
+
return formattedParts.join('<br>');
|
|
74
62
|
}
|
|
75
63
|
}
|
|
76
64
|
|