@planning-inspectorate/dynamic-forms 3.20.5 → 3.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,6 +82,55 @@ The controllers available are:
82
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
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. |
84
84
 
85
+ #### Saving answers
86
+
87
+ The `buildSave` controller accepts a `SaveDataFn` which is passed the context required to save the answer(s). To save answers to session during a journey, a `SaveDataFn` implementation is provided called `saveDataToSession`.
88
+
89
+ For other use cases, write a custom `SaveDataFn`. A basic implementation (if you are not using ManageList) needs to save the answers passed in as `data.answers`, which is an object whose keys are the question field names, and the values are the answers. For example:
90
+
91
+ ```typescript
92
+ const update: SaveDataFn = async ({data}) => {
93
+ const answers = data.answers;
94
+
95
+ console.log('saving', answers);
96
+
97
+ // TODO: map to database
98
+ // TODO: write to database
99
+ }
100
+ ```
101
+
102
+ To support `ManageList` questions as well, handle the `isManageListItem`, `manageListQuestionFieldName`, `manageListItemRemove` parameters. For example:
103
+
104
+ ```typescript
105
+ const update: SaveDataFn = async ({data, req, isManageListItem, manageListQuestionFieldName, manageListItemRemove}) => {
106
+ if (isManageListItem) {
107
+ const update = {
108
+ id: req.params.manageListItemId,
109
+ ...data.answers
110
+ };
111
+
112
+ console.log('saving', update);
113
+ // TODO: map to database
114
+ // TODO: write to database
115
+ return;
116
+ } else if (manageListItemRemove) {
117
+ const id = req.params.manageListItemId;
118
+ // identify which type of item it is via the field name
119
+ const type = manageListQuestionFieldName;
120
+ console.log('removing item', {id, type});
121
+ // TODO: delete from database
122
+ return;
123
+ }
124
+ const answers = data.answers;
125
+
126
+ console.log('saving', answers);
127
+
128
+ // TODO: map to database
129
+ // TODO: write to database
130
+ }
131
+ ```
132
+
133
+
85
134
  ### Conditions
86
135
 
87
136
  Conditions are used to show/hide questions based on the answers to other questions. There are fluent methods on a Section
@@ -78,7 +78,7 @@ Dynamic forms requires two templates to work for rendering questions and renderi
78
78
  ### Journey (or question) template
79
79
 
80
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.
81
+ It should also include the rendering of the errorSummary component, and optionally the back link. The path to this template is configured via the `journeyTemplate` property on a Journey.
82
82
 
83
83
  For example:
84
84
 
