@dwp/govuk-casa 8.7.12 → 8.9.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 (173) hide show
  1. package/dist/casa.d.ts +12 -1
  2. package/dist/casa.js +10 -2
  3. package/dist/casa.js.map +1 -0
  4. package/dist/lib/CasaTemplateLoader.js +1 -0
  5. package/dist/lib/CasaTemplateLoader.js.map +1 -0
  6. package/dist/lib/JourneyContext.d.ts +12 -3
  7. package/dist/lib/JourneyContext.js +20 -5
  8. package/dist/lib/JourneyContext.js.map +1 -0
  9. package/dist/lib/MutableRouter.js +1 -0
  10. package/dist/lib/MutableRouter.js.map +1 -0
  11. package/dist/lib/Plan.d.ts +1 -1
  12. package/dist/lib/Plan.js +2 -5
  13. package/dist/lib/Plan.js.map +1 -0
  14. package/dist/lib/ValidationError.js +1 -0
  15. package/dist/lib/ValidationError.js.map +1 -0
  16. package/dist/lib/ValidatorFactory.d.ts +2 -2
  17. package/dist/lib/ValidatorFactory.js +3 -2
  18. package/dist/lib/ValidatorFactory.js.map +1 -0
  19. package/dist/lib/configuration-ingestor.js +1 -0
  20. package/dist/lib/configuration-ingestor.js.map +1 -0
  21. package/dist/lib/configure.js +2 -1
  22. package/dist/lib/configure.js.map +1 -0
  23. package/dist/lib/constants.d.ts +9 -0
  24. package/dist/lib/constants.js +13 -0
  25. package/dist/lib/constants.js.map +1 -0
  26. package/dist/lib/end-session.js +1 -0
  27. package/dist/lib/end-session.js.map +1 -0
  28. package/dist/lib/field.js +1 -0
  29. package/dist/lib/field.js.map +1 -0
  30. package/dist/lib/index.js +1 -0
  31. package/dist/lib/index.js.map +1 -0
  32. package/dist/lib/logger.js +1 -0
  33. package/dist/lib/logger.js.map +1 -0
  34. package/dist/lib/mount.js +3 -2
  35. package/dist/lib/mount.js.map +1 -0
  36. package/dist/lib/nunjucks-filters.js +1 -0
  37. package/dist/lib/nunjucks-filters.js.map +1 -0
  38. package/dist/lib/nunjucks.js +1 -0
  39. package/dist/lib/nunjucks.js.map +1 -0
  40. package/dist/lib/utils.d.ts +45 -27
  41. package/dist/lib/utils.js +105 -67
  42. package/dist/lib/utils.js.map +1 -0
  43. package/dist/lib/validators/dateObject.js +4 -3
  44. package/dist/lib/validators/dateObject.js.map +1 -0
  45. package/dist/lib/validators/email.js +1 -0
  46. package/dist/lib/validators/email.js.map +1 -0
  47. package/dist/lib/validators/inArray.js +1 -0
  48. package/dist/lib/validators/inArray.js.map +1 -0
  49. package/dist/lib/validators/index.js +1 -0
  50. package/dist/lib/validators/index.js.map +1 -0
  51. package/dist/lib/validators/nino.js +1 -0
  52. package/dist/lib/validators/nino.js.map +1 -0
  53. package/dist/lib/validators/postalAddressObject.d.ts +2 -2
  54. package/dist/lib/validators/postalAddressObject.js +2 -1
  55. package/dist/lib/validators/postalAddressObject.js.map +1 -0
  56. package/dist/lib/validators/regex.js +1 -0
  57. package/dist/lib/validators/regex.js.map +1 -0
  58. package/dist/lib/validators/required.js +1 -0
  59. package/dist/lib/validators/required.js.map +1 -0
  60. package/dist/lib/validators/strlen.js +1 -0
  61. package/dist/lib/validators/strlen.js.map +1 -0
  62. package/dist/lib/validators/wordCount.js +1 -0
  63. package/dist/lib/validators/wordCount.js.map +1 -0
  64. package/dist/lib/waypoint-url.js +40 -9
  65. package/dist/lib/waypoint-url.js.map +1 -0
  66. package/dist/middleware/body-parser.js +1 -0
  67. package/dist/middleware/body-parser.js.map +1 -0
  68. package/dist/middleware/csrf.js +1 -0
  69. package/dist/middleware/csrf.js.map +1 -0
  70. package/dist/middleware/data.js +1 -0
  71. package/dist/middleware/data.js.map +1 -0
  72. package/dist/middleware/gather-fields.js +10 -2
  73. package/dist/middleware/gather-fields.js.map +1 -0
  74. package/dist/middleware/i18n.js +1 -0
  75. package/dist/middleware/i18n.js.map +1 -0
  76. package/dist/middleware/post.js +1 -0
  77. package/dist/middleware/post.js.map +1 -0
  78. package/dist/middleware/pre.js +1 -0
  79. package/dist/middleware/pre.js.map +1 -0
  80. package/dist/middleware/progress-journey.js +7 -2
  81. package/dist/middleware/progress-journey.js.map +1 -0
  82. package/dist/middleware/sanitise-fields.js +1 -0
  83. package/dist/middleware/sanitise-fields.js.map +1 -0
  84. package/dist/middleware/serve-first-waypoint.js +1 -0
  85. package/dist/middleware/serve-first-waypoint.js.map +1 -0
  86. package/dist/middleware/session.js +1 -0
  87. package/dist/middleware/session.js.map +1 -0
  88. package/dist/middleware/skip-waypoint.js +1 -0
  89. package/dist/middleware/skip-waypoint.js.map +1 -0
  90. package/dist/middleware/steer-journey.js +2 -0
  91. package/dist/middleware/steer-journey.js.map +1 -0
  92. package/dist/middleware/strip-proxy-path.js +1 -0
  93. package/dist/middleware/strip-proxy-path.js.map +1 -0
  94. package/dist/middleware/validate-fields.js +7 -1
  95. package/dist/middleware/validate-fields.js.map +1 -0
  96. package/dist/mjs/esm-wrapper.js +11 -15
  97. package/dist/routes/ancillary.js +1 -0
  98. package/dist/routes/ancillary.js.map +1 -0
  99. package/dist/routes/journey.js +1 -0
  100. package/dist/routes/journey.js.map +1 -0
  101. package/dist/routes/static.js +1 -0
  102. package/dist/routes/static.js.map +1 -0
  103. package/locales/cy/error.json +1 -1
  104. package/locales/en/error.json +1 -1
  105. package/package.json +17 -16
  106. package/src/casa.js +330 -0
  107. package/src/lib/CasaTemplateLoader.js +104 -0
  108. package/src/lib/JourneyContext.js +797 -0
  109. package/src/lib/MutableRouter.js +310 -0
  110. package/src/lib/Plan.js +619 -0
  111. package/src/lib/ValidationError.js +163 -0
  112. package/src/lib/ValidatorFactory.js +105 -0
  113. package/src/lib/configuration-ingestor.js +457 -0
  114. package/src/lib/configure.js +202 -0
  115. package/src/lib/constants.js +9 -0
  116. package/src/lib/dirname.cjs +1 -0
  117. package/src/lib/end-session.js +45 -0
  118. package/src/lib/field.js +456 -0
  119. package/src/lib/index.js +33 -0
  120. package/src/lib/logger.js +16 -0
  121. package/src/lib/mount.js +127 -0
  122. package/src/lib/nunjucks-filters.js +150 -0
  123. package/src/lib/nunjucks.js +53 -0
  124. package/src/lib/utils.js +232 -0
  125. package/src/lib/validators/dateObject.js +169 -0
  126. package/src/lib/validators/email.js +55 -0
  127. package/src/lib/validators/inArray.js +81 -0
  128. package/src/lib/validators/index.js +24 -0
  129. package/src/lib/validators/nino.js +57 -0
  130. package/src/lib/validators/postalAddressObject.js +162 -0
  131. package/src/lib/validators/regex.js +48 -0
  132. package/src/lib/validators/required.js +74 -0
  133. package/src/lib/validators/strlen.js +66 -0
  134. package/src/lib/validators/wordCount.js +70 -0
  135. package/src/lib/waypoint-url.js +126 -0
  136. package/src/middleware/body-parser.js +31 -0
  137. package/src/middleware/csrf.js +29 -0
  138. package/src/middleware/data.js +105 -0
  139. package/src/middleware/dirname.cjs +1 -0
  140. package/src/middleware/gather-fields.js +58 -0
  141. package/src/middleware/i18n.js +106 -0
  142. package/src/middleware/post.js +61 -0
  143. package/src/middleware/pre.js +91 -0
  144. package/src/middleware/progress-journey.js +96 -0
  145. package/src/middleware/sanitise-fields.js +58 -0
  146. package/src/middleware/serve-first-waypoint.js +28 -0
  147. package/src/middleware/session.js +129 -0
  148. package/src/middleware/skip-waypoint.js +46 -0
  149. package/src/middleware/steer-journey.js +79 -0
  150. package/src/middleware/strip-proxy-path.js +56 -0
  151. package/src/middleware/validate-fields.js +89 -0
  152. package/src/routes/ancillary.js +29 -0
  153. package/src/routes/dirname.cjs +1 -0
  154. package/src/routes/journey.js +212 -0
  155. package/src/routes/static.js +77 -0
  156. package/views/casa/components/character-count/README.md +10 -0
  157. package/views/casa/components/character-count/template.njk +6 -2
  158. package/views/casa/components/checkboxes/README.md +43 -34
  159. package/views/casa/components/checkboxes/template.njk +8 -7
  160. package/views/casa/components/date-input/README.md +11 -1
  161. package/views/casa/components/date-input/template.njk +6 -4
  162. package/views/casa/components/input/README.md +9 -0
  163. package/views/casa/components/input/template.njk +6 -2
  164. package/views/casa/components/postal-address-object/README.md +10 -0
  165. package/views/casa/components/postal-address-object/template.njk +20 -5
  166. package/views/casa/components/radios/README.md +49 -24
  167. package/views/casa/components/radios/template.njk +6 -3
  168. package/views/casa/components/select/README.md +65 -0
  169. package/views/casa/components/select/macro.njk +3 -0
  170. package/views/casa/components/select/template.njk +49 -0
  171. package/views/casa/components/textarea/README.md +9 -0
  172. package/views/casa/components/textarea/template.njk +6 -2
  173. package/views/casa/layouts/journey.njk +1 -1
