Skip to content

Commit efe00ef

Browse files
authored
Merge pull request #409 from DEFRA/docs/gaps
Docs: add any feature gaps
2 parents ca53207 + 37a9aa7 commit efe00ef

28 files changed

Lines changed: 1242 additions & 400 deletions

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Documentation
2+
3+
## What is generated vs hand-written
4+
5+
Before modifying anything under `docs/`, read the scripts in the `scripts/` directory to determine whether the target file is generated. Generated files are overwritten on every script run; edits to them will be lost. If a file is generated, modify the relevant script or its data sources instead of editing the output directly, then re-run the script to rebuild.
6+
7+
## Preview wrapper classes
8+
9+
All generated component and page previews include `app-no-prose` on their wrapper `<div>` by default. This prevents Docusaurus prose CSS from interfering with GOV.UK component styles. Do not remove it.

docs/BUILDING_THE_PACKAGE.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
---
2-
layout: default
3-
title: Building the package
4-
render_with_liquid: false
5-
nav_order: 5
6-
---
7-
81
# Building the package
92

103
1. [Overview](#overview)

docs/contributing.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,7 @@ If you would like to fix the bug yourself, contributions are accepted through pu
5656
### Adding features
5757

5858
Features should be discussed with the Defra Forms team prior to implementation. This is to prevent wasted effort if the Defra Forms team decides not to accept it, or if we suggest any significant amendments. Reach out to us on [#defra-forms-support](https://defra-digital-team.slack.com) to discuss your requirements. If accepted by the product owner, we welcome a pull request.
59+
60+
## Building and publishing the package
61+
62+
See the [Building the package](./BUILDING_THE_PACKAGE) guide for documentation on the build pipeline, path alias resolution, and the npm publish workflow.
Lines changed: 105 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,134 @@
11
# Overriding forms-engine-plugin logic with custom services
22

3-
## Customising where forms are loaded from
3+
The `services` plugin option accepts three service objects — `formsService`, `formSubmissionService`, and `outputService` — which together cover where forms come from, where submission data goes, and how submission notifications are sent. Replace any or all of them to integrate with your own infrastructure.
44

5-
The engine plugin registers several [routes](https://hapi.dev/tutorials/routing/?lang=en_US) on the hapi server.
5+
## formsService
66

7-
They look like this:
7+
Responsible for loading form metadata and definitions. Called on every page request to check for definition changes and load the full definition when needed.
88

9-
```
10-
GET /{slug}/{path}
11-
POST /{slug}/{path}
12-
```
13-
14-
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.
15-
16-
The [plugin registration options](/plugin-options) have a `services` setting to provide a `formsService` that is responsible for returning `form definition` data.
17-
18-
WARNING: This below is subject to change
19-
20-
A `formsService` has two methods, one for returning `formMetadata` and another to return `formDefinition`s.
21-
22-
```javascript
23-
const formsService = {
24-
getFormMetadata: async function (slug) {
25-
// Returns the metadata for the slug
26-
},
27-
getFormDefinition: async function (id, state) {
28-
// Returns the form definition for the given id
29-
}
9+
```ts
10+
interface FormsService {
11+
getFormMetadata: (slug: string) => Promise<FormMetadata>
12+
getFormMetadataById: (id: string) => Promise<FormMetadata>
13+
getFormDefinition: (id: string, state: FormStatus) => Promise<FormDefinition | undefined>
14+
getFormSecret: (formId: string, secretName: string) => Promise<string>
3015
}
3116
```
3217

33-
The reason for the two separate methods is caching.
34-
`formMetadata` is a lightweight record designed to give top level information about a form.
35-
This method is invoked for every page request.
18+
`getFormMetadata` is called on every request and should be fast. `getFormDefinition` is only called when the metadata signals the definition has changed, so it can be slower. `getFormMetadataById` is called by the status page to retrieve the submitted form's name by its ID — this allows the confirmation panel to display the correct form name even when the current URL belongs to a different form (for example, a shared feedback form).
3619

37-
Only when the `formMetadata` indicates that the definition has changed is a call to `getFormDefinition` is made.
38-
The response from this can be quite big as it contains the entire form definition.
20+
`getFormSecret` retrieves a secret that belongs to a specific form — for example, a payment API key scoped to that form. This is distinct from global secrets such as `ordnanceSurveyApiKey` and `ordnanceSurveyApiSecret`, which are passed once at plugin registration and apply across all forms. If your forms do not use any components that require per-form secrets, this method will never be called and you can safely return `undefined` (or leave it unimplemented). See the documentation for each component to find out whether it requires secrets and what names it requests.
3921

40-
## Loading forms from files
22+
### Loading forms from files
4123

42-
To create a `formsService` from form config files that live on disk, you can use the `FileFormService` class.
43-
Form definition config files can be either `.json` or `.yaml`.
24+
For local or file-based forms, use the built-in `FileFormService`:
4425

45-
Once created and files have been loaded using the `addForm` method,
46-
call the `toFormsService` method to return a `FormService` compliant interface which can be passed in to the `services` setting of the [plugin options](/plugin-options).
47-
48-
```javascript
26+
```js
4927
import { FileFormService } from '@defra/forms-engine-plugin/file-form-service.js'
5028

51-
// Create shared form metadata
5229
const now = new Date()
5330
const user = { id: 'user', displayName: 'Username' }
5431
const author = { createdAt: now, createdBy: user, updatedAt: now, updatedBy: user }
55-
const metadata = {
32+
33+
const loader = new FileFormService()
34+
35+
await loader.addForm('src/definitions/example-form.yaml', {
36+
id: '95e92559-968d-44ae-8666-2b1ad3dffd31',
37+
title: 'Example form',
38+
slug: 'example-form',
5639
organisation: 'Defra',
5740
teamName: 'Team name',
5841
teamEmail: 'team@defra.gov.uk',
5942
submissionGuidance: "Thanks for your submission, we'll be in touch",
60-
notificationEmail: 'email@domain.com',
43+
notificationEmail: 'team@defra.gov.uk',
6144
...author,
6245
live: author
46+
})
47+
48+
const formsService = loader.toFormsService()
49+
```
50+
51+
---
52+
53+
## formSubmissionService
54+
55+
Called during form submission to persist the submitted data and any uploaded files. The default implementation calls the Defra Forms submission API (`SUBMISSION_URL`), which is part of the Defra Forms hosting infrastructure. Teams not using that infrastructure must provide their own implementation.
56+
57+
```ts
58+
interface FormSubmissionService {
59+
persistFiles: (
60+
files: { fileId: string; initiatedRetrievalKey: string }[],
61+
persistedRetrievalKey: string
62+
) => Promise<object>
63+
submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>
6364
}
65+
```
6466

65-
// Instantiate the file loader form service
66-
const loader = new FileFormService()
67+
`submit` is called first with the structured form payload. The `SubmitResponsePayload` it returns (including CSV file IDs) is then passed to `outputService.submit`. `persistFiles` is called by `FileUploadField` during submission to move uploaded files from temporary to permanent storage.
6768

68-
// Add a Json form
69-
await loader.addForm(
70-
'src/definitions/example-form.json', {
71-
...metadata,
72-
id: '95e92559-968d-44ae-8666-2b1ad3dffd31',
73-
title: 'Example Json',
74-
slug: 'example-json'
75-
}
76-
)
77-
78-
// Add a Yaml form
79-
await loader.addForm(
80-
'src/definitions/example-form.yaml', {
81-
...metadata,
82-
id: '641aeafd-13dd-40fa-9186-001703800efb',
83-
title: 'Example Yaml',
84-
slug: 'example-yaml'
69+
Override this service if you are not using the Defra Forms hosting infrastructure, or if you handle file persistence differently.
70+
71+
---
72+
73+
## outputService
74+
75+
Called after `formSubmissionService.submit` completes. Its job is to deliver the submission — by default, as a GOV.UK Notify email.
76+
77+
```ts
78+
interface OutputService {
79+
submit: (
80+
context: FormContext,
81+
request: FormRequestPayload,
82+
model: FormModel,
83+
emailAddress: string,
84+
items: DetailItem[],
85+
submitResponse: SubmitResponsePayload,
86+
formMetadata?: FormMetadata
87+
) => Promise<void>
88+
}
89+
```
90+
91+
The default implementation (`notifyService`) formats the submission using the [output formatter](#output-format) configured on the form definition and sends it to `emailAddress` via GOV.UK Notify.
92+
93+
Override this service to deliver submissions differently — for example, publishing to an SNS topic, calling a webhook, or writing to a database. Your implementation receives the full `FormContext`, `FormModel`, `DetailItem[]` array, and the `SubmitResponsePayload` from `formSubmissionService`, giving you everything needed to format and route the submission however you need.
94+
95+
```js
96+
await server.register({
97+
plugin,
98+
options: {
99+
services: {
100+
formsService,
101+
formSubmissionService,
102+
outputService: {
103+
async submit(context, request, model, emailAddress, items, submitResponse, formMetadata) {
104+
// publish to SNS, call a webhook, etc.
105+
}
106+
}
107+
}
85108
}
86-
)
109+
})
110+
```
87111

88-
// Get the forms service
89-
const formsService = loader.toFormsService()
112+
### Output format
113+
114+
If you use the default `notifyService`, the format of the email body is controlled by the `output` field in the form definition:
115+
116+
```json
117+
{
118+
"output": {
119+
"audience": "human",
120+
"version": "1"
121+
}
122+
}
90123
```
124+
125+
| Value | Description |
126+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
127+
| `audience: "human"` | Formats the submission as readable Markdown for a GOV.UK Notify email template. Default. |
128+
| `audience: "machine"` | Formats the submission as a JSON payload, base64-encoded into the Notify email body. Useful when a downstream system reads the email programmatically. |
129+
130+
`version` selects the formatter version within that audience. Currently `"1"` is the only stable version for `human`; `"1"` and `"2"` are available for `machine`. Defaults to `"1"` when omitted.
131+
132+
If you provide a custom `outputService`, the `output` field has no effect — your service controls formatting entirely.
133+
134+
> **Note:** Page events always use the `machine/v1` payload format regardless of the `output` setting. The `output` field only affects what the default `notifyService` sends.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Form definition formats
2+
3+
Form definitions are retrieved by `forms-engine-plugin` using the `formsService` plugin registration option. The plugin calls `getFormDefinition()` on every page request, which must return a JavaScript object matching the form definition schema.
4+
5+
Two approaches are available:
6+
7+
- **File-based loading** — store form definitions as YAML or JSON files in your repository and use the built-in `FileFormService`
8+
- **Custom service** — implement your own `formsService` to load definitions from an API, database, or any other source
9+
10+
## File-based loading
11+
12+
The built-in `FileFormService` loads form definitions from disk. YAML is recommended for forms with multi-line HTML content, as it natively supports block scalars. JSON is more portable but requires manually escaping quotes and line breaks in string values.
13+
14+
### Registering forms
15+
16+
```js
17+
import { FileFormService } from '@defra/forms-engine-plugin/file-form-service.js'
18+
19+
const now = new Date()
20+
const user = { id: 'user', displayName: 'Username' }
21+
const author = { createdAt: now, createdBy: user, updatedAt: now, updatedBy: user }
22+
23+
const loader = new FileFormService()
24+
25+
await loader.addForm('src/definitions/example-form.yaml', {
26+
id: '95e92559-968d-44ae-8666-2b1ad3dffd31',
27+
title: 'Example form',
28+
slug: 'example-form',
29+
organisation: 'Defra',
30+
teamName: 'Team name',
31+
teamEmail: 'team@defra.gov.uk',
32+
submissionGuidance: "Thanks for your submission, we'll be in touch",
33+
notificationEmail: 'team@defra.gov.uk',
34+
...author,
35+
live: author
36+
})
37+
38+
const formsService = loader.toFormsService()
39+
```
40+
41+
Pass the resulting `formsService` as a plugin registration option:
42+
43+
```js
44+
await server.register({
45+
plugin,
46+
options: {
47+
services: { formsService },
48+
// ...
49+
}
50+
})
51+
```
52+
53+
Call `loader.addForm()` once per form definition file. The `slug` controls the URL path — a form with `slug: 'example-form'` is served at `/example-form/*`.
54+
55+
### YAML vs JSON
56+
57+
```yaml
58+
# example-form.yaml — YAML supports multi-line content natively
59+
name: "Form name"
60+
pages:
61+
- title: "Page title"
62+
components:
63+
- type: "Html"
64+
content: |
65+
<h1 class="govuk-heading-l">Heading</h1>
66+
<p class="govuk-body">Body text</p>
67+
```
68+
69+
```jsonc
70+
// example-form.json — JSON requires escaped quotes and no newlines in strings
71+
{
72+
"name": "Form name",
73+
"pages": [
74+
{
75+
"title": "Page title",
76+
"components": [
77+
{
78+
"type": "Html",
79+
"content": "<h1 class=\"govuk-heading-l\">Heading</h1><p class=\"govuk-body\">Body text</p>"
80+
}
81+
]
82+
}
83+
]
84+
}
85+
```
86+
87+
## Custom formsService
88+
89+
To load form definitions from an API, database, or CMS, implement a custom `formsService` and pass it at plugin registration. The interface requires four methods:
90+
91+
```ts
92+
interface FormsService {
93+
getFormMetadata: (slug: string) => Promise<FormMetadata>
94+
getFormMetadataById: (id: string) => Promise<FormMetadata>
95+
getFormDefinition: (id: string, state: FormStatus) => Promise<FormDefinition | undefined>
96+
getFormSecret: (formId: string, secretName: string) => Promise<string>
97+
}
98+
```
99+
100+
`getFormMetadata` is called on every page request and should be fast. `getFormDefinition` is only called when the metadata signals the definition has changed, so it can do heavier lifting.
101+
102+
See [Custom Services](./custom-services) for a full implementation guide.

docs/features/code-based/index.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,15 @@ Automatically copy query string parameter values into hidden fields on first loa
3131
## [Save and Exit](./code-based/save-and-exit)
3232

3333
Show a secondary "Save and exit" button on question pages and handle the persisted session using a route handler you supply, enabling users to leave and resume their journey later.
34+
35+
## [Template Extensions](./code-based/template-extensions)
36+
37+
Add custom globals and filters to the Nunjucks template environment, making them available across all form page templates and LiquidJS page templates.
38+
39+
## [Form Definition Formats](./code-based/form-definition-formats)
40+
41+
Options for loading form definitions — file-based loading with the built-in `FileFormService`, or a custom `formsService` implementation for API and database sources.
42+
43+
## [Session Cache](./code-based/session-cache)
44+
45+
Configuring the server-side session store for production: named catbox cache or a custom `CacheService` subclass.

0 commit comments

Comments
 (0)