@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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/dynamic-forms",
3
- "version": "3.16.0",
3
+ "version": "3.18.0",
4
4
  "description": "This package is for building GDS forms using a configuration-based approach.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -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
- formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
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;IAED;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAI/C;IA8BD;;;OAGG;IACH;;eAUC;IAED;;OAEG;IACH;;;;QAgBC;IACD,kEAMC;CACD;;yBArJwB,WAAW"}
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
- formatAnswerForSummary(sectionSegment, journey, answer) {
127
- let formattedAnswer = this.notStartedText;
128
-
129
- if (answer) {
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({ title, question, fieldName, url, pageTitle, description, options, validators, viewData }: import("#typedefs/question-props.d.ts").CheckboxQuestionParams);
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;;;;;GAKG;AAEH;IACC;;OAEG;IACH,wGAFW,OAAO,+BAA+B,EAAE,sBAAsB,EAiBxE;IAED;;OAEG;IACH,8GAqCC;CACD;;;;;;WAlEa,MAAM;;;;iBACN,MAAM;;4BATQ,qCAAqC"}
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({ title, question, fieldName, url, pageTitle, description, options, validators, viewData }) {
10
+ constructor({ ...parentParams }) {
18
11
  super({
19
- title,
20
- question,
21
- viewFolder: 'checkbox',
22
- fieldName,
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
- * returns the formatted answers values to be used to build task list elements
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
- formatAnswerForSummary(sectionSegment, journey, answer) {
38
- if (!answer) {
39
- return super.formatAnswerForSummary(sectionSegment, journey, answer, false);
30
+ formatAnswer(answer) {
31
+ if (answer === null || answer === undefined || answer === '') {
32
+ return this.notStartedText;
40
33
  }
41
34
 
42
- // answer is single ConditionalAnswerObject
43
- if (answer?.conditional) {
44
- const selectedOption = this.options.find((option) => option.value === answer.value);
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
- // answer is a string
56
- const answerArray = answer.split(this.optionJoinString);
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
- const formattedAnswer = this.options
59
- .filter((option) => answerArray.includes(option.value))
60
- .map((option) => {
61
- if (option.conditional) {
62
- const conditionalAnswer =
63
- journey.response.answers[
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
- return option.text;
70
- })
71
- .join('\n');
58
+ return optionText;
59
+ });
72
60
 
73
- return super.formatAnswerForSummary(sectionSegment, journey, formattedAnswer, false);
61
+ return formattedParts.join('<br>');
74
62
  }
75
63
  }
76
64