Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
cd26718
chore(docs): add component/page doc dirs to gitignore, update build s…
alexluckett May 1, 2026
cbc3efd
docs: add component and page metadata for doc generation
alexluckett May 1, 2026
807aa96
feat(docs): add component and page documentation generator script
alexluckett May 1, 2026
e9f6dd0
fix(docs): add missing date field options, warning for missing interf…
alexluckett May 1, 2026
0030466
feat(docs): add Components and Page Types nav sections, mark advanced…
alexluckett May 1, 2026
43199ac
docs: update features index, add Advanced callouts to configuration-b…
alexluckett May 1, 2026
f5c28ee
fix(docs): simplify redundant nested titles in schema docs generation
alexluckett May 1, 2026
1f25e4c
fix(docs): remove component/page sub-items from Features sidebar
alexluckett May 1, 2026
c1a2731
chore(docs): run component generator before docs:dev
alexluckett May 1, 2026
e59786d
fix(docs): use .md extensions in index links so Docusaurus resolves t…
alexluckett May 1, 2026
3547d1c
docs: remove SummaryPageWithConfirmationEmail and Status page types
alexluckett May 1, 2026
178890a
docs: add Advanced heading to features page and sidebar
alexluckett May 1, 2026
ccaa036
docs: remove TypeScript mention from code-based advanced callout
alexluckett May 1, 2026
addccf4
docs: remove Advanced callouts from configuration-based and code-base…
alexluckett May 1, 2026
42c93f8
docs: move payment/geospatial under Input fields as subheadings, fix …
alexluckett May 1, 2026
cc55ce2
refactor(docs): generate all component data from forms-model types, m…
alexluckett May 1, 2026
520dbd5
fix(docs): correct relative links in features index
alexluckett May 1, 2026
d1c8c43
fix(docs): use absolute URL paths in features index to avoid Docusaur…
alexluckett May 1, 2026
478227f
fix(docs): add href to Advanced sidebar group to prevent Layout crash
alexluckett May 1, 2026
968e93d
feat(docs): include options/schema structure in generated examples, f…
alexluckett May 1, 2026
6fd6cd0
refactor(docs): derive page controller structure from forms-model types
alexluckett May 1, 2026
a151950
fix(docs): harden component doc generator and add unit tests
alexluckett May 1, 2026
9ee4f13
refactor(docs): derive all doc structure from forms-model types, add …
alexluckett May 1, 2026
65a2a72
fix(docs): use relative links in features index to fix base URL routing
alexluckett May 1, 2026
6f9484a
fix(docs): fix features index links, add file upload and page cross-r…
alexluckett May 1, 2026
02d0584
docs: replace bullet-point option list with table in plugin-options.md
alexluckett May 1, 2026
b974053
docs: write custom controllers section in plugin-options.md
alexluckett May 1, 2026
c4c4bfd
docs: add js syntax highlighting to Custom filters and Custom cache c…
alexluckett May 1, 2026
6bec930
docs: capitalise Nunjucks in section heading and table link
alexluckett May 1, 2026
8af4414
fix(docs): detect list/content props inherited from base interfaces
alexluckett May 1, 2026
112a096
fix(docs): resolve full interface inheritance chain in component parser
alexluckett May 1, 2026
ca071cc
fix(docs): replace hasList/hasContent flags with generic top-level pr…
alexluckett May 1, 2026
c9062b0
docs: link list property to schema page, add propertyExamples for mea…
alexluckett May 1, 2026
81265cb
revert: remove propertyExamples, keep simple placeholderForType
alexluckett May 1, 2026
f072fe0
docs(scripts): rewrite JSDoc on TS parsing functions to explain why t…
alexluckett May 1, 2026
7b2ff46
refactor(scripts): hoist UNIVERSAL, extract parseSourceFile, drop nex…
alexluckett May 1, 2026
19db580
fix(scripts): reduce complexity of simplifyNestedTitles, fix bare ret…
alexluckett May 1, 2026
c78f2f4
fix(scripts): add JSDoc types to satisfy strict tsc, reduce recurseSc…
alexluckett May 1, 2026
d3a345c
fix(docs): make all getting-started sidebar items consistent links
alexluckett May 1, 2026
0c934e0
docs(getting-started): simplify and tighten prose
alexluckett May 1, 2026
7932c8c
fix(docs): correct code bugs and add missing SESSION_COOKIE_PASSWORD
alexluckett May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ package
.env
tsconfig.tsbuildinfo
docs/schemas
docs/features/components
docs/features/pages
temp-schemas

