@defra/forms-engine-plugin 0.1.3 → 0.1.5
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/.server/server/plugins/engine/components/UkAddressField.js +12 -0
- package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
- package/README.md +11 -306
- package/package.json +2 -1
- package/src/config/index.ts +13 -13
- package/src/server/plugins/engine/components/UkAddressField.test.ts +80 -2
- package/src/server/plugins/engine/components/UkAddressField.ts +12 -0
- package/src/server/plugins/engine/views/layout.html +1 -1
|
@@ -49,6 +49,18 @@ export class UkAddressField extends FormComponent {
|
|
|
49
49
|
required: isRequired,
|
|
50
50
|
optionalText: !isRequired && (hideOptional || !hideTitle)
|
|
51
51
|
}
|
|
52
|
+
}, {
|
|
53
|
+
type: ComponentType.TextField,
|
|
54
|
+
name: `${name}__county`,
|
|
55
|
+
title: 'County',
|
|
56
|
+
schema: {
|
|
57
|
+
max: 100
|
|
58
|
+
},
|
|
59
|
+
options: {
|
|
60
|
+
autocomplete: 'county',
|
|
61
|
+
required: false,
|
|
62
|
+
optionalText: !isRequired && (hideOptional || !hideTitle)
|
|
63
|
+
}
|
|
52
64
|
}, {
|
|
53
65
|
type: ComponentType.TextField,
|
|
54
66
|
name: `${name}__postcode`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UkAddressField.js","names":["ComponentType","ComponentCollection","FormComponent","isFormState","TextField","UkAddressField","constructor","def","props","name","options","isRequired","required","hideOptional","optionalText","hideTitle","collection","type","title","schema","max","autocomplete","classes","regex","parent","formSchema","stateSchema","getFormValueFromState","state","value","isState","undefined","getDisplayStringFromState","getContextValueFromState","join","Object","values","filter","Boolean","getViewModel","payload","errors","viewModel","components","fieldset","hint","label","legend","text","id","attributes","isUkAddress","isText","addressLine1","town","postcode"],"sources":["../../../../../src/server/plugins/engine/components/UkAddressField.ts"],"sourcesContent":["import { ComponentType, type UkAddressFieldComponent } from '@defra/forms-model'\nimport { type ObjectSchema } from 'joi'\n\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport {\n FormComponent,\n isFormState\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { TextField } from '~/src/server/plugins/engine/components/TextField.js'\nimport { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nexport class UkAddressField extends FormComponent {\n declare options: UkAddressFieldComponent['options']\n declare formSchema: ObjectSchema<FormPayload>\n declare stateSchema: ObjectSchema<FormState>\n declare collection: ComponentCollection\n\n constructor(\n def: UkAddressFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n const { name, options } = def\n\n const isRequired = options.required !== false\n const hideOptional = !!options.optionalText\n const hideTitle = !!options.hideTitle\n\n this.collection = new ComponentCollection(\n [\n {\n type: ComponentType.TextField,\n name: `${name}__addressLine1`,\n title: 'Address line 1',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-line1',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__addressLine2`,\n title: 'Address line 2',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-line2',\n required: false,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__town`,\n title: 'Town or city',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-level2',\n classes: 'govuk-!-width-two-thirds',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__postcode`,\n title: 'Postcode',\n schema: {\n regex: '^[a-zA-Z]{1,2}\\\\d[a-zA-Z\\\\d]?\\\\s?\\\\d[a-zA-Z]{2}$'\n },\n options: {\n autocomplete: 'postal-code',\n classes: 'govuk-input--width-10',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n }\n ],\n { ...props, parent: this }\n )\n\n this.options = options\n this.formSchema = this.collection.formSchema\n this.stateSchema = this.collection.stateSchema\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const value = super.getFormValueFromState(state)\n return this.isState(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState) {\n return this.getContextValueFromState(state)?.join(', ') ?? ''\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n const value = this.getFormValueFromState(state)\n\n if (!value) {\n return null\n }\n\n return Object.values(value).filter(Boolean)\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const { collection, name, options } = this\n\n const viewModel = super.getViewModel(payload, errors)\n let { components, fieldset, hint, label } = viewModel\n\n fieldset ??= {\n legend: {\n text: label.text,\n\n /**\n * For screen readers, only hide legend visually. This can be overridden\n * by single component {@link QuestionPageController | `showTitle` handling}\n */\n classes: options.hideTitle\n ? 'govuk-visually-hidden'\n : 'govuk-fieldset__legend--m'\n }\n }\n\n if (hint) {\n hint.id ??= `${name}-hint`\n fieldset.attributes ??= {\n 'aria-describedby': hint.id\n }\n }\n\n components = collection.getViewModel(payload, errors)\n\n return {\n ...viewModel,\n fieldset,\n components\n }\n }\n\n isState(value?: FormStateValue | FormState): value is UkAddressState {\n return UkAddressField.isUkAddress(value)\n }\n\n static isUkAddress(\n value?: FormStateValue | FormState\n ): value is UkAddressState {\n return (\n isFormState(value) &&\n TextField.isText(value.addressLine1) &&\n TextField.isText(value.town) &&\n TextField.isText(value.postcode)\n )\n }\n}\n\nexport interface UkAddressState extends Record<string, string> {\n addressLine1: string\n addressLine2: string\n town: string\n postcode: string\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"UkAddressField.js","names":["ComponentType","ComponentCollection","FormComponent","isFormState","TextField","UkAddressField","constructor","def","props","name","options","isRequired","required","hideOptional","optionalText","hideTitle","collection","type","title","schema","max","autocomplete","classes","regex","parent","formSchema","stateSchema","getFormValueFromState","state","value","isState","undefined","getDisplayStringFromState","getContextValueFromState","join","Object","values","filter","Boolean","getViewModel","payload","errors","viewModel","components","fieldset","hint","label","legend","text","id","attributes","isUkAddress","isText","addressLine1","town","postcode"],"sources":["../../../../../src/server/plugins/engine/components/UkAddressField.ts"],"sourcesContent":["import { ComponentType, type UkAddressFieldComponent } from '@defra/forms-model'\nimport { type ObjectSchema } from 'joi'\n\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport {\n FormComponent,\n isFormState\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { TextField } from '~/src/server/plugins/engine/components/TextField.js'\nimport { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nexport class UkAddressField extends FormComponent {\n declare options: UkAddressFieldComponent['options']\n declare formSchema: ObjectSchema<FormPayload>\n declare stateSchema: ObjectSchema<FormState>\n declare collection: ComponentCollection\n\n constructor(\n def: UkAddressFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n const { name, options } = def\n\n const isRequired = options.required !== false\n const hideOptional = !!options.optionalText\n const hideTitle = !!options.hideTitle\n\n this.collection = new ComponentCollection(\n [\n {\n type: ComponentType.TextField,\n name: `${name}__addressLine1`,\n title: 'Address line 1',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-line1',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__addressLine2`,\n title: 'Address line 2',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-line2',\n required: false,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__town`,\n title: 'Town or city',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-level2',\n classes: 'govuk-!-width-two-thirds',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__county`,\n title: 'County',\n schema: { max: 100 },\n options: {\n autocomplete: 'county',\n required: false,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__postcode`,\n title: 'Postcode',\n schema: {\n regex: '^[a-zA-Z]{1,2}\\\\d[a-zA-Z\\\\d]?\\\\s?\\\\d[a-zA-Z]{2}$'\n },\n options: {\n autocomplete: 'postal-code',\n classes: 'govuk-input--width-10',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n }\n ],\n { ...props, parent: this }\n )\n\n this.options = options\n this.formSchema = this.collection.formSchema\n this.stateSchema = this.collection.stateSchema\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const value = super.getFormValueFromState(state)\n return this.isState(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState) {\n return this.getContextValueFromState(state)?.join(', ') ?? ''\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n const value = this.getFormValueFromState(state)\n\n if (!value) {\n return null\n }\n\n return Object.values(value).filter(Boolean)\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const { collection, name, options } = this\n\n const viewModel = super.getViewModel(payload, errors)\n let { components, fieldset, hint, label } = viewModel\n\n fieldset ??= {\n legend: {\n text: label.text,\n\n /**\n * For screen readers, only hide legend visually. This can be overridden\n * by single component {@link QuestionPageController | `showTitle` handling}\n */\n classes: options.hideTitle\n ? 'govuk-visually-hidden'\n : 'govuk-fieldset__legend--m'\n }\n }\n\n if (hint) {\n hint.id ??= `${name}-hint`\n fieldset.attributes ??= {\n 'aria-describedby': hint.id\n }\n }\n\n components = collection.getViewModel(payload, errors)\n\n return {\n ...viewModel,\n fieldset,\n components\n }\n }\n\n isState(value?: FormStateValue | FormState): value is UkAddressState {\n return UkAddressField.isUkAddress(value)\n }\n\n static isUkAddress(\n value?: FormStateValue | FormState\n ): value is UkAddressState {\n return (\n isFormState(value) &&\n TextField.isText(value.addressLine1) &&\n TextField.isText(value.town) &&\n TextField.isText(value.postcode)\n )\n }\n}\n\nexport interface UkAddressState extends Record<string, string> {\n addressLine1: string\n addressLine2: string\n town: string\n county: string\n postcode: string\n}\n"],"mappings":"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","ignoreList":[]}
|
package/README.md
CHANGED
|
@@ -6,24 +6,9 @@ It is designed to be embedded in the frontend of a digital service and provide a
|
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
9
|
+
- [Demo of DXT](#demo-of-dxt)
|
|
9
10
|
- [Installation](#installation)
|
|
10
|
-
- [
|
|
11
|
-
- [Setup](#setup)
|
|
12
|
-
- [Form Config](#form-config)
|
|
13
|
-
- [Static Assets and Styles](#static-assets-and-styles)
|
|
14
|
-
- [Example](#example)
|
|
15
|
-
- [Environment Variables](#environment-variables)
|
|
16
|
-
- [Options](#options)
|
|
17
|
-
- [Services](#services)
|
|
18
|
-
- [Custom Controllers](#custom-controllers)
|
|
19
|
-
- [Custom Filters](#custom-filters)
|
|
20
|
-
- [Custom Cache](#custom-cache)
|
|
21
|
-
- [Exemplar](#exemplar)
|
|
22
|
-
- [Templates](#templates)
|
|
23
|
-
- [Template Data](#template-data)
|
|
24
|
-
- [Liquid Filters](#liquid-filters)
|
|
25
|
-
- [Examples](#examples)
|
|
26
|
-
- [Templates and Views: Extending the Default Layout](#templates-and-views-extending-the-default-layout)
|
|
11
|
+
- [Documentation](#documentation)
|
|
27
12
|
- [Publishing the Package](#publishing-the-package)
|
|
28
13
|
- [Semantic Versioning Control](#semantic-versioning-control)
|
|
29
14
|
- [Major-Version Release Branches](#major-version-release-branches)
|
|
@@ -31,305 +16,25 @@ It is designed to be embedded in the frontend of a digital service and provide a
|
|
|
31
16
|
- [Workflow Triggers](#workflow-triggers)
|
|
32
17
|
- [Safety and Consistency](#safety-and-consistency)
|
|
33
18
|
|
|
34
|
-
##
|
|
35
|
-
|
|
36
|
-
`npm install @defra/forms-engine-plugin --save`
|
|
37
|
-
|
|
38
|
-
## Dependencies
|
|
39
|
-
|
|
40
|
-
The following are [plugin dependencies](<https://hapi.dev/api/?v=21.4.0#server.dependency()>) that are required to be registered with hapi:
|
|
41
|
-
|
|
42
|
-
`npm install hapi-pino @hapi/crumb @hapi/yar @hapi/vision --save`
|
|
43
|
-
|
|
44
|
-
- [hapi-pino](https://github.com/hapijs/hapi-pino) - [Pino](https://github.com/pinojs/pino) logger for hapi
|
|
45
|
-
- [@hapi/crumb](https://github.com/hapijs/crumb) - CSRF crumb generation and validation
|
|
46
|
-
- [@hapi/yar](https://github.com/hapijs/yar) - Session manager
|
|
47
|
-
- [@hapi/vision](https://github.com/hapijs/vision) - Template rendering support
|
|
48
|
-
|
|
49
|
-
Additional npm dependencies that you will need are:
|
|
50
|
-
|
|
51
|
-
`npm install nunjucks govuk-frontend --save`
|
|
52
|
-
|
|
53
|
-
- [nunjucks](https://www.npmjs.com/package/nunjucks) - [templating engine](https://mozilla.github.io/nunjucks/) used by GOV.UK design system
|
|
54
|
-
- [govuk-frontend](https://www.npmjs.com/package/govuk-frontend) - [code](https://github.com/alphagov/govuk-frontend) you need to build a user interface for government platforms and services
|
|
55
|
-
|
|
56
|
-
Optional dependencies
|
|
57
|
-
|
|
58
|
-
`npm install @hapi/inert --save`
|
|
59
|
-
|
|
60
|
-
- [@hapi/inert](https://www.npmjs.com/package/@hapi/inert) - static file and directory handlers for serving GOV.UK assets and styles
|
|
61
|
-
|
|
62
|
-
## Setup
|
|
63
|
-
|
|
64
|
-
### Form config
|
|
65
|
-
|
|
66
|
-
The `form-engine-plugin` uses JSON configuration files to serve form journeys.
|
|
67
|
-
These files are called `Form definitions` and are built up of:
|
|
68
|
-
|
|
69
|
-
- `pages` - includes a `path`, `title`
|
|
70
|
-
- `components` - one or more questions on a page
|
|
71
|
-
- `conditions` - used to conditionally show and hide pages and
|
|
72
|
-
- `lists` - data used to in selection fields like [Select](https://design-system.service.gov.uk/components/select/), [Checkboxes](https://design-system.service.gov.uk/components/checkboxes/) and [Radios](https://design-system.service.gov.uk/components/radios/)
|
|
73
|
-
|
|
74
|
-
The [types](https://github.com/DEFRA/forms-designer/blob/main/model/src/form/form-definition/types.ts), `joi` [schema](https://github.com/DEFRA/forms-designer/blob/main/model/src/form/form-definition/index.ts) and the [examples](test/form/definitions) folder are a good place to learn about the structure of these files.
|
|
75
|
-
|
|
76
|
-
TODO - Link to wiki for `Form metadata`
|
|
77
|
-
TODO - Link to wiki for `Form definition`
|
|
78
|
-
|
|
79
|
-
#### Providing form config to the engine
|
|
80
|
-
|
|
81
|
-
The engine plugin registers several [routes](https://hapi.dev/tutorials/routing/?lang=en_US) on the hapi server.
|
|
82
|
-
|
|
83
|
-
They look like this:
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
GET /{slug}/{path}
|
|
87
|
-
POST /{slug}/{path}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
A unique `slug` is used to route the user to the correct form, and the `path` used to identify the correct page within the form to show.
|
|
91
|
-
The [plugin registration options](#options) have a `services` setting to provide a `formsService` that is responsible for returning `form definition` data.
|
|
92
|
-
|
|
93
|
-
WARNING: This below is subject to change
|
|
94
|
-
|
|
95
|
-
A `formsService` has two methods, one for returning `formMetadata` and another to return `formDefinition`s.
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
const formsService = {
|
|
99
|
-
getFormMetadata: async function (slug) {
|
|
100
|
-
// Returns the metadata for the slug
|
|
101
|
-
},
|
|
102
|
-
getFormDefinition: async function (id, state) {
|
|
103
|
-
// Returns the form definition for the given id
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
The reason for the two separate methods is caching.
|
|
109
|
-
`formMetadata` is a lightweight record designed to give top level information about a form.
|
|
110
|
-
This method is invoked for every page request.
|
|
111
|
-
|
|
112
|
-
Only when the `formMetadata` indicates that the definition has changed is a call to `getFormDefinition` is made.
|
|
113
|
-
The response from this can be quite big as it contains the entire form definition.
|
|
114
|
-
|
|
115
|
-
See [example](#example) below for more detail
|
|
116
|
-
|
|
117
|
-
### Static assets and styles
|
|
118
|
-
|
|
119
|
-
TODO
|
|
120
|
-
|
|
121
|
-
## Example
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
import hapi from '@hapi/hapi'
|
|
125
|
-
import yar from '@hapi/yar'
|
|
126
|
-
import crumb from '@hapi/crumb'
|
|
127
|
-
import inert from '@hapi/inert'
|
|
128
|
-
import pino from 'hapi-pino'
|
|
129
|
-
import plugin from '@defra/forms-engine-plugin'
|
|
130
|
-
|
|
131
|
-
const server = hapi.server({
|
|
132
|
-
port: 3000
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
// Register the dependent plugins
|
|
136
|
-
await server.register(pino)
|
|
137
|
-
await server.register(inert)
|
|
138
|
-
await server.register(crumb)
|
|
139
|
-
await server.register({
|
|
140
|
-
plugin: yar,
|
|
141
|
-
options: {
|
|
142
|
-
cookieOptions: {
|
|
143
|
-
password: 'ENTER_YOUR_SESSION_COOKIE_PASSWORD_HERE' // Must be > 32 chars
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
// Register the `forms-engine-plugin`
|
|
149
|
-
await server.register({
|
|
150
|
-
plugin
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
await server.start()
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## Environment variables
|
|
157
|
-
|
|
158
|
-
## Options
|
|
159
|
-
|
|
160
|
-
The forms plugin is configured with [registration options](https://hapi.dev/api/?v=21.4.0#plugins)
|
|
161
|
-
|
|
162
|
-
- `services` (optional) - object containing `formsService`, `formSubmissionService` and `outputService`
|
|
163
|
-
- `formsService` - used to load `formMetadata` and `formDefinition`
|
|
164
|
-
- `formSubmissionService` - used prepare the form during submission (ignore - subject to change)
|
|
165
|
-
- `outputService` - used to save the submission
|
|
166
|
-
- `controllers` (optional) - Object map of custom page controllers used to override the default. See [custom controllers](#custom-controllers)
|
|
167
|
-
- `filters` (optional) - A map of custom template filters to include
|
|
168
|
-
- `cacheName` (optional) - The cache name to use. Defaults to hapi's [default server cache]. Recommended for production. See [here]
|
|
169
|
-
(#custom-cache) for more details
|
|
170
|
-
- `viewPaths` (optional) - Include additional view paths when using custom `page.view`s
|
|
171
|
-
- `pluginPath` (optional) - The location of the plugin (defaults to `node_modules/@defra/forms-engine-plugin`)
|
|
172
|
-
|
|
173
|
-
### Services
|
|
174
|
-
|
|
175
|
-
TODO
|
|
176
|
-
|
|
177
|
-
### Custom controllers
|
|
178
|
-
|
|
179
|
-
TODO
|
|
180
|
-
|
|
181
|
-
### Custom filters
|
|
182
|
-
|
|
183
|
-
Use the `filter` plugin option to provide custom template filters.
|
|
184
|
-
Filters are available in both [nunjucks](https://mozilla.github.io/nunjucks/templating.html#filters) and [liquid](https://liquidjs.com/filters/overview.html) templates.
|
|
185
|
-
|
|
186
|
-
```
|
|
187
|
-
const formatter = new Intl.NumberFormat('en-GB')
|
|
188
|
-
|
|
189
|
-
await server.register({
|
|
190
|
-
plugin,
|
|
191
|
-
options: {
|
|
192
|
-
filters: {
|
|
193
|
-
money: value => formatter.format(value),
|
|
194
|
-
upper: value => typeof value === 'string' ? value.toUpperCase() : value
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Custom cache
|
|
201
|
-
|
|
202
|
-
The plugin will use the [default server cache](https://hapi.dev/api/?v=21.4.0#-serveroptionscache) to store form answers on the server.
|
|
203
|
-
This is just an in-memory cache which is fine for development.
|
|
204
|
-
|
|
205
|
-
In production you should create a custom cache one of the available `@hapi/catbox` adapters.
|
|
206
|
-
|
|
207
|
-
E.g. [Redis](https://github.com/hapijs/catbox-redis)
|
|
208
|
-
|
|
209
|
-
```
|
|
210
|
-
import { Engine as CatboxRedis } from '@hapi/catbox-redis'
|
|
211
|
-
|
|
212
|
-
const server = new Hapi.Server({
|
|
213
|
-
cache : [
|
|
214
|
-
{
|
|
215
|
-
name: 'my_cache',
|
|
216
|
-
provider: {
|
|
217
|
-
constructor: CatboxRedis,
|
|
218
|
-
options: {}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
]
|
|
222
|
-
})
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## Exemplar
|
|
19
|
+
## Demo of DXT
|
|
226
20
|
|
|
227
21
|
TODO: Link to CDP exemplar
|
|
228
22
|
|
|
229
|
-
##
|
|
230
|
-
|
|
231
|
-
The following elements support [LiquidJS templates](https://liquidjs.com/):
|
|
232
|
-
|
|
233
|
-
- Page **title**
|
|
234
|
-
- Form component **title**
|
|
235
|
-
- Support for fieldset legend text or label text
|
|
236
|
-
- This includes when the title is used in **error messages**
|
|
237
|
-
- Html (guidance) component **content**
|
|
238
|
-
- Summary component **row** key title (check answers and repeater summary)
|
|
239
|
-
|
|
240
|
-
### Template Data
|
|
241
|
-
|
|
242
|
-
The data the templates are evaluated against is the raw answers the user has provided up to the page they're currently on.
|
|
243
|
-
For example, given a YesNoField component called `TKsWbP`, the template `{{ TKsWbP }}` would render "true" or "false" depending on how the user answered the question.
|
|
244
|
-
|
|
245
|
-
The current FormContext is also available as `context` in the templates. This allows access to the full data including the path the user has taken in their journey and any miscellaneous data returned from `Page event`s in `context.data`.
|
|
246
|
-
|
|
247
|
-
### Liquid Filters
|
|
248
|
-
|
|
249
|
-
There are a number of `LiquidJS` filters available to you from within the templates:
|
|
250
|
-
|
|
251
|
-
- `page` - returns the page definition for the given path
|
|
252
|
-
- `field` - returns the component definition for the given name
|
|
253
|
-
- `href` - returns the page href for the given page path
|
|
254
|
-
- `answer` - returns the user's answer for a given component
|
|
255
|
-
- `evaluate` - evaluates and returns a Liquid template using the current context
|
|
256
|
-
|
|
257
|
-
### Examples
|
|
258
|
-
|
|
259
|
-
```json
|
|
260
|
-
"pages": [
|
|
261
|
-
{
|
|
262
|
-
"title": "What's your name?",
|
|
263
|
-
"path": "/full-name",
|
|
264
|
-
"components": [
|
|
265
|
-
{
|
|
266
|
-
"name": "WmHfSb",
|
|
267
|
-
"title": "What's your full name?",
|
|
268
|
-
"type": "TextField"
|
|
269
|
-
}
|
|
270
|
-
]
|
|
271
|
-
},
|
|
272
|
-
// This example shows how a component can use an answer to a previous question (What's your full name) in it's title
|
|
273
|
-
{
|
|
274
|
-
"title": "Are you in England?",
|
|
275
|
-
"path": "/are-you-in-england",
|
|
276
|
-
"components": [
|
|
277
|
-
{
|
|
278
|
-
"name": "TKsWbP",
|
|
279
|
-
"title": "Are you in England, {{ WmHfSb }}?",
|
|
280
|
-
"type": "YesNoField"
|
|
281
|
-
}
|
|
282
|
-
]
|
|
283
|
-
},
|
|
284
|
-
// This example shows how a Html (guidance) component can use the available filters to get the form definition and user answers and display them
|
|
285
|
-
{
|
|
286
|
-
"title": "Template example for {{ WmHfSb }}?",
|
|
287
|
-
"path": "/example",
|
|
288
|
-
"components": [
|
|
289
|
-
{
|
|
290
|
-
"title": "Html",
|
|
291
|
-
"type": "Html",
|
|
292
|
-
"content": "<p class=\"govuk-body\">
|
|
293
|
-
// Use Liquid's `assign` to create a variable that holds reference to the \"/are-you-in-england\" page
|
|
294
|
-
{%- assign inEngland = \"/are-you-in-england\" | page -%}
|
|
295
|
-
|
|
296
|
-
// Use the reference to `evaluate` the title
|
|
297
|
-
{{ inEngland.title | evaluate }}<br>
|
|
298
|
-
|
|
299
|
-
// Use the href filter to display the full page path
|
|
300
|
-
{{ \"/are-you-in-england\" | href }}<br>
|
|
301
|
-
|
|
302
|
-
// Use the `answer` filter to render the user provided answer to a question
|
|
303
|
-
{{ 'TKsWbP' | answer }}
|
|
304
|
-
</p>\n"
|
|
305
|
-
}
|
|
306
|
-
]
|
|
307
|
-
}
|
|
308
|
-
]
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
## Templates and views
|
|
312
|
-
|
|
313
|
-
### Extending the default layout
|
|
314
|
-
|
|
315
|
-
TODO
|
|
316
|
-
|
|
317
|
-
To override the default page template, vision and nunjucks both need to be configured to search in the `forms-engine-plugin` views directory when looking for template files.
|
|
23
|
+
## Installation
|
|
318
24
|
|
|
319
|
-
|
|
320
|
-
For nunjucks it is configured through the environment [configure options](https://mozilla.github.io/nunjucks/api.html#configure).
|
|
25
|
+
[See our getting started developer guide](./docs/GETTING_STARTED.md).
|
|
321
26
|
|
|
322
|
-
|
|
27
|
+
## Documentation
|
|
323
28
|
|
|
324
|
-
|
|
29
|
+
DXT has a mix of configuration-driven and code-based features that developers can utilise.
|
|
325
30
|
|
|
326
|
-
|
|
31
|
+
[See our documentation folder](./docs/INDEX.md) to learn more about the features of DXT.
|
|
327
32
|
|
|
328
|
-
|
|
33
|
+
## Contributing
|
|
329
34
|
|
|
330
|
-
|
|
35
|
+
[See our contribution guide](./docs/CONTRIBUTING.md).
|
|
331
36
|
|
|
332
|
-
## Publishing the
|
|
37
|
+
## Publishing the package
|
|
333
38
|
|
|
334
39
|
Our GitHub Actions workflow (`publish.yml`) is set up to make publishing a breeze, using semantic versioning and a variety of release strategies. Here's how you can make the most of it:
|
|
335
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/forms-engine-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Defra forms engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"./file-upload.min.js.map": "./.public/javascripts/file-upload.min.js.map",
|
|
23
23
|
"./application.min.css": "./.public/stylesheets/application.min.css",
|
|
24
24
|
"./controllers/*": "./.server/server/plugins/engine/pageControllers/*",
|
|
25
|
+
"./services/*": "./.server/server/plugins/engine/services/*",
|
|
25
26
|
"./package.json": "./package.json"
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
package/src/config/index.ts
CHANGED
|
@@ -90,13 +90,13 @@ export const config = convict({
|
|
|
90
90
|
doc: 'The service version, this variable is injected into your docker container in CDP environments',
|
|
91
91
|
format: String,
|
|
92
92
|
nullable: true,
|
|
93
|
-
default:
|
|
93
|
+
default: '',
|
|
94
94
|
env: 'SERVICE_VERSION'
|
|
95
95
|
} as SchemaObj<string>,
|
|
96
96
|
feedbackLink: {
|
|
97
97
|
doc: 'Used in your phase banner. Can be a URL or more commonly mailto mailto:feedback@department.gov.uk',
|
|
98
98
|
format: String,
|
|
99
|
-
default:
|
|
99
|
+
default: '',
|
|
100
100
|
env: 'FEEDBACK_LINK'
|
|
101
101
|
} as SchemaObj<string>,
|
|
102
102
|
phaseTag: {
|
|
@@ -121,7 +121,7 @@ export const config = convict({
|
|
|
121
121
|
},
|
|
122
122
|
sessionCookiePassword: {
|
|
123
123
|
format: String,
|
|
124
|
-
default:
|
|
124
|
+
default: '',
|
|
125
125
|
sensitive: true,
|
|
126
126
|
env: 'SESSION_COOKIE_PASSWORD'
|
|
127
127
|
} as SchemaObj<string>,
|
|
@@ -129,26 +129,26 @@ export const config = convict({
|
|
|
129
129
|
host: {
|
|
130
130
|
doc: 'Redis cache host',
|
|
131
131
|
format: String,
|
|
132
|
-
default:
|
|
132
|
+
default: '',
|
|
133
133
|
env: 'REDIS_HOST'
|
|
134
134
|
} as SchemaObj<string>,
|
|
135
135
|
username: {
|
|
136
136
|
doc: 'Redis cache username',
|
|
137
137
|
format: String,
|
|
138
|
-
default:
|
|
138
|
+
default: '',
|
|
139
139
|
env: 'REDIS_USERNAME'
|
|
140
140
|
} as SchemaObj<string>,
|
|
141
141
|
password: {
|
|
142
142
|
doc: 'Redis cache password',
|
|
143
143
|
format: '*',
|
|
144
|
-
default:
|
|
144
|
+
default: '',
|
|
145
145
|
sensitive: true,
|
|
146
146
|
env: 'REDIS_PASSWORD'
|
|
147
147
|
} as SchemaObj<string>,
|
|
148
148
|
keyPrefix: {
|
|
149
149
|
doc: 'Redis cache key prefix name used to isolate the cached results across multiple clients',
|
|
150
150
|
format: String,
|
|
151
|
-
default:
|
|
151
|
+
default: '',
|
|
152
152
|
env: 'REDIS_KEY_PREFIX'
|
|
153
153
|
} as SchemaObj<string>
|
|
154
154
|
},
|
|
@@ -167,12 +167,12 @@ export const config = convict({
|
|
|
167
167
|
*/
|
|
168
168
|
notifyTemplateId: {
|
|
169
169
|
format: String,
|
|
170
|
-
default:
|
|
170
|
+
default: '',
|
|
171
171
|
env: 'NOTIFY_TEMPLATE_ID'
|
|
172
172
|
} as SchemaObj<string>,
|
|
173
173
|
notifyAPIKey: {
|
|
174
174
|
format: String,
|
|
175
|
-
default:
|
|
175
|
+
default: '',
|
|
176
176
|
env: 'NOTIFY_API_KEY'
|
|
177
177
|
} as SchemaObj<string>,
|
|
178
178
|
|
|
@@ -181,25 +181,25 @@ export const config = convict({
|
|
|
181
181
|
*/
|
|
182
182
|
managerUrl: {
|
|
183
183
|
format: String,
|
|
184
|
-
default:
|
|
184
|
+
default: 'http://localhost:3001',
|
|
185
185
|
env: 'MANAGER_URL'
|
|
186
186
|
} as SchemaObj<string>,
|
|
187
187
|
|
|
188
188
|
designerUrl: {
|
|
189
189
|
format: String,
|
|
190
|
-
default:
|
|
190
|
+
default: 'http://localhost:3000',
|
|
191
191
|
env: 'DESIGNER_URL'
|
|
192
192
|
} as SchemaObj<string>,
|
|
193
193
|
|
|
194
194
|
submissionUrl: {
|
|
195
195
|
format: String,
|
|
196
|
-
default:
|
|
196
|
+
default: 'http://localhost:3002',
|
|
197
197
|
env: 'SUBMISSION_URL'
|
|
198
198
|
} as SchemaObj<string>,
|
|
199
199
|
|
|
200
200
|
uploaderUrl: {
|
|
201
201
|
format: String,
|
|
202
|
-
default:
|
|
202
|
+
default: 'http://localhost:7337',
|
|
203
203
|
env: 'UPLOADER_URL'
|
|
204
204
|
} as SchemaObj<string>,
|
|
205
205
|
|
|
@@ -65,6 +65,13 @@ describe('UkAddressField', () => {
|
|
|
65
65
|
})
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
+
expect(keys).toHaveProperty(
|
|
69
|
+
'myComponent__county',
|
|
70
|
+
expect.objectContaining({
|
|
71
|
+
flags: expect.objectContaining({ label: 'County' })
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
|
|
68
75
|
expect(keys).toHaveProperty(
|
|
69
76
|
`myComponent__postcode`,
|
|
70
77
|
expect.objectContaining({
|
|
@@ -82,6 +89,7 @@ describe('UkAddressField', () => {
|
|
|
82
89
|
'myComponent__addressLine1',
|
|
83
90
|
'myComponent__addressLine2',
|
|
84
91
|
'myComponent__town',
|
|
92
|
+
'myComponent__county',
|
|
85
93
|
'myComponent__postcode'
|
|
86
94
|
])
|
|
87
95
|
|
|
@@ -118,6 +126,14 @@ describe('UkAddressField', () => {
|
|
|
118
126
|
})
|
|
119
127
|
)
|
|
120
128
|
|
|
129
|
+
expect(keys).toHaveProperty(
|
|
130
|
+
'myComponent__county',
|
|
131
|
+
expect.objectContaining({
|
|
132
|
+
allow: [''], // Required but empty string is allowed
|
|
133
|
+
flags: expect.objectContaining({ presence: 'required' })
|
|
134
|
+
})
|
|
135
|
+
)
|
|
136
|
+
|
|
121
137
|
expect(keys).toHaveProperty(
|
|
122
138
|
`myComponent__postcode`,
|
|
123
139
|
expect.objectContaining({
|
|
@@ -157,6 +173,11 @@ describe('UkAddressField', () => {
|
|
|
157
173
|
expect.objectContaining({ allow: [''] })
|
|
158
174
|
)
|
|
159
175
|
|
|
176
|
+
expect(keys).toHaveProperty(
|
|
177
|
+
'myComponent__county',
|
|
178
|
+
expect.objectContaining({ allow: [''] })
|
|
179
|
+
)
|
|
180
|
+
|
|
160
181
|
expect(keys).toHaveProperty(
|
|
161
182
|
`myComponent__postcode`,
|
|
162
183
|
expect.objectContaining({ allow: [''] })
|
|
@@ -167,6 +188,7 @@ describe('UkAddressField', () => {
|
|
|
167
188
|
addressLine1: '',
|
|
168
189
|
addressLine2: '',
|
|
169
190
|
town: '',
|
|
191
|
+
county: '',
|
|
170
192
|
postcode: ''
|
|
171
193
|
})
|
|
172
194
|
)
|
|
@@ -180,6 +202,7 @@ describe('UkAddressField', () => {
|
|
|
180
202
|
addressLine1: 'Richard Fairclough House',
|
|
181
203
|
addressLine2: 'Knutsford Road',
|
|
182
204
|
town: 'Warrington',
|
|
205
|
+
county: 'Cheshire',
|
|
183
206
|
postcode: 'WA4 1HT'
|
|
184
207
|
})
|
|
185
208
|
)
|
|
@@ -189,6 +212,7 @@ describe('UkAddressField', () => {
|
|
|
189
212
|
addressLine1: 'Richard Fairclough House',
|
|
190
213
|
addressLine2: '', // Optional field
|
|
191
214
|
town: 'Warrington',
|
|
215
|
+
county: '', // Optional field
|
|
192
216
|
postcode: 'WA4 1HT'
|
|
193
217
|
})
|
|
194
218
|
)
|
|
@@ -203,6 +227,7 @@ describe('UkAddressField', () => {
|
|
|
203
227
|
addressLine1: '',
|
|
204
228
|
addressLine2: '',
|
|
205
229
|
town: '',
|
|
230
|
+
county: '',
|
|
206
231
|
postcode: ''
|
|
207
232
|
})
|
|
208
233
|
)
|
|
@@ -228,6 +253,7 @@ describe('UkAddressField', () => {
|
|
|
228
253
|
addressLine1: ['invalid'],
|
|
229
254
|
addressLine2: ['invalid'],
|
|
230
255
|
town: ['invalid'],
|
|
256
|
+
county: ['invalid'],
|
|
231
257
|
postcode: ['invalid']
|
|
232
258
|
})
|
|
233
259
|
)
|
|
@@ -237,6 +263,7 @@ describe('UkAddressField', () => {
|
|
|
237
263
|
addressLine1: 'invalid',
|
|
238
264
|
addressLine2: 'invalid',
|
|
239
265
|
town: 'invalid',
|
|
266
|
+
county: 'invalid',
|
|
240
267
|
postcode: 'invalid'
|
|
241
268
|
})
|
|
242
269
|
)
|
|
@@ -252,6 +279,7 @@ describe('UkAddressField', () => {
|
|
|
252
279
|
addressLine1: 'Richard Fairclough House',
|
|
253
280
|
addressLine2: 'Knutsford Road',
|
|
254
281
|
town: 'Warrington',
|
|
282
|
+
county: 'Cheshire',
|
|
255
283
|
postcode: 'WA4 1HT'
|
|
256
284
|
}
|
|
257
285
|
|
|
@@ -263,7 +291,7 @@ describe('UkAddressField', () => {
|
|
|
263
291
|
const answer2 = getAnswer(field, state2)
|
|
264
292
|
|
|
265
293
|
expect(answer1).toBe(
|
|
266
|
-
'Richard Fairclough House<br>Knutsford Road<br>Warrington<br>WA4 1HT<br>'
|
|
294
|
+
'Richard Fairclough House<br>Knutsford Road<br>Warrington<br>Cheshire<br>WA4 1HT<br>'
|
|
267
295
|
)
|
|
268
296
|
|
|
269
297
|
expect(answer2).toBe('')
|
|
@@ -302,6 +330,7 @@ describe('UkAddressField', () => {
|
|
|
302
330
|
'Richard Fairclough House',
|
|
303
331
|
'Knutsford Road',
|
|
304
332
|
'Warrington',
|
|
333
|
+
'Cheshire',
|
|
305
334
|
'WA4 1HT'
|
|
306
335
|
])
|
|
307
336
|
|
|
@@ -325,6 +354,7 @@ describe('UkAddressField', () => {
|
|
|
325
354
|
addressLine1: 'Richard Fairclough House',
|
|
326
355
|
addressLine2: 'Knutsford Road',
|
|
327
356
|
town: 'Warrington',
|
|
357
|
+
county: 'Cheshire',
|
|
328
358
|
postcode: 'WA4 1HT'
|
|
329
359
|
}
|
|
330
360
|
|
|
@@ -363,6 +393,14 @@ describe('UkAddressField', () => {
|
|
|
363
393
|
})
|
|
364
394
|
}),
|
|
365
395
|
|
|
396
|
+
expect.objectContaining({
|
|
397
|
+
model: getViewModel(address, 'county', {
|
|
398
|
+
label: { text: 'County (optional)' },
|
|
399
|
+
attributes: { autocomplete: 'county' },
|
|
400
|
+
value: address.county
|
|
401
|
+
})
|
|
402
|
+
}),
|
|
403
|
+
|
|
366
404
|
expect.objectContaining({
|
|
367
405
|
model: getViewModel(address, 'postcode', {
|
|
368
406
|
label: { text: 'Postcode' },
|
|
@@ -395,6 +433,7 @@ describe('UkAddressField', () => {
|
|
|
395
433
|
addressLine1: 'Richard Fairclough House',
|
|
396
434
|
addressLine2: 'Knutsford Road',
|
|
397
435
|
town: 'Warrington',
|
|
436
|
+
county: 'Cheshire',
|
|
398
437
|
postcode: 'WA4 1HT'
|
|
399
438
|
}
|
|
400
439
|
|
|
@@ -407,6 +446,9 @@ describe('UkAddressField', () => {
|
|
|
407
446
|
const townInvalid =
|
|
408
447
|
'Town 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
|
409
448
|
|
|
449
|
+
const countyInvalid =
|
|
450
|
+
'County 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
|
451
|
+
|
|
410
452
|
const postcodeInvalid = '111 XX2'
|
|
411
453
|
|
|
412
454
|
describe.each([
|
|
@@ -424,6 +466,7 @@ describe('UkAddressField', () => {
|
|
|
424
466
|
addressLine1: ' Richard Fairclough House',
|
|
425
467
|
addressLine2: ' Knutsford Road',
|
|
426
468
|
town: ' Warrington',
|
|
469
|
+
county: 'Cheshire',
|
|
427
470
|
postcode: ' WA4 1HT'
|
|
428
471
|
}),
|
|
429
472
|
output: {
|
|
@@ -435,6 +478,7 @@ describe('UkAddressField', () => {
|
|
|
435
478
|
addressLine1: 'Richard Fairclough House ',
|
|
436
479
|
addressLine2: 'Knutsford Road ',
|
|
437
480
|
town: 'Warrington ',
|
|
481
|
+
county: 'Cheshire ',
|
|
438
482
|
postcode: 'WA4 1HT '
|
|
439
483
|
}),
|
|
440
484
|
output: {
|
|
@@ -446,6 +490,7 @@ describe('UkAddressField', () => {
|
|
|
446
490
|
addressLine1: ' Richard Fairclough House \n\n',
|
|
447
491
|
addressLine2: ' Knutsford Road \n\n',
|
|
448
492
|
town: ' Warrington \n\n',
|
|
493
|
+
county: ' Cheshire \n\n',
|
|
449
494
|
postcode: ' WA4 1HT \n\n'
|
|
450
495
|
}),
|
|
451
496
|
output: {
|
|
@@ -468,6 +513,7 @@ describe('UkAddressField', () => {
|
|
|
468
513
|
addressLine1: addressLine1Invalid,
|
|
469
514
|
addressLine2: 'Knutsford Road',
|
|
470
515
|
town: 'Warrington',
|
|
516
|
+
county: 'Cheshire',
|
|
471
517
|
postcode: 'WA4 1HT'
|
|
472
518
|
}),
|
|
473
519
|
output: {
|
|
@@ -475,6 +521,7 @@ describe('UkAddressField', () => {
|
|
|
475
521
|
addressLine1: addressLine1Invalid,
|
|
476
522
|
addressLine2: 'Knutsford Road',
|
|
477
523
|
town: 'Warrington',
|
|
524
|
+
county: 'Cheshire',
|
|
478
525
|
postcode: 'WA4 1HT'
|
|
479
526
|
}),
|
|
480
527
|
errors: [
|
|
@@ -489,6 +536,7 @@ describe('UkAddressField', () => {
|
|
|
489
536
|
addressLine1: 'Richard Fairclough House',
|
|
490
537
|
addressLine2: addressLine2Invalid,
|
|
491
538
|
town: 'Warrington',
|
|
539
|
+
county: 'Cheshire',
|
|
492
540
|
postcode: 'WA4 1HT'
|
|
493
541
|
}),
|
|
494
542
|
output: {
|
|
@@ -496,6 +544,7 @@ describe('UkAddressField', () => {
|
|
|
496
544
|
addressLine1: 'Richard Fairclough House',
|
|
497
545
|
addressLine2: addressLine2Invalid,
|
|
498
546
|
town: 'Warrington',
|
|
547
|
+
county: 'Cheshire',
|
|
499
548
|
postcode: 'WA4 1HT'
|
|
500
549
|
}),
|
|
501
550
|
errors: [
|
|
@@ -510,6 +559,7 @@ describe('UkAddressField', () => {
|
|
|
510
559
|
addressLine1: 'Richard Fairclough House',
|
|
511
560
|
addressLine2: 'Knutsford Road',
|
|
512
561
|
town: townInvalid,
|
|
562
|
+
county: 'Cheshire',
|
|
513
563
|
postcode: 'WA4 1HT'
|
|
514
564
|
}),
|
|
515
565
|
output: {
|
|
@@ -517,6 +567,7 @@ describe('UkAddressField', () => {
|
|
|
517
567
|
addressLine1: 'Richard Fairclough House',
|
|
518
568
|
addressLine2: 'Knutsford Road',
|
|
519
569
|
town: townInvalid,
|
|
570
|
+
county: 'Cheshire',
|
|
520
571
|
postcode: 'WA4 1HT'
|
|
521
572
|
}),
|
|
522
573
|
errors: [
|
|
@@ -531,6 +582,30 @@ describe('UkAddressField', () => {
|
|
|
531
582
|
addressLine1: 'Richard Fairclough House',
|
|
532
583
|
addressLine2: 'Knutsford Road',
|
|
533
584
|
town: 'Warrington',
|
|
585
|
+
county: countyInvalid,
|
|
586
|
+
postcode: 'WA4 1HT'
|
|
587
|
+
}),
|
|
588
|
+
output: {
|
|
589
|
+
value: getFormData({
|
|
590
|
+
addressLine1: 'Richard Fairclough House',
|
|
591
|
+
addressLine2: 'Knutsford Road',
|
|
592
|
+
town: 'Warrington',
|
|
593
|
+
county: countyInvalid,
|
|
594
|
+
postcode: 'WA4 1HT'
|
|
595
|
+
}),
|
|
596
|
+
errors: [
|
|
597
|
+
expect.objectContaining({
|
|
598
|
+
text: 'County must be 100 characters or less'
|
|
599
|
+
})
|
|
600
|
+
]
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
input: getFormData({
|
|
605
|
+
addressLine1: 'Richard Fairclough House',
|
|
606
|
+
addressLine2: 'Knutsford Road',
|
|
607
|
+
town: 'Warrington',
|
|
608
|
+
county: 'Cheshire',
|
|
534
609
|
postcode: postcodeInvalid
|
|
535
610
|
}),
|
|
536
611
|
output: {
|
|
@@ -538,6 +613,7 @@ describe('UkAddressField', () => {
|
|
|
538
613
|
addressLine1: 'Richard Fairclough House',
|
|
539
614
|
addressLine2: 'Knutsford Road',
|
|
540
615
|
town: 'Warrington',
|
|
616
|
+
county: 'Cheshire',
|
|
541
617
|
postcode: postcodeInvalid
|
|
542
618
|
}),
|
|
543
619
|
errors: [
|
|
@@ -602,6 +678,7 @@ function getFormData(address: FormPayload): FormPayload {
|
|
|
602
678
|
myComponent__addressLine1: address.addressLine1,
|
|
603
679
|
myComponent__addressLine2: address.addressLine2,
|
|
604
680
|
myComponent__town: address.town,
|
|
681
|
+
myComponent__county: address.county,
|
|
605
682
|
myComponent__postcode: address.postcode
|
|
606
683
|
}
|
|
607
684
|
}
|
|
@@ -610,7 +687,7 @@ function getFormData(address: FormPayload): FormPayload {
|
|
|
610
687
|
* UK address session state
|
|
611
688
|
*/
|
|
612
689
|
function getFormState(address: FormPayload): FormState {
|
|
613
|
-
const [addressLine1, addressLine2, town, postcode] = Object.values(
|
|
690
|
+
const [addressLine1, addressLine2, town, county, postcode] = Object.values(
|
|
614
691
|
getFormData(address)
|
|
615
692
|
)
|
|
616
693
|
|
|
@@ -618,6 +695,7 @@ function getFormState(address: FormPayload): FormState {
|
|
|
618
695
|
myComponent__addressLine1: addressLine1 ?? null,
|
|
619
696
|
myComponent__addressLine2: addressLine2 ?? null,
|
|
620
697
|
myComponent__town: town ?? null,
|
|
698
|
+
myComponent__county: county ?? null,
|
|
621
699
|
myComponent__postcode: postcode ?? null
|
|
622
700
|
}
|
|
623
701
|
}
|
|
@@ -70,6 +70,17 @@ export class UkAddressField extends FormComponent {
|
|
|
70
70
|
optionalText: !isRequired && (hideOptional || !hideTitle)
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
+
{
|
|
74
|
+
type: ComponentType.TextField,
|
|
75
|
+
name: `${name}__county`,
|
|
76
|
+
title: 'County',
|
|
77
|
+
schema: { max: 100 },
|
|
78
|
+
options: {
|
|
79
|
+
autocomplete: 'county',
|
|
80
|
+
required: false,
|
|
81
|
+
optionalText: !isRequired && (hideOptional || !hideTitle)
|
|
82
|
+
}
|
|
83
|
+
},
|
|
73
84
|
{
|
|
74
85
|
type: ComponentType.TextField,
|
|
75
86
|
name: `${name}__postcode`,
|
|
@@ -168,5 +179,6 @@ export interface UkAddressState extends Record<string, string> {
|
|
|
168
179
|
addressLine1: string
|
|
169
180
|
addressLine2: string
|
|
170
181
|
town: string
|
|
182
|
+
county: string
|
|
171
183
|
postcode: string
|
|
172
184
|
}
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
{% set feedbackLink = feedbackLink or config.feedbackLink -%}
|
|
133
133
|
{% set phaseTag = phaseTag or config.phaseTag -%}
|
|
134
134
|
|
|
135
|
-
{% if phaseTag %}
|
|
135
|
+
{% if phaseTag and feedbackLink %}
|
|
136
136
|
{% set feedbackLinkHtml -%}
|
|
137
137
|
<a href="{{ currentPath if context.isForceAccess else feedbackLink }}" class="govuk-link" {%- if not context.isForceAccess %} target="_blank" rel="noopener noreferrer" {%- endif %}>
|
|
138
138
|
{%- if "mailto:" in feedbackLink -%}
|