@@ -0,0 +1,212 @@
1
+ /* eslint-disable object-curly-newline,max-len */
2
+ import MutableRouter from '../lib/MutableRouter.js';
3
+ import skipWaypointMiddlewareFactory from '../middleware/skip-waypoint.js';
4
+ import steerJourneyMiddlewareFactory from '../middleware/steer-journey.js';
5
+ import sanitiseFieldsMiddlewareFactory from '../middleware/sanitise-fields.js';
6
+ import gatherFieldsMiddlewareFactory from '../middleware/gather-fields.js';
7
+ import validateFieldsMiddlewareFactory from '../middleware/validate-fields.js';
8
+ import progressJourneyMiddlewareFactory from '../middleware/progress-journey.js';
9
+ import waypointUrl from '../lib/waypoint-url.js';
10
+ import logger from '../lib/logger.js';
11
+ import { resolveMiddlewareHooks } from '../lib/utils.js';
12
+
13
+ const log = logger('routes:journey');
14
+
15
+ /**
16
+ * @access private
17
+ * @param {import('../casa.js').GlobalHook} GlobalHook
18
+ */
19
+
20
+ /**
21
+ * @access private
22
+ * @param {import('../casa.js').Page} Page
23
+ */
24
+
25
+ /**
26
+ * @access private
27
+ * @param {import('../casa.js').Plan} Plan
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} JourneyRouterOptions Options to configure static router
32
+ * @property {GlobalHook[]} globalHooks Global hooks
33
+ * @property {Page[]} pages Page definitions
34
+ * @property {Plan} plan Plan
35
+ * @property {Function[]} csrfMiddleware Middleware for providing CSRF controls
36
+ */
37
+
38
+ const renderMiddlewareFactory = (view, contextFactory) => [
39
+ (req, res, next) => {
40
+ res.render(view, {
41
+ // Common template variables for both GET and POST requests
42
+ inEditMode: req.casa.editMode,
43
+ editOriginUrl: req.casa.editOrigin,
44
+ activeContextId: req.casa.journeyContext.identity.id,
45
+ ...contextFactory(req),
46
+ }, (err, templateString) => {
47
+ if (err) {
48
+ next(err);
49
+ } else {
50
+ res.send(templateString);
51
+ }
52
+ });
53
+ },
54
+ ];
55
+
56
+ /**
57
+ * Create an instance of the router for all waypoints visited during a Journey
58
+ * through the Plan.
59
+ *
60
+ * @access private
61
+ * @param {JourneyRouterOptions} opts Options
62
+ * @returns {MutableRouter} Router
63
+ */
64
+ export default function journeyRouter({
65
+ globalHooks,
66
+ pages,
67
+ plan,
68
+ csrfMiddleware,
69
+ }) {
70
+ // Router
71
+ const router = new MutableRouter();
72
+
73
+ // Special "_" route which handles redirecting the user between sub-apps
74
+ // /app1/_/?refmount=app2&route=prev
75
+ router.all('/_', (req, res) => {
76
+ const mountUrl = `${req.baseUrl}/`;
77
+ const refmount = req.query?.refmount;
78
+ const route = req.query?.route;
79
+ log.trace(`App root ${mountUrl}: refmount = ${refmount}, route = ${route}`);
80
+
81
+ let redirectTo;
82
+ const fallback = waypointUrl({
83
+ mountUrl,
84
+ waypoint: plan.traverse(req.casa.journeyContext, {
85
+ stopCondition: () => (true), // we only need one; stop at the first
86
+ })[0],
87
+ });
88
+
89
+ if (route === 'prev') {
90
+ const routes = plan.traversePrevRoutes(req.casa.journeyContext, { startWaypoint: refmount });
91
+ redirectTo = routes.length ? waypointUrl({ mountUrl, waypoint: routes[0].target }) : fallback;
92
+ } else {
93
+ const routes = plan.traverseNextRoutes(req.casa.journeyContext, { startWaypoint: refmount });
94
+ if (routes[0].target !== null) {
95
+ redirectTo = routes.length ? waypointUrl({ mountUrl, waypoint: routes[0].target }) : fallback;
96
+ } else {
97
+ redirectTo = fallback;
98
+ }
99
+ }
100
+
101
+ // Carry over any params
102
+ const url = new URL(redirectTo, 'https://placeholder.test/');
103
+ const searchParams = new URLSearchParams(req.query);
104
+ searchParams.delete('refmount');
105
+ searchParams.delete('route');
106
+ url.search = searchParams.toString();
107
+ redirectTo = `${url.pathname.replace(/\/+/g, '/')}${url.search}`;
108
+
109
+ log.trace(`Redirect to ${redirectTo}`);
110
+ return res.redirect(redirectTo);
111
+ });
112
+
113
+ // Create GET / POST routes for each page
114
+ const commonMiddleware = [
115
+ ...csrfMiddleware,
116
+ ];
117
+
118
+ pages.forEach((page) => {
119
+ const { waypoint, view, hooks: pageHooks = [], fields } = page;
120
+ const waypointPath = `/${waypoint}`;
121
+
122
+ let commonWaypointMiddleware = [
123
+ (req, res, next) => {
124
+ req.casa.waypoint = waypoint;
125
+ res.locals.casa.waypoint = waypoint;
126
+ next();
127
+ },
128
+ ];
129
+
130
+ if (plan.isSkippable(waypoint)) {
131
+ log.info(`Configuring "${waypoint}" as a skippable waypoint`);
132
+ commonWaypointMiddleware = [
133
+ ...commonWaypointMiddleware,
134
+ ...skipWaypointMiddlewareFactory({ waypoint }),
135
+ ];
136
+ }
137
+
138
+ router.get(
139
+ waypointPath,
140
+ ...commonMiddleware,
141
+ ...commonWaypointMiddleware,
142
+
143
+ ...resolveMiddlewareHooks('journey.presteer', waypointPath, [...globalHooks, ...pageHooks]),
144
+ ...steerJourneyMiddlewareFactory({ waypoint, plan }),
145
+ ...resolveMiddlewareHooks('journey.poststeer', waypointPath, [...globalHooks, ...pageHooks]),
146
+
147
+ ...resolveMiddlewareHooks('journey.prerender', waypointPath, [...globalHooks, ...pageHooks]),
148
+ renderMiddlewareFactory(view, (req) => ({
149
+ formUrl: waypointUrl({ mountUrl: `${req.baseUrl}/`, waypoint }),
150
+ formData: req.casa.journeyContext.getDataForPage(waypoint),
151
+ })),
152
+ );
153
+
154
+ router.post(
155
+ waypointPath,
156
+ ...commonMiddleware,
157
+ ...commonWaypointMiddleware,
158
+
159
+ ...resolveMiddlewareHooks('journey.presteer', waypointPath, [...globalHooks, ...pageHooks]),
160
+ ...steerJourneyMiddlewareFactory({ waypoint, plan }),
161
+ ...resolveMiddlewareHooks('journey.poststeer', waypointPath, [...globalHooks, ...pageHooks]),
162
+
163
+ ...resolveMiddlewareHooks('journey.presanitise', waypointPath, [...globalHooks, ...pageHooks]),
164
+ ...sanitiseFieldsMiddlewareFactory({ waypoint, fields }),
165
+ ...resolveMiddlewareHooks('journey.postsanitise', waypointPath, [...globalHooks, ...pageHooks]),
166
+
167
+ ...resolveMiddlewareHooks('journey.pregather', waypointPath, [...globalHooks, ...pageHooks]),
168
+ ...gatherFieldsMiddlewareFactory({ waypoint, fields }),
169
+ ...resolveMiddlewareHooks('journey.postgather', waypointPath, [...globalHooks, ...pageHooks]),
170
+
171
+ ...resolveMiddlewareHooks('journey.prevalidate', waypointPath, [...globalHooks, ...pageHooks]),
172
+ ...validateFieldsMiddlewareFactory({ waypoint, fields, plan }),
173
+ ...resolveMiddlewareHooks('journey.postvalidate', waypointPath, [...globalHooks, ...pageHooks]),
174
+
175
+ // If there were validation errors, jump out of this route and into the
176
+ // next, where the errors will be rendered
177
+ (req, res, next) => (req.casa.journeyContext.hasValidationErrorsForPage(waypoint) ? next('route') : next()),
178
+
179
+ ...resolveMiddlewareHooks('journey.preredirect', waypointPath, [...globalHooks, ...pageHooks]),
180
+ ...progressJourneyMiddlewareFactory({ waypoint, plan }),
181
+ );
182
+
183
+ router.post(
184
+ waypointPath,
185
+ ...resolveMiddlewareHooks('journey.prerender', waypointPath, [...globalHooks, ...pageHooks]),
186
+ renderMiddlewareFactory(view, (req) => {
187
+ const errors = req.casa.journeyContext.getValidationErrorsForPageByField(waypoint) ?? Object.create(null);
188
+
189
+ // This is a convenience for the template. The `govukErrorSummary` macro
190
+ // requires the errors be in a particular format, so here we provide our
191
+ // errors in that format.
192
+ // Where there are multiple errors against a particular field, only the
193
+ // first one is shown.
194
+ // Disabling security/detect-object-injection rule because both `errors`
195
+ // and the `k` property are known entities
196
+ const govukErrors = Object.keys(errors).map((k) => ({
197
+ text: req.t(errors[k][0].summary, errors[k][0].variables), /* eslint-disable-line security/detect-object-injection */
198
+ href: errors[k][0].fieldHref, /* eslint-disable-line security/detect-object-injection */
199
+ }));
200
+
201
+ return {
202
+ formUrl: waypointUrl({ mountUrl: `${req.baseUrl}/`, waypoint }),
203
+ formData: req.body,
204
+ formErrors: Object.keys(errors).length ? errors : null,
205
+ formErrorsGovukArray: govukErrors.length ? govukErrors : null,
206
+ };
207
+ }),
208
+ );
209
+ });
210
+
211
+ return router;
212
+ }
@@ -0,0 +1,77 @@
1
+ import ExpressJS from 'express';
2
+ import { readFileSync } from 'fs';
3
+ import { URL } from 'url';
4
+ import { resolve } from 'path';
5
+ import { createRequire } from 'module';
6
+
7
+ import dirname from './dirname.cjs';
8
+ import MutableRouter from '../lib/MutableRouter.js';
9
+ import { validateUrlPath } from '../lib/utils.js';
10
+
11
+ const { static: ExpressStatic } = ExpressJS; // CommonJS
12
+
13
+ const oneDay = 86400000;
14
+
15
+ /**
16
+ * @typedef {object} StaticOptions Options to configure static router
17
+ * @property {number} [maxAge=3600000] Cache TTL for all assets (optional, default 1 hour)
18
+ */
19
+
20
+ /**
21
+ * Create a router for serving CASA's static assets.
22
+ *
23
+ * @access private
24
+ * @param {StaticOptions} options Options
25
+ * @returns {MutableRouter} ExpressJS Router instance
26
+ */
27
+ export default function staticRouter({
28
+ maxAge = 3600000,
29
+ } = {}) {
30
+ const router = new MutableRouter();
31
+
32
+ const notFoundHandler = (req, res, next) => {
33
+ // Fall through to a general purpose error handler
34
+ next(new Error('404'));
35
+ };
36
+
37
+ const setHeaders = (req, res, next) => {
38
+ res.set('cache-control', 'public');
39
+ res.set('pragma', 'cache');
40
+ res.set('expires', new Date(Date.now() + oneDay).toUTCString());
41
+ const { pathname } = new URL(req?.originalUrl ?? '', 'https://placeholder.test/');
42
+ if (pathname.substr(-4) === '.css') {
43
+ // Just needed for our in-memory CSS assets
44
+ res.set('content-type', 'text/css');
45
+ }
46
+ next();
47
+ };
48
+
49
+ const staticConfig = {
50
+ etag: true,
51
+ lastModified: false,
52
+ maxAge,
53
+ setHeaders: (res) => {
54
+ setHeaders(null, res, () => {});
55
+ },
56
+ };
57
+
58
+ // The CASA CSS source contains the placeholder `~~~CASA_MOUNT_URL~~~` which
59
+ // must be replaced with the dynamic `mountUrl` to ensure govuk-frontend
60
+ // assets are served from the correct location.
61
+ const casaCss = readFileSync(resolve(dirname, '../../dist/assets/css/casa.css'), { encoding: 'utf8' });
62
+ const casaCssIe8 = readFileSync(resolve(dirname, '../../dist/assets/css/casa-ie8.css'), { encoding: 'utf8' });
63
+
64
+ // The static middleware will only server GET/HEAD requests, so we can mount
65
+ // the middleware using `use()` rather than resorting to `get()`
66
+ const govukFrontendDirectory = resolve(createRequire(dirname).resolve('govuk-frontend'), '../../');
67
+
68
+ router.use('/govuk/assets/js/all.js', ExpressStatic(`${govukFrontendDirectory}/govuk/all.js`, staticConfig));
69
+ router.use('/govuk/assets', ExpressStatic(`${govukFrontendDirectory}/govuk/assets`, staticConfig));
70
+ router.use('/govuk/assets', notFoundHandler);
71
+
72
+ router.get('/casa/assets/css/casa.css', setHeaders, (req, res) => res.send(casaCss.replace(/~~~CASA_MOUNT_URL~~~/g, validateUrlPath(`${req.baseUrl}/`))));
73
+ router.get('/casa/assets/css/casa-ie8.css', setHeaders, (req, res) => res.send(casaCssIe8.replace(/~~~CASA_MOUNT_URL~~~/g, validateUrlPath(`${req.baseUrl}/`))));
74
+ router.use('/casa/assets', notFoundHandler);
75
+
76
+ return router;
77
+ }
@@ -19,3 +19,13 @@ casaGovukCharacterCount({
19
19
  casaErrors: formErrors
20
20
  })