@@ -87,7 +87,13 @@ e.g.
87
87
  ```nunjucks
88
88
  {% extends "govuk/template.njk" %}
89
89
 
90
- {% block beforeContent %}
90
+ {% from "govuk/components/back-link/macro.njk" import govukBackLink %}
91
+ {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
92
+
93
+ {% block containerStart %}
94
+ {% if backLink %}
95
+ {{ govukBackLink({ href: backLink }) }}
96
+ {% endif %}
91
97
  {% if errorSummary %}
92
98
  <div class="govuk-grid-row">
93
99
  <div class="govuk-grid-column-two-thirds">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/dynamic-forms",
3
- "version": "3.20.5",
3
+ "version": "3.21.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",
@@ -17,7 +17,7 @@ export class BooleanQuestion extends RadioQuestion {
17
17
  /**
18
18
  * Get the data to save from the request, returns an object of answers
19
19
  * @param {import('express').Request} req
20
- * @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
20
+ * @param {JourneyResponse} journeyResponse - current journey response
21
21
  * @returns {Promise<{ answers: Record<string, unknown> }>}
22
22
  */ getDataToSave(req: import("express").Request, journeyResponse: JourneyResponse): Promise<{
23
23
  answers: Record<string, unknown>;
@@ -97,7 +97,7 @@ export class BooleanQuestion extends RadioQuestion {
97
97
  /**
98
98
  * Get the data to save from the request, returns an object of answers
99
99
  * @param {import('express').Request} req
100
- * @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
100
+ * @param {JourneyResponse} journeyResponse - current journey response
101
101
  * @returns {Promise<{ answers: Record<string, unknown> }>}
102
102
  */ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
103
103
  async getDataToSave(req, journeyResponse) {
@@ -10,7 +10,7 @@ export class DateQuestion extends Question {
10
10
  /**
11
11
  * Get the data to save from the request, returns an object of answers
12
12
  * @param {import('express').Request} req
13
- * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response, modified with the new answers
13
+ * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
14
14
  * @returns {Promise.<Object>}
15
15
  */ getDataToSave(req: import("express").Request, journeyResponse: import("#journey-response").JourneyResponse): Promise<Object>;
16
16
  answerForViewModel(answers: any, isPayload: any): {
@@ -29,7 +29,7 @@ export class DateQuestion extends Question {
29
29
  /**
30
30
  * Get the data to save from the request, returns an object of answers
31
31
  * @param {import('express').Request} req
32
- * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response, modified with the new answers
32
+ * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
33
33
  * @returns {Promise.<Object>}
34
34
  */ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
35
35
  async getDataToSave(req, journeyResponse) {
@@ -19,7 +19,7 @@ export class DatePeriodQuestion extends Question {
19
19
  /**
20
20
  * Get the data to save from the request, returns an object of answers
21
21
  * @param {import('express').Request} req
22
- * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response, modified with the new answers
22
+ * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
23
23
  * @returns {Promise.<Object>}
24
24
  */ getDataToSave(req: import("express").Request, journeyResponse: import("#journey-response").JourneyResponse): Promise<Object>;
25
25
  answerForViewModel(answers: any, isPayload: any): {
@@ -54,7 +54,7 @@ export class DatePeriodQuestion extends Question {
54
54
  /**
55
55
  * Get the data to save from the request, returns an object of answers
56
56
  * @param {import('express').Request} req
57
- * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response, modified with the new answers
57
+ * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
58
58
  * @returns {Promise.<Object>}
59
59
  */ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
60
60
  async getDataToSave(req, journeyResponse) {
@@ -10,7 +10,7 @@ export class DateTimeQuestion extends Question {
10
10
  /**
11
11
  * Get the data to save from the request, returns an object of answers
12
12
  * @param {import('express').Request} req
13
- * @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
13
+ * @param {JourneyResponse} journeyResponse - current journey response
14
14
  * @returns {Promise.<Object>}
15
15
  */ getDataToSave(req: import("express").Request, journeyResponse: JourneyResponse): Promise<Object>;
16
16
  answerForViewModel(answers: any, isPayload: any): {
@@ -38,7 +38,7 @@ export class DateTimeQuestion extends Question {
38
38
  /**
39
39
  * Get the data to save from the request, returns an object of answers
40
40
  * @param {import('express').Request} req
41
- * @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
41
+ * @param {JourneyResponse} journeyResponse - current journey response
42
42
  * @returns {Promise.<Object>}
43
43
  */ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
44
44
  async getDataToSave(req, journeyResponse) {
@@ -44,7 +44,7 @@ export class MultiFieldInputQuestion extends Question {
44
44
  /**
45
45
  * Get the data to save from the request, returns an object of answers
46
46
  * @param {import('express').Request} req
47
- * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response, modified with the new answers
47
+ * @param {import('#journey-response').JourneyResponse} journeyResponse - current journey response
48
48
  * @returns {Promise<{ answers: Record<string, unknown> }>}
49
49
  */ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
50
50
  async getDataToSave(req, journeyResponse) {
@@ -38,6 +38,8 @@
38
38
  text: errors[question.fieldName].msg
39
39
  },
40
40
  classes: question.classes,
41
+ prefix: question.prefix,
42
+ suffix: question.suffix,
41
43
  attributes: question.attributes,
42
44
  type: question.type,
43
45
  autocomplete: question.autocomplete
@@ -62,6 +64,8 @@
62
64
  text: errors[question.fieldName].msg
63
65
  },
64
66
  classes: question.classes,
67
+ prefix: question.prefix,
68
+ suffix: question.suffix,
65
69
  attributes: question.attributes,
66
70
  type: question.type,
67
71
  autocomplete: question.autocomplete
@@ -10,6 +10,8 @@ export class SingleLineInputQuestion extends Question {
10
10
  inputAttributes: Record<string, string>;
11
11
  label: string | undefined;
12
12
  classes: string;
13
+ prefix: import("#typedefs/question-props.d.ts").Affix | undefined;
14
+ suffix: import("#typedefs/question-props.d.ts").Affix | undefined;
13
15
  }
14
16
  export default SingleLineInputQuestion;
15
17
  import { Question } from '#question';
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;GAEG;AACH;IAIC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAW/E;IAfD,qCAAqC;IACrC,iBADW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CACjB;IAWf,0BAAyB;IAEzB,gBAAmC;CAepC;;yBAnCwB,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;GAEG;AACH;IAIC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAa/E;IAjBD,qCAAqC;IACrC,iBADW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CACjB;IAWf,0BAAyB;IAEzB,gBAAmC;IACnC,kEAA2B;IAC3B,kEAA2B;CAiB5B;;yBAvCwB,WAAW"}
@@ -19,6 +19,8 @@ export class SingleLineInputQuestion extends Question {
19
19
  this.label = params.label;
20
20
  this.inputAttributes = params.inputAttributes || {};
21
21
  this.classes = params.classes || '';
22
+ this.prefix = params.prefix;
23
+ this.suffix = params.suffix;
22
24
  }
23
25
 
24
26
  /**
@@ -32,6 +34,8 @@ export class SingleLineInputQuestion extends Question {
32
34
  viewModel.question.type = type;
33
35
 
34
36
  viewModel.question.classes = this.classes;
37
+ viewModel.question.prefix = this.prefix;
38
+ viewModel.question.suffix = this.suffix;
35
39
  }
36
40
  }
37
41
 
@@ -11,6 +11,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
11
11
  const HTML = '/path/to/html.njk';
12
12
  const HINT = 'hint';
13
13
  const LABEL = 'A label';
14
+ const PREFIX = { text: '£', classes: 'govuk-!-font-weight-bold' };
15
+ const SUFFIX = { text: 'per year' };
14
16
 
15
17
  const question = new SingleLineInputQuestion({
16
18
  title: TITLE,
@@ -20,6 +22,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
20
22
  html: HTML,
21
23
  hint: HINT,
22
24
  label: LABEL,
25
+ prefix: PREFIX,
26
+ suffix: SUFFIX,
23
27
  autocomplete: FIELDNAME
24
28
  });
25
29
 
@@ -31,6 +35,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
31
35
  assert.strictEqual(question.html, HTML);
32
36
  assert.strictEqual(question.hint, HINT);
33
37
  assert.strictEqual(question.label, LABEL);
38
+ assert.strictEqual(question.prefix, PREFIX);
39
+ assert.strictEqual(question.suffix, SUFFIX);
34
40
  assert.strictEqual(question.autocomplete, FIELDNAME);
35
41
  });
36
42
  });
@@ -54,7 +54,7 @@ export class UnitOptionEntryQuestion extends Question {
54
54
  /**
55
55
  * Get the data to save from the request, returns an object of answers
56
56
  * @param {import('express').Request} req
57
- * @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
57
+ * @param {JourneyResponse} journeyResponse - current journey response
58
58
  * @returns {Promise<{ answers: Record<string, unknown> }>}
59
59
  */ getDataToSave(req: import("express").Request, journeyResponse: JourneyResponse): Promise<{
60
60
  answers: Record<string, unknown>;
@@ -125,7 +125,7 @@ export class UnitOptionEntryQuestion extends Question {
125
125
  /**
126
126
  * Get the data to save from the request, returns an object of answers
127
127
  * @param {import('express').Request} req
128
- * @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
128
+ * @param {JourneyResponse} journeyResponse - current journey response
129
129
  * @returns {Promise<{ answers: Record<string, unknown> }>}
130
130
  */ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
131
131
  async getDataToSave(req, journeyResponse) {
@@ -147,7 +147,7 @@ export class OptionsQuestion extends Question {
147
147
  /**
148
148
  * Get the data to save from the request, returns an object of answers
149
149
  * @param {import('express').Request} req
150
- * @param {import('../journey/journey-response.js').JourneyResponse} journeyResponse - current journey response, modified with the new answers
150
+ * @param {import('../journey/journey-response.js').JourneyResponse} journeyResponse - current journey response
151
151
  * @returns {Promise<{ answers: Record<string, unknown> }>}
152
152
  */ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
153
153
  async getDataToSave(req, journeyResponse) {
@@ -68,7 +68,7 @@ export type SelectableOption = {
68
68
  attributes?: Record<string, string>;
69
69
  behaviour?: 'exclusive';
70
70
  conditional?: {
71
- question: string;
71
+ question?: string;
72
72
  type: string;
73
73
  fieldName: string;
74
74
  inputClasses?: string;
@@ -261,6 +261,8 @@ export type SingleLineInputQuestionParams = CommonQuestionParams & {
261
261
  inputAttributes?: Record<string, string>; // HTML attributes to add to the input
262
262
  label?: string; // if defined this will show as a label for the input and the question will just be a standard h1
263
263
  classes?: string; // HTML classes to add to the input
264
+ prefix?: Affix;
265
+ suffix?: Affix;
264
266
  };
265
267
 
266
268
  type SingleLineInputQuestionProps = SingleLineInputQuestionParams & {