@planning-inspectorate/dynamic-forms 2.4.0 → 3.1.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 +12 -1
- package/package.json +3 -2
- package/src/components/address/question.js +25 -19
- package/src/components/address/question.test.js +1 -1
- package/src/components/date/question.js +7 -21
- package/src/components/date/question.test.js +1 -1
- package/src/components/date-period/question.js +17 -25
- package/src/components/date-period/question.test.js +1 -1
- package/src/components/date-time/question.js +10 -23
- package/src/components/identifier/question.js +3 -6
- package/src/components/manage-list/README.md +97 -0
- package/src/components/manage-list/answer-summary-list.njk +10 -0
- package/src/components/manage-list/index.njk +61 -0
- package/src/components/manage-list/manage-list-section.js +39 -0
- package/src/components/manage-list/manage-list-section.test.js +52 -0
- package/src/components/manage-list/question.js +147 -0
- package/src/components/manage-list/question.test.js +139 -0
- package/src/components/multi-field-input/index.njk +1 -1
- package/src/components/multi-field-input/question.js +12 -13
- package/src/components/multi-field-input/question.test.js +8 -8
- package/src/components/number-entry/question.js +7 -13
- package/src/components/radio/question.js +2 -4
- package/src/components/select/question.js +2 -4
- package/src/components/single-line-input/question.js +5 -6
- package/src/components/text-entry/question.js +5 -5
- package/src/components/text-entry-redact/question.js +18 -9
- package/src/components/text-entry-redact/question.test.js +12 -19
- package/src/components/unit-option-entry/question.js +9 -8
- package/src/components/utils/nunjucks.js +20 -0
- package/src/controller.js +39 -10
- package/src/controller.test.js +90 -24
- package/src/index.js +3 -2
- package/src/journey/journey-response.js +2 -2
- package/src/journey/journey-types.d.ts +21 -1
- package/src/journey/journey.js +64 -18
- package/src/journey/journey.test.js +80 -15
- package/src/questions/options-question.js +6 -7
- package/src/questions/question-props.d.ts +2 -1
- package/src/questions/question-types.d.ts +26 -0
- package/src/questions/question.js +119 -15
- package/src/questions/question.test.js +98 -5
- package/src/questions/questions.js +3 -1
- package/src/section-types.d.ts +18 -0
- package/src/section.js +40 -11
- package/src/section.test.js +89 -0
- package/test/journey.js +5 -0
- package/test/mock/journey.js +17 -0
- package/test/questions.js +30 -1
- package/test/questions.test.js +104 -40
- package/test/snapshots/check-your-answers-not-started.html +11 -0
- package/test/snapshots/check-your-answers-with-answers.html +11 -0
- package/test/snapshots/manage-list-answer-summary.html +14 -0
- package/test/snapshots/manage-list-render-questions.html +97 -0
- package/test/snapshots/manage-list-render.html +91 -0
- package/test/snapshots/travel-companions.html +19 -0
- package/test/snapshots/travel-companions_travel-companion-email.html +21 -0
- package/test/snapshots/travel-companions_travel-companion-name.html +21 -0
- package/tsconfig.json +2 -1
package/README.md
CHANGED
|
@@ -296,4 +296,15 @@ There are some lightweight tests in the `test` directory which sets up a basic j
|
|
|
296
296
|
|
|
297
297
|
When adding a new question type, be sure to add an example question into `test/questions.js`, and mock answers into `test/questions.test.js#mockAnswerBody` and `test/questions.test.js#mockAnswer`. Also, the question should be added to the journey in `test/journey.js`.
|
|
298
298
|
|
|
299
|
-
To update any snapshots with rendering changes (or new questions), run `node --test --test-update-snapshots`.
|
|
299
|
+
To update any snapshots with rendering changes (or new questions), run `node --test --test-update-snapshots`.
|
|
300
|
+
|
|
301
|
+
### Components
|
|
302
|
+
|
|
303
|
+
When implement a new component, extend the base `Question` class. Common methods to override are:
|
|
304
|
+
|
|
305
|
+
* `formatAnswerForSummary` - used for check-your-answers display
|
|
306
|
+
* `getDataToSave` - answer data to save
|
|
307
|
+
* `addCustomDataToViewModel` - customise the view model with extra data/configuration
|
|
308
|
+
* `answerForViewModel` - customise the view model answer value
|
|
309
|
+
|
|
310
|
+
Where possible `addCustomDataToViewModel` and `answerForViewModel` should be overridden instead of `prepQuestionForRendering`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planning-inspectorate/dynamic-forms",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.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",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"imports": {
|
|
28
28
|
"#question-types": "./src/questions/question-types.d.ts",
|
|
29
29
|
"#src/*": "./src/*",
|
|
30
|
-
"#test/*": "./test/*"
|
|
30
|
+
"#test/*": "./test/*",
|
|
31
|
+
"#question": "./src/questions/question.js"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@commitlint/cli": "^20.0.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
|
|
3
3
|
import escape from 'escape-html';
|
|
4
4
|
import { Address } from '../../lib/address.js';
|
|
@@ -40,29 +40,35 @@ export default class AddressQuestion extends Question {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* @param {
|
|
44
|
-
* @
|
|
45
|
-
* @param {Record<string, unknown>} customViewData
|
|
46
|
-
* @returns {QuestionViewModel}
|
|
43
|
+
* @param {Record<string, any>} answers
|
|
44
|
+
* @returns {*|string}
|
|
47
45
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
townCity: address.townCity || '',
|
|
58
|
-
county: address.county || '',
|
|
59
|
-
postcode: address.postcode || ''
|
|
46
|
+
answerForViewModel(answers) {
|
|
47
|
+
let address = answers[this.fieldName];
|
|
48
|
+
if (!address) {
|
|
49
|
+
address = {
|
|
50
|
+
addressLine1: answers[this.fieldName + '_addressLine1'],
|
|
51
|
+
addressLine2: answers[this.fieldName + '_addressLine2'],
|
|
52
|
+
townCity: answers[this.fieldName + '_townCity'],
|
|
53
|
+
county: answers[this.fieldName + '_county'],
|
|
54
|
+
postcode: answers[this.fieldName + '_postcode']
|
|
60
55
|
};
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
return {
|
|
59
|
+
addressLine1: address?.addressLine1 || '',
|
|
60
|
+
addressLine2: address?.addressLine2 || '',
|
|
61
|
+
townCity: address?.townCity || '',
|
|
62
|
+
county: address?.county || '',
|
|
63
|
+
postcode: address?.postcode || ''
|
|
64
|
+
};
|
|
65
|
+
}
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
/**
|
|
68
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
69
|
+
*/
|
|
70
|
+
addCustomDataToViewModel(viewModel) {
|
|
71
|
+
viewModel.question.labels = this.addressLabels;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert';
|
|
|
3
3
|
import AddressQuestion from './question.js';
|
|
4
4
|
import { Address } from '../../lib/address.js';
|
|
5
5
|
import AddressValidator from '../../validator/address-validator.js';
|
|
6
|
-
import { configureNunjucksTestEnv } from '
|
|
6
|
+
import { configureNunjucksTestEnv } from '#test/utils/nunjucks.js';
|
|
7
7
|
|
|
8
8
|
describe('AddressQuestion', () => {
|
|
9
9
|
const TITLE = 'What is the site address?';
|
|
@@ -50,29 +50,17 @@ export default class DateQuestion extends Question {
|
|
|
50
50
|
return responseToSave;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
* gets the view model for this question
|
|
55
|
-
* @param {Section} section - the current section
|
|
56
|
-
* @param {Journey} journey - the journey we are in
|
|
57
|
-
* @param {Object|undefined} [customViewData] additional data to send to view
|
|
58
|
-
* @returns {QuestionViewModel & { answer: Record<string, unknown> }}
|
|
59
|
-
*/
|
|
60
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
61
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
|
|
62
|
-
|
|
63
|
-
/** @type {Record<string, unknown>} */
|
|
64
|
-
let answer = {};
|
|
53
|
+
answerForViewModel(answers, isPayload) {
|
|
65
54
|
let day;
|
|
66
55
|
let month;
|
|
67
56
|
let year;
|
|
68
57
|
|
|
69
|
-
if (
|
|
70
|
-
day =
|
|
71
|
-
month =
|
|
72
|
-
year =
|
|
58
|
+
if (isPayload && answers) {
|
|
59
|
+
day = answers[`${this.fieldName}_day`];
|
|
60
|
+
month = answers[`${this.fieldName}_month`];
|
|
61
|
+
year = answers[`${this.fieldName}_year`];
|
|
73
62
|
} else {
|
|
74
|
-
const answerDateString =
|
|
75
|
-
|
|
63
|
+
const answerDateString = answers[this.fieldName];
|
|
76
64
|
if (answerDateString && (typeof answerDateString === 'string' || answerDateString instanceof Date)) {
|
|
77
65
|
const answerDate = new Date(answerDateString);
|
|
78
66
|
day = formatDateForDisplay(answerDate, { format: 'd' });
|
|
@@ -81,13 +69,11 @@ export default class DateQuestion extends Question {
|
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
71
|
|
|
84
|
-
|
|
72
|
+
return {
|
|
85
73
|
[`${this.fieldName}_day`]: day,
|
|
86
74
|
[`${this.fieldName}_month`]: month,
|
|
87
75
|
[`${this.fieldName}_year`]: year
|
|
88
76
|
};
|
|
89
|
-
|
|
90
|
-
return { ...viewModel, answer, question: { ...viewModel.question, value: answer } };
|
|
91
77
|
}
|
|
92
78
|
|
|
93
79
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatDateForDisplay, parseDateInput } from '../../lib/date-utils.js';
|
|
2
|
-
import { Question } from '
|
|
2
|
+
import { Question } from '#question';
|
|
3
3
|
import { nl2br } from '../../lib/utils.js';
|
|
4
4
|
import escape from 'escape-html';
|
|
5
5
|
|
|
@@ -77,20 +77,7 @@ export default class DatePeriodQuestion extends Question {
|
|
|
77
77
|
return responseToSave;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
* gets the view model for this question
|
|
82
|
-
* @param {Section} section - the current section
|
|
83
|
-
* @param {Journey} journey - the journey we are in
|
|
84
|
-
* @param {Object|undefined} [customViewData] additional data to send to view
|
|
85
|
-
* @param {Object|undefined} [payload] the current payload
|
|
86
|
-
* @returns {QuestionViewModel & { answer: Record<string, unknown> }}
|
|
87
|
-
*/
|
|
88
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
89
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
|
|
90
|
-
viewModel.labels = this.labels;
|
|
91
|
-
|
|
92
|
-
/** @type {Record<string, unknown>} */
|
|
93
|
-
let answer;
|
|
80
|
+
answerForViewModel(answers, isPayload) {
|
|
94
81
|
let startDay;
|
|
95
82
|
let startMonth;
|
|
96
83
|
let startYear;
|
|
@@ -98,15 +85,15 @@ export default class DatePeriodQuestion extends Question {
|
|
|
98
85
|
let endMonth;
|
|
99
86
|
let endYear;
|
|
100
87
|
|
|
101
|
-
if (
|
|
102
|
-
startDay =
|
|
103
|
-
startMonth =
|
|
104
|
-
startYear =
|
|
105
|
-
endDay =
|
|
106
|
-
endMonth =
|
|
107
|
-
endYear =
|
|
88
|
+
if (isPayload) {
|
|
89
|
+
startDay = answers[`${this.fieldName}_start_day`];
|
|
90
|
+
startMonth = answers[`${this.fieldName}_start_month`];
|
|
91
|
+
startYear = answers[`${this.fieldName}_start_year`];
|
|
92
|
+
endDay = answers[`${this.fieldName}_end_day`];
|
|
93
|
+
endMonth = answers[`${this.fieldName}_end_month`];
|
|
94
|
+
endYear = answers[`${this.fieldName}_end_year`];
|
|
108
95
|
} else {
|
|
109
|
-
const answerPeriod =
|
|
96
|
+
const answerPeriod = answers[this.fieldName];
|
|
110
97
|
if (answerPeriod && answerPeriod.start) {
|
|
111
98
|
const startDate = new Date(answerPeriod.start);
|
|
112
99
|
startDay = formatDateForDisplay(startDate, { format: 'd' });
|
|
@@ -121,7 +108,7 @@ export default class DatePeriodQuestion extends Question {
|
|
|
121
108
|
}
|
|
122
109
|
}
|
|
123
110
|
|
|
124
|
-
|
|
111
|
+
return {
|
|
125
112
|
[`${this.fieldName}_start_day`]: startDay,
|
|
126
113
|
[`${this.fieldName}_start_month`]: startMonth,
|
|
127
114
|
[`${this.fieldName}_start_year`]: startYear,
|
|
@@ -129,8 +116,13 @@ export default class DatePeriodQuestion extends Question {
|
|
|
129
116
|
[`${this.fieldName}_end_month`]: endMonth,
|
|
130
117
|
[`${this.fieldName}_end_year`]: endYear
|
|
131
118
|
};
|
|
119
|
+
}
|
|
132
120
|
|
|
133
|
-
|
|
121
|
+
/**
|
|
122
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
123
|
+
*/
|
|
124
|
+
addCustomDataToViewModel(viewModel) {
|
|
125
|
+
viewModel.labels = this.labels;
|
|
134
126
|
}
|
|
135
127
|
|
|
136
128
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
|
|
4
|
-
import { Question } from '
|
|
4
|
+
import { Question } from '#question';
|
|
5
5
|
import DatePeriodQuestion from './question.js';
|
|
6
6
|
|
|
7
7
|
describe('DatePeriodQuestion', () => {
|
|
@@ -52,18 +52,7 @@ export default class DateTimeQuestion extends Question {
|
|
|
52
52
|
return responseToSave;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
* gets the view model for this question
|
|
57
|
-
* @param {Section} section - the current section
|
|
58
|
-
* @param {Journey} journey - the journey we are in
|
|
59
|
-
* @param {Object|undefined} [customViewData] additional data to send to view
|
|
60
|
-
* @returns {QuestionViewModel & { answer: Record<string, unknown> }}
|
|
61
|
-
*/
|
|
62
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
63
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
|
|
64
|
-
|
|
65
|
-
/** @type {Record<string, unknown>} */
|
|
66
|
-
let answer = {};
|
|
55
|
+
answerForViewModel(answers, isPayload) {
|
|
67
56
|
let day;
|
|
68
57
|
let month;
|
|
69
58
|
let year;
|
|
@@ -71,15 +60,15 @@ export default class DateTimeQuestion extends Question {
|
|
|
71
60
|
let minutes;
|
|
72
61
|
let period;
|
|
73
62
|
|
|
74
|
-
if (
|
|
75
|
-
day =
|
|
76
|
-
month =
|
|
77
|
-
year =
|
|
78
|
-
hour =
|
|
79
|
-
minutes =
|
|
80
|
-
period =
|
|
63
|
+
if (isPayload) {
|
|
64
|
+
day = answers[`${this.fieldName}_day`];
|
|
65
|
+
month = answers[`${this.fieldName}_month`];
|
|
66
|
+
year = answers[`${this.fieldName}_year`];
|
|
67
|
+
hour = answers[`${this.fieldName}_hour`];
|
|
68
|
+
minutes = answers[`${this.fieldName}_minutes`];
|
|
69
|
+
period = answers[`${this.fieldName}_period`];
|
|
81
70
|
} else {
|
|
82
|
-
const answerDateString =
|
|
71
|
+
const answerDateString = answers[this.fieldName];
|
|
83
72
|
|
|
84
73
|
if (answerDateString && (typeof answerDateString === 'string' || answerDateString instanceof Date)) {
|
|
85
74
|
const answerDate = new Date(answerDateString);
|
|
@@ -92,7 +81,7 @@ export default class DateTimeQuestion extends Question {
|
|
|
92
81
|
}
|
|
93
82
|
}
|
|
94
83
|
|
|
95
|
-
|
|
84
|
+
return {
|
|
96
85
|
[`${this.fieldName}_day`]: day,
|
|
97
86
|
[`${this.fieldName}_month`]: month,
|
|
98
87
|
[`${this.fieldName}_year`]: year,
|
|
@@ -100,8 +89,6 @@ export default class DateTimeQuestion extends Question {
|
|
|
100
89
|
[`${this.fieldName}_minutes`]: minutes,
|
|
101
90
|
[`${this.fieldName}_period`]: period
|
|
102
91
|
};
|
|
103
|
-
|
|
104
|
-
return { ...viewModel, answer, question: { ...viewModel.question, value: answer } };
|
|
105
92
|
}
|
|
106
93
|
|
|
107
94
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
|
|
3
3
|
export default class IdentifierQuestion extends Question {
|
|
4
4
|
/** @type {string|undefined} page h1, optional, will default to use question's label */
|
|
@@ -22,13 +22,10 @@ export default class IdentifierQuestion extends Question {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
26
26
|
*/
|
|
27
|
-
|
|
28
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
|
|
27
|
+
addCustomDataToViewModel(viewModel) {
|
|
29
28
|
viewModel.question.inputClasses = this.inputClasses;
|
|
30
29
|
viewModel.question.label = this.label;
|
|
31
|
-
viewModel.question.value = payload ? payload[viewModel.question.fieldName] : viewModel.question.value;
|
|
32
|
-
return viewModel;
|
|
33
30
|
}
|
|
34
31
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Manage list
|
|
2
|
+
|
|
3
|
+
The manage list component uses a similar "add to a list" pattern found in other design systems, including:
|
|
4
|
+
|
|
5
|
+
- [Ministry of Justice
|
|
6
|
+
Design System](https://design-patterns.service.justice.gov.uk/patterns/add-to-a-list/)
|
|
7
|
+
- [HM Revenue & Customs
|
|
8
|
+
Design resources](https://design.tax.service.gov.uk/hmrc-design-patterns/add-to-a-list/)
|
|
9
|
+
- [Home Office User-Centred Design Manual](https://design.homeoffice.gov.uk/design-system/patterns/help-users-to/add-multiple-things)
|
|
10
|
+
|
|
11
|
+
Users are able to:
|
|
12
|
+
- repeat a small journey to add multiple items into a list
|
|
13
|
+
- change items in the list
|
|
14
|
+
- remove items from the list
|
|
15
|
+
- check their answers
|
|
16
|
+
|
|
17
|
+
The question view is the check-your-answers page, which by default shows a summary list. The summary list has the value for the item added, and action links for editing or removing the item.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
To use the manage list component, configure a question for the manage list and for each question within the "add to list" journey. The questions for the manage list journey are added to a `ManageListSection` and passed to `Section.addQuestion`. For example:
|
|
22
|
+
|
|
23
|
+
### 1. Set up the questions
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// Define question configuration
|
|
27
|
+
const questionProps = {
|
|
28
|
+
// ... other questions
|
|
29
|
+
// manage list question
|
|
30
|
+
holidayActivitiesList: {
|
|
31
|
+
type: COMPONENT_TYPES.MANAGE_LIST,
|
|
32
|
+
title: 'Holiday Activities',
|
|
33
|
+
question: 'Holiday Activities',
|
|
34
|
+
fieldName: 'holidayActivitiesList',
|
|
35
|
+
url: 'holiday-activities-list'
|
|
36
|
+
},
|
|
37
|
+
// other questions to use for each item in the list
|
|
38
|
+
holidayActivityType: {
|
|
39
|
+
type: COMPONENT_TYPES.CHECKBOX,
|
|
40
|
+
title: 'Holiday Activity Type',
|
|
41
|
+
question: 'Which type of holiday activity is it?',
|
|
42
|
+
fieldName: 'holidayActivityType',
|
|
43
|
+
url: 'holiday-activity-type',
|
|
44
|
+
options: [
|
|
45
|
+
{ value: 'swimming', text: 'Swimming' },
|
|
46
|
+
{ value: 'hiking', text: 'Hiking' },
|
|
47
|
+
{ value: 'sightseeing', text: 'Sightseeing' }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
holidayActivityDate: {
|
|
51
|
+
type: COMPONENT_TYPES.DATE,
|
|
52
|
+
title: 'Holiday Activity Date',
|
|
53
|
+
question: 'On which date will you do the holiday activity?',
|
|
54
|
+
fieldName: 'holidayActivityDate',
|
|
55
|
+
url: 'holiday-activity-date'
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Set up the section within the journey
|
|
61
|
+
|
|
62
|
+
You can use conditional logic as normal in the `ManageListSection`.
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
new Section('New Holiday', 'holiday')
|
|
66
|
+
.addQuestion(questions.holidayDestination)
|
|
67
|
+
// add a manage list question like any other, but include the `ManageListSection` as well
|
|
68
|
+
.addQuestion(
|
|
69
|
+
questions.holidayActivitiesList,
|
|
70
|
+
new ManageListSection()
|
|
71
|
+
.addQuestion(questions.holidayActivityType)
|
|
72
|
+
.addQuestion(questions.holidayActivityDate)
|
|
73
|
+
)
|
|
74
|
+
.addQuestion(questions.departureDate)
|
|
75
|
+
.addQuestion(questions.holidayPeriod)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Add extra route parameters
|
|
79
|
+
|
|
80
|
+
`{/:manageListAction/:manageListItemId/:manageListQuestion}` optional route parameters are added to the get and post routes for questions
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
router.get('/:section/:question{/:manageListAction/:manageListItemId/:manageListQuestion}',
|
|
84
|
+
getJourneyResponse,
|
|
85
|
+
getJourney,
|
|
86
|
+
question
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
router.post(
|
|
90
|
+
'/:section/:question{/:manageListAction/:manageListItemId/:manageListQuestion}',
|
|
91
|
+
getJourneyResponse,
|
|
92
|
+
getJourney,
|
|
93
|
+
validate,
|
|
94
|
+
validationErrorHandler,
|
|
95
|
+
buildSave(saveDataToSession)
|
|
96
|
+
);
|
|
97
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{% extends layoutTemplate %}
|
|
2
|
+
|
|
3
|
+
{% from "govuk/components/button/macro.njk" import govukButton %}
|
|
4
|
+
|
|
5
|
+
{% block dynQuestionContent %}
|
|
6
|
+
<div class="govuk-grid-row">
|
|
7
|
+
<div class="govuk-grid-column-two-thirds">
|
|
8
|
+
<form action="" method="post" novalidate>
|
|
9
|
+
{% if _csrf %}<input type="hidden" name="_csrf" value="{{ _csrf }}">{% endif %}
|
|
10
|
+
|
|
11
|
+
<h1 class="govuk-heading-l">You have added {{ question.value.length }} {{ question.pageTitle }}</h1>
|
|
12
|
+
|
|
13
|
+
<dl class="govuk-summary-list">
|
|
14
|
+
{% for item in question.valueSummary %}
|
|
15
|
+
<div class="govuk-summary-list__row ">
|
|
16
|
+
<dt class="govuk-summary-list__key">
|
|
17
|
+
{{ titleSingular }} {{ loop.index }}
|
|
18
|
+
</dt>
|
|
19
|
+
<dt class="govuk-summary-list__value">
|
|
20
|
+
{% for value in item.value %}
|
|
21
|
+
{% if showManageListQuestions %}
|
|
22
|
+
{{ value.question }}:<br>
|
|
23
|
+
{% endif %}
|
|
24
|
+
{{ value.answer | safe }}
|
|
25
|
+
{% if not loop.last %}
|
|
26
|
+
<br>{% if showManageListQuestions %}<br>{% endif %}
|
|
27
|
+
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% endfor %}
|
|
30
|
+
</dt>
|
|
31
|
+
<dd class="govuk-summary-list__actions">
|
|
32
|
+
<ul class="govuk-summary-list__actions-list">
|
|
33
|
+
<li class="govuk-summary-list__actions-list-item">
|
|
34
|
+
<a class="govuk-link" href="{{ originalUrl }}/edit/{{ item.id }}/{{ question.firstQuestionUrl }}">
|
|
35
|
+
Change <span class="govuk-visually-hidden">{{ titleSingular }} {{ loop.index }}</span>
|
|
36
|
+
</a>
|
|
37
|
+
</li>
|
|
38
|
+
<li class="govuk-summary-list__actions-list-item">
|
|
39
|
+
<a class="govuk-link" href="{{ originalUrl }}/remove/{{ item.id }}/confirm">
|
|
40
|
+
Remove <span class="govuk-visually-hidden">{{ titleSingular }} {{ loop.index }}</span>
|
|
41
|
+
</a>
|
|
42
|
+
</li>
|
|
43
|
+
</ul>
|
|
44
|
+
</dd>
|
|
45
|
+
</div>
|
|
46
|
+
{% endfor %}
|
|
47
|
+
</dl>
|
|
48
|
+
|
|
49
|
+
<p class="govuk-body">
|
|
50
|
+
<a href="{{ originalUrl }}/{{ question.addAnotherLink }}" class="govuk-link">Add another</a>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
{{ govukButton({
|
|
54
|
+
text: continueButtonText,
|
|
55
|
+
type: "submit",
|
|
56
|
+
attributes: { "data-cy":"button-save-and-continue"}
|
|
57
|
+
}) }}
|
|
58
|
+
</form>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
{% endblock %}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Section } from '#src/section.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extends the Section class for extra logic around managing lists.
|
|
5
|
+
*/
|
|
6
|
+
export class ManageListSection extends Section {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('unused', 'unused');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Is this section a manage list section?
|
|
13
|
+
*
|
|
14
|
+
* Used by controller and other logic.
|
|
15
|
+
*
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
get isManageListSection() {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fluent API method for adding questions
|
|
24
|
+
* @param {import('#src/questions/question.js').Question} question
|
|
25
|
+
* @param {import('#src/components/manage-list/manage-list-section.js').ManageListSection} [manageListSection]
|
|
26
|
+
* @returns {Section}
|
|
27
|
+
*/
|
|
28
|
+
addQuestion(question, manageListSection) {
|
|
29
|
+
if (!question) {
|
|
30
|
+
throw new Error('question is required');
|
|
31
|
+
}
|
|
32
|
+
if (question.isManageListQuestion || Boolean(manageListSection)) {
|
|
33
|
+
throw new Error('manage list sections do not support nested manage list questions');
|
|
34
|
+
}
|
|
35
|
+
// mark that this question is within a manage list section for routing and controller logic
|
|
36
|
+
question.isInManageListSection = true;
|
|
37
|
+
return super.addQuestion(question);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { ManageListSection } from '#src/components/manage-list/manage-list-section.js';
|
|
4
|
+
import { Section } from '#src/section.js';
|
|
5
|
+
|
|
6
|
+
describe('components/manage-list/manage-list-section', () => {
|
|
7
|
+
it('should extend Section', () => {
|
|
8
|
+
assert.ok(ManageListSection.prototype instanceof Section);
|
|
9
|
+
});
|
|
10
|
+
it('should implement isManageListSection', () => {
|
|
11
|
+
const mls = new ManageListSection();
|
|
12
|
+
assert.strictEqual(mls.isManageListSection, true);
|
|
13
|
+
});
|
|
14
|
+
it('should mark each question with isInManageListSection', () => {
|
|
15
|
+
const mls = new ManageListSection();
|
|
16
|
+
const mockSetter = mock.fn();
|
|
17
|
+
const q = {
|
|
18
|
+
set isInManageListSection(value) {
|
|
19
|
+
mockSetter(value);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
mls.addQuestion(q);
|
|
23
|
+
assert.strictEqual(mockSetter.mock.callCount(), 1);
|
|
24
|
+
assert.strictEqual(mockSetter.mock.calls[0].arguments[0], true);
|
|
25
|
+
});
|
|
26
|
+
it('should not allow nested manage list questions', () => {
|
|
27
|
+
const mls = new ManageListSection();
|
|
28
|
+
const q = {
|
|
29
|
+
get isManageListQuestion() {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
assert.throws(
|
|
34
|
+
() => mls.addQuestion(q),
|
|
35
|
+
(thrown) => {
|
|
36
|
+
assert.strictEqual(thrown.message, 'manage list sections do not support nested manage list questions');
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
it('should not allow passing nested manageListSection', () => {
|
|
42
|
+
const mls = new ManageListSection();
|
|
43
|
+
const q = {};
|
|
44
|
+
assert.throws(
|
|
45
|
+
() => mls.addQuestion(q, {}),
|
|
46
|
+
(thrown) => {
|
|
47
|
+
assert.strictEqual(thrown.message, 'manage list sections do not support nested manage list questions');
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|