21
21
  ```
22
+
23
+ ## Google Tag Manager
24
+
25
+ The following attributes will be attached to the error `<p>` tag if `casaWithAnalytics` is `true`:
26
+
27
+ * `data-ga-question`: Holds the fieldset label's text content
28
+
29
+ These are the conventions used by DWP.
30
+
31
+ > **IMPORTANT:** DO NOT ENABLE this option if the question or answer may contain personally-identifiable information as values will be pushed to Google
@@ -1,4 +1,4 @@
1
- {% from "govuk/components/character-count/macro.njk" import govukCharacterCount %}
1
+ {% from 'govuk/components/character-count/macro.njk' import govukCharacterCount %}
2
2
 
3
3
  {% set fieldErrors = params.casaErrors[params.name] %}
4
4
 
@@ -10,11 +10,15 @@
10
10
  }) %}
11
11
  {% endif %}
12
12
 
13
+ {# Merge parameters #}
13
14
  {% set mergedParams = mergeObjects(params, {
14
15
  id: params.id if params.id else 'f-' + params.name,
15
16
  attributes: mergedAttributes,
16
17
  errorMessage: {
17
- text: t(params.casaErrors[params.name][0].inline, params.casaErrors[params.name][0].variables)
18
+ text: t(params.casaErrors[params.name][0].inline, params.casaErrors[params.name][0].variables),
19
+ attributes: {
20
+ 'data-ga-question': params.label.text or params.label.html | striptags
21
+ } if params.casaWithAnalytics else {}
18
22
  } if params.casaErrors[params.name].length
19
23
  }) %}
20
24
 
@@ -15,62 +15,67 @@ Custom parameters:
15
15
  Basic example:
16
16
 
17
17
  ```nunjucks