# Docusaurus
Expand Down
16 changes: 13 additions & 3 deletions docs/features/index.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
# Features

forms-engine-plugin provides two categories of features to help you extend and customise your form journeys beyond the out-of-the-box behaviour.
forms-engine-plugin provides built-in components and page types you can use immediately in your form definitions, as well as advanced features for driving dynamic behaviour or writing custom code.

## [Configuration-based Features](./features/configuration-based)
## [Components](./features/components)

A library of built-in form components — text fields, date inputs, radio buttons, file upload, payment, geospatial fields, and more. Add them to your form definition by name.

## [Page Types](./features/pages)

Built-in page controllers that define how a page behaves — question pages, repeating groups, file upload pages, summary and confirmation pages.

## Advanced

### [Configuration-based Features](./features/configuration-based)

Drive advanced functionality — such as calling APIs and rendering dynamic content — entirely through form definitions, with no custom code required.

## [Code-based Features](./features/code-based)
### [Code-based Features](./features/code-based)

Implement highly tailored behaviour by writing custom TypeScript/JavaScript that integrates with forms-engine-plugin's extension points.
64 changes: 45 additions & 19 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@

## Foundational knowledge

forms-engine-plugin is a hapi plugin for a frontend service, which allows development teams to construct forms using configuration and minimal code. Forms are closely based on the knowledge, components and patterns from the GDS Design System. Forms should remain as lightweight as possible, with business logic being implemented in a backend/BFF API and forms-engine-plugin used as a simple presentation layer.
forms-engine-plugin is a hapi plugin that lets teams build GOV.UK forms using configuration and minimal code, based on GDS Design System patterns. Keep business logic in a backend/BFF API and treat the plugin as a thin presentation layer.

You should aim, wherever possible, to utilise the existing behaviours of forms-engine-plugin. Our team puts a lot of effort into development, user testing and accessibility testing to ensure the forms created with forms-engine-plugin will be of a consistently high quality. Where your team introduces custom behaviour, such as custom components or custom pages, this work will now need to be done by your team. Where possible, favour fixing something upstream in the plugin so many teams can benefit from the work we do. Then, if you still need custom behaviour - go for it! forms-engine-plugin is designed to be extended, just be wise with how you spend your efforts.
Prefer built-in behaviour wherever possible — it's been developed, user-tested, and accessibility-tested to a consistent standard. If you need something the plugin doesn't support, consider fixing it upstream so all teams benefit. When custom code is genuinely the right call, the plugin is designed to be extended.

When developing with forms-engine-plugin, you should favour development using the below priority order. This will ensure your team is writing the minimum amount of code, focusing your efforts on custom code where the requirements are niche and there is value.
Favour this priority order:

1. Use out-of-the box forms-engine-plugin components and page types (components, controllers)
2. Use configuration-driven advanced functionality to integrate with backends and dynamically change page content (page events, page templates)
3. Use custom views, custom components and page controllers to implement highly tailored and niche logic (custom Nunjucks, custom Javascript)
1. Built-in components and page types
2. Configuration-driven features (page events, templates) to integrate with backends
3. Custom views, components, and controllers for niche requirements

### Contributing back to forms-engine-plugin
### Contributing back to forms-engine-plugin <!-- no-sidebar -->

When you build custom components and page controllers, they might be useful for other teams in Defra to utilise. For example, many teams collect CPH numbers but have no way to validate it's correct. Rather than creating a new CPH number component and letting it sit in your codebase for just your team, see our [contribution guide](./contributing) to learn how to contribute this back to forms-engine-plugin for everyone to benefit from.
Custom components you build may be useful to other Defra teams. See the [contribution guide](./contributing) to share them upstream rather than keeping them in your own codebase.

## Step 1: Add forms-engine-plugin as a dependency

### Installation
### Installation <!-- no-sidebar -->

`npm install @defra/forms-engine-plugin --save`

### Dependencies
### Dependencies <!-- no-sidebar -->

The following are [plugin dependencies](<https://hapi.dev/api/?v=21.4.0#server.dependency()>) that are required to be registered with hapi:

Expand All @@ -40,7 +40,7 @@ Additional npm dependencies that you will need are:
- [nunjucks](https://www.npmjs.com/package/nunjucks) - [templating engine](https://mozilla.github.io/nunjucks/) used by GOV.UK design system
- [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

Optional dependencies
### Optional dependencies <!-- no-sidebar -->

`npm install @hapi/inert --save`

Expand All @@ -67,11 +67,14 @@ await server.register({
Full example:

```javascript
import { join } from 'node:path'
import hapi from '@hapi/hapi'
import vision from '@hapi/vision'
import yar from '@hapi/yar'
import crumb from '@hapi/crumb'
import inert from '@hapi/inert'
import pino from 'hapi-pino'
import nunjucks from 'nunjucks'
import plugin from '@defra/forms-engine-plugin'

const server = hapi.server({
Expand All @@ -93,6 +96,23 @@ await server.register({

const paths = [join(config.get('appDir'), 'views')]

await server.register({
plugin: vision,
options: {
engines: {
html: {
compile(path, { environment }) {
return (context) => nunjucks.compile(path, environment).render(context)
}
}
},
path: paths,
compileOptions: {
environment: nunjucks.configure(paths)
}
}
})

// Register the `forms-engine-plugin`
await server.register({
plugin,
Expand Down Expand Up @@ -120,8 +140,8 @@ await server.register({
const user = await userService.getUser(request.auth.credentials)

return {
"greeting": "Hello" // available to render on a nunjucks page as {{ greeting }}
"username": user.username // available to render on a nunjucks page as {{ username }}
greeting: 'Hello', // available to render on a nunjucks page as {{ greeting }}
username: user.username // available to render on a nunjucks page as {{ username }}
}
}
}
Expand All @@ -134,7 +154,7 @@ await server.start()

1. Import forms-engine-plugin's styling

If you are using the CDP templates, you just need to ensure your `src/client/stylesheets/application.scss` file contains:
If you are on CDP, ensure your `src/client/stylesheets/application.scss` file contains:

```sass
@use "pkg:@defra/forms-engine-plugin";
Expand All @@ -156,6 +176,12 @@ Example: https://github.com/DEFRA/forms-runner/blob/24c5623946cdfddca593bcba8a68

## Step 5: Environment variables

The following variable is always required:

```shell
SESSION_COOKIE_PASSWORD=your-secret-password-at-least-32-chars
```

Blocks marked with `# FEATURE: <name>` are optional and can be omitted if the feature is not used.

```shell
Expand Down Expand Up @@ -184,20 +210,20 @@ GOOGLE_ANALYTICS_TRACKING_ID='12345'

## Step 6: Creating and loading a form

Forms in forms-engine-plugin are represented by a configuration object called a "form definition". The form definition can be stored in a location and format of your choosing by providing a `formsService` as a registration option. If you are using our 'loader' pattern as recommended in step 2, you will likely be writing YAML or JSON files in your repository as files.
Forms in forms-engine-plugin are represented by a configuration object called a "form definition". The form definition can be stored in a location and format of your choosing by providing a `formsService` as a registration option. If you are using our 'loader' pattern as recommended in step 2, you will likely be writing YAML or JSON files in your repository.

Our examples primarily use JSON. If you are using YAML, simply convert the data structure from JSON to YAML and the examples will still work.

The configuration defines several top-level elements:

- `pages` - includes a `path`, `title`
- `pages` - the form journey, each representing a single web page with a path, title, and components
- `components` - one or more questions on a page
- `conditions` - used to conditionally show and hide pages and
- `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/)
- `conditions` - used to conditionally show and hide pages
- `lists` - data used 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/)

To understand the full set of options available to you, consult our [schema documentation](https://defra.github.io/forms-engine-plugin/schemas/). Specifically, the [form definition schema](https://defra.github.io/forms-engine-plugin/schemas/form-definition-v2-payload-schema).

### Config
### Config <!-- no-sidebar -->

#### Pages

Expand Down
108 changes: 81 additions & 27 deletions docs/plugin-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,25 @@

The forms plugin is configured with [registration options](https://hapi.dev/api/?v=21.4.0#plugins)

## Required options

- `nunjucks` (required) - Template engine configuration. See [nunjucks configuration](#nunjucks-configuration)
- `viewContext` (required) - A function that provides global context to all templates. See [viewContext](#viewcontext)
- `baseUrl` (required) - Base URL of the application (protocol and hostname, e.g., `"https://myservice.gov.uk"`). Used for generating absolute URLs in markdown rendering and other contexts

## Optional options

- `model` (optional) - Pre-built `FormModel` instance. When provided, the plugin serves a single static form definition. When omitted, forms are loaded dynamically via `formsService`. See [model](#model)
- `services` (optional) - object containing `formsService`, `formSubmissionService` and `outputService`
- `formsService` - used to load `formMetadata` and `formDefinition`
- `formSubmissionService` - used prepare the form during submission (ignore - subject to change)
- `outputService` - used to save the submission
- `controllers` (optional) - Object map of custom page controllers used to override the default. See [custom controllers](#custom-controllers)
- `globals` (optional) - A map of custom template globals to include. See [custom globals](#custom-globals)
- `filters` (optional) - A map of custom template filters to include. See [custom filters](#custom-filters)
- `cache` (optional) - Caching options. Recommended for production. This can be either:
- a string representing the cache name to use (e.g. hapi's default server cache). See [custom cache](#custom-cache) for more details.
- a custom `CacheService` instance implementing your own caching logic
- `pluginPath` (optional) - The location of the plugin (defaults to `node_modules/@defra/forms-engine-plugin`)
- `preparePageEventRequestOptions` (optional) - A function that will be invoked for http-based [page events](./features/configuration-based/page-events). See [here](./features/configuration-based/page-events#authenticating-a-http-page-event-request-from-forms-engine-plugin-in-your-api) for details
- `saveAndExit` (optional) - Configuration for custom session management including key generation, session hydration, and persistence. See [save and exit documentation](./features/code-based/save-and-exit) for details
- `onRequest` (optional) - A function that will be invoked on each request to any form route e.g `/{slug}/{path}`. See [onRequest](#onrequest) for more details
## Options

| Option | Required | Description |
| -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `nunjucks` | Yes | Template engine configuration. See [Nunjucks configuration](#nunjucks-configuration) |
| `viewContext` | Yes | A function that provides global context to all templates. See [viewContext](#viewcontext) |
| `baseUrl` | Yes | Base URL of the application (protocol and hostname, e.g. `"https://myservice.gov.uk"`). Used for generating absolute URLs in markdown rendering and other contexts |
| `model` | No | Pre-built `FormModel` instance. When provided, the plugin serves a single static form definition. When omitted, forms are loaded dynamically via `formsService`. See [model](#model) |
| `services` | No | Object containing `formsService`, `formSubmissionService`, and `outputService`. See [services](#services) |
| `controllers` | No | Object map of custom page controllers used to override the default. See [custom controllers](#custom-controllers) |
| `globals` | No | A map of custom template globals to include. See [custom globals](#custom-globals) |
| `filters` | No | A map of custom template filters to include. See [custom filters](#custom-filters) |
| `cache` | No | Caching options. Recommended for production — either a cache name string or a custom `CacheService` instance. See [custom cache](#custom-cache) |
| `pluginPath` | No | The location of the plugin. Defaults to `node_modules/@defra/forms-engine-plugin` |
| `preparePageEventRequestOptions` | No | A function invoked for HTTP-based [page events](./features/configuration-based/page-events) to customise outbound request options |
| `saveAndExit` | No | Configuration for custom session management. See [save and exit](./features/code-based/save-and-exit) |
| `onRequest` | No | A function invoked on each request to any form route (e.g. `/{slug}/{path}`). See [onRequest](#onrequest) |
| `ordnanceSurveyApiKey` | No | Ordnance Survey API key. Required to enable the inline map on geospatial components. See [geospatial map](#geospatial-map) |
| `ordnanceSurveyApiSecret` | No | Ordnance Survey API secret. Required alongside `ordnanceSurveyApiKey` to enable the inline map on geospatial components |

## Option details

Expand All @@ -34,9 +30,50 @@ See [our services documentation](./features/code-based/custom-services).

### Custom controllers

TODO
The `controllers` option lets you register custom page controller classes that extend the built-in `PageController`. A custom controller is tied to a page in your form definition by setting the page's `controller` property to the key you register it under.

### nunjucks configuration
```ts
import { PageController } from '@defra/forms-engine-plugin/controllers/PageController.js'
import { type FormModel } from '@defra/forms-engine-plugin/types'
import { type Page } from '@defra/forms-model'

class ConfirmationPageController extends PageController {
constructor(model: FormModel, pageDef: Page) {
super(model, pageDef)
}

makeGetRouteHandler() {
return async (request, h) => {
// custom logic before rendering
return h.view(this.viewName, { ...await this.getViewModel(request) })
}
}
}

await server.register({
plugin,
options: {
controllers: {
ConfirmationPageController
}
}
})
```

In your form definition, set the `controller` property of any page to the same key:

```json
{
"path": "/confirmation",
"title": "Confirmation",
"controller": "ConfirmationPageController",
"components": []
}
```

When the engine instantiates pages, it first checks for a matching built-in controller, then falls back to the `controllers` map. If no match is found the default `PageController` is used.

### Nunjucks configuration

The `nunjucks` option is required and configures the template engine paths and layout.

Expand Down Expand Up @@ -184,7 +221,7 @@ In your templates:
Use the `filter` plugin option to provide custom template filters.
Filters are available in both [nunjucks](https://mozilla.github.io/nunjucks/templating.html#filters) and [liquid](https://liquidjs.com/filters/overview.html) templates.

```
```js
const formatter = new Intl.NumberFormat('en-GB')

await server.register({
Expand All @@ -207,7 +244,7 @@ In production you should create a custom cache one of the available `@hapi/catbo

E.g. [Redis](https://github.com/hapijs/catbox-redis)

```
```js
import { Engine as CatboxRedis } from '@hapi/catbox-redis'

const server = new Hapi.Server({
Expand Down Expand Up @@ -288,3 +325,20 @@ await server.register({
```

For detailed documentation and examples, see [Save and Exit](./features/code-based/save-and-exit).

### Geospatial map

Geospatial components ([Easting Northing](./features/components/easting-northing-field.md), [OS Grid Ref](./features/components/os-grid-ref-field.md), [National Grid Field Number](./features/components/national-grid-field-number-field.md), [Lat Long](./features/components/lat-long-field.md), [Geospatial](./features/components/geospatial-field.md)) render an inline Ordnance Survey map alongside their input fields. The map lets users click a location to auto-populate the coordinate inputs, rather than typing values manually.

The map is only activated when both `ordnanceSurveyApiKey` and `ordnanceSurveyApiSecret` are provided at plugin registration. Without them the components still function as plain text inputs.

```js
await server.register({
plugin,
options: {
ordnanceSurveyApiKey: process.env.ORDNANCE_SURVEY_API_KEY,
ordnanceSurveyApiSecret: process.env.ORDNANCE_SURVEY_API_SECRET,
// ... other options
}
})
```
Loading
Loading