18
- {% from "casa/components/checkboxes/macro.njk" import casaGovukCheckboxes with context %}
18
+ {% from 'casa/components/checkboxes/macro.njk' import casaGovukCheckboxes with context %}
19
19
 
20
20
  casaGovukCheckboxes({
21
- name: "preferences",
21
+ name: 'preferences',
22
22
  casaValue: formData.preferences,
23
23
  casaErrors: formErrors,
24
24
  fieldset: {
25
25
  legend: {
26
- text: "Choose your preferences",
26
+ text: 'Choose your preferences',
27
27
  isPageHeading: true,
28
- classes: "govuk-fieldset__legend--xl"
28
+ classes: 'govuk-fieldset__legend--xl'
29
29
  }
30
30
  },
31
31
  hint: {
32
- text: "Some instructive hints"
32
+ text: 'Some instructive hints'
33
33
  },
34
- items: [{
35
- value: "twistedflax",
36
- text: "Twisted Flax"
37
- }, {
38
- value: "tworeeds",
39
- text: "Two Reeds"
40
- }, {
41
- value: "water",
42
- text: "Water"
43
- }, {
44
- value: "horus",
45
- text: "Horus"
46
- }]
34
+ items: [
35
+ {
36
+ value: 'twoReeds',
37
+ text: 'Two Reeds'
38
+ }, {
39
+ value: 'twistedFlax',
40
+ text: 'Twisted Flax'
41
+ }, {
42
+ value: 'water',
43
+ text: 'Water'
44
+ }, {
45
+ value: 'eyeOfHorus',
46
+ text: 'Eye of Horus'
47
+ }
48
+ ]
47
49
  })
48
50
  ```
49
51
 
50
- To associate a checkbox item with a toggleable DOM element:
52
+ If you want one of the checkbox items to toggle the display of an element:
51
53
 
52
54
  ```nunjucks
53
- {% from "casa/components/checkboxes/macro.njk" import casaGovukCheckboxes with context %}
55
+ {% from 'casa/components/checkboxes/macro.njk' import casaGovukCheckboxes with context %}
54
56
 
55
57
  {% set panel %}
56
- This panel will remain hidden until the "First Choice" option is checked
58
+ This panel will remain hidden until the 'Yes' checkbox is chosen
57
59
  {% endset %}
58
60
 
59
- casaGovukCheckboxes({
60
- name: "preferences",
61
- casaValue: formData.preferences,
61
+ {{ casaGovukCheckboxes({
62
+ name: 'preference',
63
+ casaValue: formData.preference,
62
64
  casaErrors: formErrors,
63
- items: [{
64
- value: "first-choice",
65
- text: "First Choice",
66
- conditional: {
67
- html: panel
65
+ items: [
66
+ {
67
+ value: 'yes',
68
+ text: 'Yes',
69
+ conditional: {
70
+ html: panel
71
+ }
72
+ },
73
+ {
74
+ value: 'no',
75
+ text: 'No'
68
76
  }
69
- }, {
70
- value: "second-choice",
71
- text: "Second Choice"
72
- }]
73
- })
77
+ ]
78
+ }) }}
74
79
  ```
75
80
 
76
81
  ## Displaying errors
@@ -86,6 +91,10 @@ The following attributes will be attached to each `<input>` option if `casaWithA
86
91
  * `data-ga-question`: Holds the fieldset legend's content
87
92
  * `data-ga-answer`: Holds the specific answer's text/html value
88
93
 
94
+ The following attributes will be attached to the error `<p>` tag if `casaWithAnalytics` is `true`:
95
+
96
+ * `data-ga-question`: Holds the fieldset legend's content after removing all html tags
97
+
89
98
  These are the conventions used by DWP.
90
99
 
91
100
  > **IMPORTANT:** DO NOT ENABLE this option if the question or answer may contain personally-identifiable information as values will be pushed to Google
@@ -1,23 +1,23 @@
1
- {% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %}
1
+ {% from 'govuk/components/checkboxes/macro.njk' import govukCheckboxes %}
2
2
 
3
3
  {# Ensure field name is always suffixed with [] to have it parsed consistently as an array #}
4
4
  {% set fieldName = params.name | replace('[]', '') + '[]' %}
5
5
 
6
6
  {% set fieldErrors = params.casaErrors[params.name] %}
7
7
 
8
- {# Build up attributes #}
8
+ {# Generate validation data- attributes #}
9
9
  {% set mergedAttributes = params.attributes or {} %}
10
10
  {% if fieldErrors %}
11
11
  {% set mergedAttributes = mergeObjects(mergedAttributes, {
12
12
  'data-validation': {fn: params.name, va: fieldErrors[0].validator} | dump
13
13
  }) %}
14
14
  {% endif %}
15
+
15
16
  {% set mergedAttributes = mergeObjects(mergedAttributes, {
16
17
  id: params.id if params.id else 'f-' + params.name + '-wrapper' | safe
17
18
  }) %}
18
19
 
19
-
20
- {# Add checked flag to chosen inputs #}
20
+ {# Add checked flag to chosen items #}
21
21
  {% set mergedItems = [] %}
22
22
  {% for item in params.items %}
23
23
  {% set item = mergeObjects({
@@ -31,7 +31,6 @@
31
31
  {% set mergedItems = (mergedItems.push(item), mergedItems) %}
32
32
  {% endfor %}
33
33
 
34
-
35
34
  {# Merge parameters #}
36
35
  {% set mergedParams = mergeObjects(params, {
37
36
  name: fieldName,
@@ -39,7 +38,10 @@
39
38
  attributes: mergedAttributes,
40
39
  items: mergedItems,
41
40
  errorMessage: {
42
- text: t(params.casaErrors[params.name][0].inline, params.casaErrors[params.name][0].variables)
41
+ text: t(params.casaErrors[params.name][0].inline, params.casaErrors[params.name][0].variables),
42
+ attributes: {
43
+ 'data-ga-question': params.fieldset.legend.text or params.fieldset.legend.html | striptags
44
+ } if params.casaWithAnalytics else {}
43
45
  } if params.casaErrors[params.name] else null
44
46
  }) %}
45
47
 
@@ -52,5 +54,4 @@
52
54
  }) %}
53
55
  {% endif %}
54
56
 
55
-
56
57
  {{ govukCheckboxes(mergedParams) }}
@@ -72,7 +72,7 @@ module.exports = {
72
72
  myDateField: sf([
73
73
  r.required.make({
74
74
  errorMsg: {
75
- summary: 'Your error mesasge',
75
+ summary: 'Your error message',
76
76
  focusSuffix: '[dd]'
77
77
  }
78
78
  }),
@@ -102,3 +102,13 @@ You can also pass a locale:
102
102
  ```nunjucks
103
103
  1 Ionawr 1980
104
104
  ```
105
+
106
+ ## Google Tag Manager
107
+
108
+ The following attributes will be attached to the error `<p>` tag if `casaWithAnalytics` is `true`:
109
+
110
+ * `data-ga-question`: Holds the fieldset label's text content after removing all html tags
111
+
112
+ These are the conventions used by DWP.
113
+
114
+ > **IMPORTANT:** DO NOT ENABLE this option if the question or answer may contain personally-identifiable information as values will be pushed to Google
@@ -1,4 +1,4 @@
1
- {% from "govuk/components/date-input/macro.njk" import govukDateInput %}
1
+ {% from 'govuk/components/date-input/macro.njk' import govukDateInput %}
2
2
 
3
3
  {% set fieldErrors = params.casaErrors[params.namePrefix] %}
4
4
  {% set hasSuffixHighlights = true if fieldErrors[0].focusSuffix.length else false %}
@@ -7,7 +7,7 @@
7
7
  {{ 'govuk-input--error' if fieldErrors }}
8
8
  {% endset -%}
9
9
 
10
- {# Build up attributes #}
10
+ {# Generate validation data- attributes #}
11
11
  {% set mergedAttributes = params.attributes or {} %}
12
12
  {% if fieldErrors %}
13
13
  {% set mergedAttributes = mergeObjects(mergedAttributes, {
@@ -47,9 +47,11 @@
47
47
  })
48
48
  ],
49
49
  errorMessage: {
50
- text: t(params.casaErrors[params.namePrefix][0].inline, params.casaErrors[params.namePrefix][0].variables)
50
+ text: t(params.casaErrors[params.namePrefix][0].inline, params.casaErrors[params.namePrefix][0].variables),
51
+ attributes: {
52
+ 'data-ga-question': params.fieldset.legend.text or params.fieldset.legend.html | striptags
53
+ } if params.casaWithAnalytics else {}
51
54
  } if params.casaErrors[params.namePrefix] else null
52
55
  }) %}
53
56
 
54
-
55
57
  {{ govukDateInput(mergedParams) }}
@@ -19,3 +19,12 @@ casaGovukInput({
19
19
  casaErrors: formErrors
20
20
  })
21
21
  ```
22
+ ## Google Tag Manager
23
+
24
+ The following attributes will be attached to the error `<p>` tag if `casaWithAnalytics` is `true`:
25
+
26
+ * `data-ga-question`: Holds the fieldset label's text content after removing all html tags
27
+
28
+ These are the conventions used by DWP.
29
+
30
+ > **IMPORTANT:** DO NOT ENABLE this option if the question or answer may contain personally-identifiable information as values will be pushed to Google
@@ -1,4 +1,4 @@
1
- {% from "govuk/components/input/macro.njk" import govukInput %}
1
+ {% from 'govuk/components/input/macro.njk' import govukInput %}
2
2
 
3
3
  {% set fieldErrors = params.casaErrors[params.name] %}
4
4
 
@@ -10,11 +10,15 @@
10
10
  }) %}
11
11
  {% endif %}
12
12
 
13
+ {# Merge parameters #}
13
14
  {% set mergedParams = mergeObjects(params, {
14
15
  id: params.id if params.id else 'f-' + params.name,
15
16
  attributes: mergedAttributes,
16
17
  errorMessage: {
17
- text: t(params.casaErrors[params.name][0].inline, params.casaErrors[params.name][0].variables)
18
+ text: t(params.casaErrors[params.name][0].inline, params.casaErrors[params.name][0].variables),
19
+ attributes: {
20
+ 'data-ga-question': params.label.text or params.label.html | striptags
21
+ } if params.casaWithAnalytics else {}
18
22
  } if params.casaErrors[params.name].length
19
23
  }) %}
20
24
 
@@ -52,3 +52,13 @@ With configurable items:
52
52
  There is a companion validation rule - [`postalAddressObject`](../../../../../docs/field-validation-rules.md#postalAddressObject) - that you can use to ensure the addresses are passed in correctly. See this rule for more information on how to control error highlighting among other things.
53
53
 
54
54
  And when using the `required` field validator in conjunction with this macro, you will need to configure the validator to indicate which part of the date input you want to focus on when linked from the error summary, using the `focusSuffix` error option:
55
+
56
+ ## Google Tag Manager
57
+
58
+ The following attributes will be attached to the error `<p>` tags if `casaWithAnalytics` is `true`:
59
+
60
+ * `data-ga-question`: Holds the specific item's label's text content after removing all html tags
61
+
62
+ These are the conventions used by DWP.
63
+
64
+ > **IMPORTANT:** DO NOT ENABLE this option if the question or answer may contain personally-identifiable information as values will be pushed to Google
@@ -20,7 +20,10 @@
20
20
  },
21
21
  attributes: fieldAttributes,
22
22
  errorMessage: {
23
- text: t(fieldAddress1Errors[0].inline, fieldAddress1Errors[0].variables)
23
+ text: t(fieldAddress1Errors[0].inline, fieldAddress1Errors[0].variables),
24
+ attributes: {
25
+ 'data-ga-question': t('macros:postalAddressObject.address1') | striptags
26
+ } if params.casaWithAnalytics else {}
24
27
  } if fieldAddress1Errors else null
25
28
  }, params.address1 if params.address1 else {}, {
26
29
  id: 'f-' + params.name + '[address1]',
@@ -39,7 +42,10 @@
39
42
  },
40
43
  attributes: fieldAttributes,
41
44
  errorMessage: {
42
- text: t(fieldAddress2Errors[0].inline, fieldAddress2Errors[0].variables)
45
+ text: t(fieldAddress2Errors[0].inline, fieldAddress2Errors[0].variables),
46
+ attributes: {
47
+ 'data-ga-question': t('macros:postalAddressObject.address2') | striptags
48
+ } if params.casaWithAnalytics else {}
43
49
  } if fieldAddress2Errors else null
44
50
  }, params.address2 if params.address2 else {}, {
45
51
  id: 'f-' + params.name + '[address2]',
@@ -61,7 +67,10 @@
61
67
  },
62
68
  attributes: fieldAttributes,
63
69
  errorMessage: {
64
- text: t(fieldErrors[0].inline, fieldErrors[0].variables)
70
+ text: t(fieldErrors[0].inline, fieldErrors[0].variables),
71
+ attributes: {
72
+ 'data-ga-question': t('macros:postalAddressObject.address3')
73
+ } if params.casaWithAnalytics else {}
65
74
  } if fieldErrors else null
66
75
  }, params.address3 if params.address3 else {}, {
67
76
  id: 'f-' + params.name + '[address3]',
@@ -82,7 +91,10 @@
82
91
  },
83
92
  attributes: fieldAttributes,
84
93
  errorMessage: {
85
- text: t(fieldErrors[0].inline, fieldErrors[0].variables)
94
+ text: t(fieldErrors[0].inline, fieldErrors[0].variables),
95
+ attributes: {
96
+ 'data-ga-question': t('macros:postalAddressObject.address4')
97
+ } if params.casaWithAnalytics else {}
86
98
  } if fieldErrors else null
87
99
  }, params.address4 if params.address4 else {}, {
88
100
  id: 'f-' + params.name + '[address4]',
@@ -103,7 +115,10 @@
103
115
  },
104
116
  attributes: fieldAttributes,
105
117
  errorMessage: {
106
- text: t(fieldErrors[0].inline, fieldErrors[0].variables)
118
+ text: t(fieldErrors[0].inline, fieldErrors[0].variables),
119
+ attributes: {
120
+ 'data-ga-question': t('macros:postalAddressObject.postcode')
121
+ } if params.casaWithAnalytics else {}
107
122
  } if fieldErrors else null
108
123
  }, params.postcode if params.postcode else {}, {
109
124
  id: 'f-' + params.name + '[postcode]',