diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from docs/PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/README.md b/README.md index a013c083d..aacfadc5d 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,9 @@ It is designed to be embedded in the frontend of a digital service and provide a ## Table of Contents +- [Demo of DXT](#demo-of-dxt) - [Installation](#installation) -- [Dependencies](#dependencies) -- [Setup](#setup) - - [Form Config](#form-config) - - [Static Assets and Styles](#static-assets-and-styles) -- [Example](#example) -- [Environment Variables](#environment-variables) -- [Options](#options) - - [Services](#services) - - [Custom Controllers](#custom-controllers) - - [Custom Filters](#custom-filters) - - [Custom Cache](#custom-cache) -- [Exemplar](#exemplar) -- [Templates](#templates) - - [Template Data](#template-data) - - [Liquid Filters](#liquid-filters) - - [Examples](#examples) -- [Templates and Views: Extending the Default Layout](#templates-and-views-extending-the-default-layout) +- [Documentation](#documentation) - [Publishing the Package](#publishing-the-package) - [Semantic Versioning Control](#semantic-versioning-control) - [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 - [Workflow Triggers](#workflow-triggers) - [Safety and Consistency](#safety-and-consistency) -## Installation - -`npm install @defra/forms-engine-plugin --save` - -## Dependencies - -The following are [plugin dependencies]() that are required to be registered with hapi: - -`npm install hapi-pino @hapi/crumb @hapi/yar @hapi/vision --save` - -- [hapi-pino](https://github.com/hapijs/hapi-pino) - [Pino](https://github.com/pinojs/pino) logger for hapi -- [@hapi/crumb](https://github.com/hapijs/crumb) - CSRF crumb generation and validation -- [@hapi/yar](https://github.com/hapijs/yar) - Session manager -- [@hapi/vision](https://github.com/hapijs/vision) - Template rendering support - -Additional npm dependencies that you will need are: - -`npm install nunjucks govuk-frontend --save` - -- [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 - -`npm install @hapi/inert --save` - -- [@hapi/inert](https://www.npmjs.com/package/@hapi/inert) - static file and directory handlers for serving GOV.UK assets and styles - -## Setup - -### Form config - -The `form-engine-plugin` uses JSON configuration files to serve form journeys. -These files are called `Form definitions` and are built up of: - -- `pages` - includes a `path`, `title` -- `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/) - -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. - -TODO - Link to wiki for `Form metadata` -TODO - Link to wiki for `Form definition` - -#### Providing form config to the engine - -The engine plugin registers several [routes](https://hapi.dev/tutorials/routing/?lang=en_US) on the hapi server. - -They look like this: - -``` -GET /{slug}/{path} -POST /{slug}/{path} -``` - -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. -The [plugin registration options](#options) have a `services` setting to provide a `formsService` that is responsible for returning `form definition` data. - -WARNING: This below is subject to change - -A `formsService` has two methods, one for returning `formMetadata` and another to return `formDefinition`s. - -``` -const formsService = { - getFormMetadata: async function (slug) { - // Returns the metadata for the slug - }, - getFormDefinition: async function (id, state) { - // Returns the form definition for the given id - } -} -``` - -The reason for the two separate methods is caching. -`formMetadata` is a lightweight record designed to give top level information about a form. -This method is invoked for every page request. - -Only when the `formMetadata` indicates that the definition has changed is a call to `getFormDefinition` is made. -The response from this can be quite big as it contains the entire form definition. - -See [example](#example) below for more detail - -### Static assets and styles - -TODO - -## Example - -``` -import hapi from '@hapi/hapi' -import yar from '@hapi/yar' -import crumb from '@hapi/crumb' -import inert from '@hapi/inert' -import pino from 'hapi-pino' -import plugin from '@defra/forms-engine-plugin' - -const server = hapi.server({ - port: 3000 -}) - -// Register the dependent plugins -await server.register(pino) -await server.register(inert) -await server.register(crumb) -await server.register({ - plugin: yar, - options: { - cookieOptions: { - password: 'ENTER_YOUR_SESSION_COOKIE_PASSWORD_HERE' // Must be > 32 chars - } - } -}) - -// Register the `forms-engine-plugin` -await server.register({ - plugin -}) - -await server.start() -``` - -## Environment variables - -## Options - -The forms plugin is configured with [registration options](https://hapi.dev/api/?v=21.4.0#plugins) - -- `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) -- `filters` (optional) - A map of custom template filters to include -- `cacheName` (optional) - The cache name to use. Defaults to hapi's [default server cache]. Recommended for production. See [here] - (#custom-cache) for more details -- `viewPaths` (optional) - Include additional view paths when using custom `page.view`s -- `pluginPath` (optional) - The location of the plugin (defaults to `node_modules/@defra/forms-engine-plugin`) - -### Services - -TODO - -### Custom controllers - -TODO - -### Custom filters - -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. - -``` -const formatter = new Intl.NumberFormat('en-GB') - -await server.register({ - plugin, - options: { - filters: { - money: value => formatter.format(value), - upper: value => typeof value === 'string' ? value.toUpperCase() : value - } - } -}) -``` - -### Custom cache - -The plugin will use the [default server cache](https://hapi.dev/api/?v=21.4.0#-serveroptionscache) to store form answers on the server. -This is just an in-memory cache which is fine for development. - -In production you should create a custom cache one of the available `@hapi/catbox` adapters. - -E.g. [Redis](https://github.com/hapijs/catbox-redis) - -``` -import { Engine as CatboxRedis } from '@hapi/catbox-redis' - -const server = new Hapi.Server({ - cache : [ - { - name: 'my_cache', - provider: { - constructor: CatboxRedis, - options: {} - } - } - ] -}) -``` - -## Exemplar +## Demo of DXT TODO: Link to CDP exemplar -## Templates - -The following elements support [LiquidJS templates](https://liquidjs.com/): - -- Page **title** -- Form component **title** - - Support for fieldset legend text or label text - - This includes when the title is used in **error messages** -- Html (guidance) component **content** -- Summary component **row** key title (check answers and repeater summary) - -### Template Data - -The data the templates are evaluated against is the raw answers the user has provided up to the page they're currently on. -For example, given a YesNoField component called `TKsWbP`, the template `{{ TKsWbP }}` would render "true" or "false" depending on how the user answered the question. - -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`. - -### Liquid Filters - -There are a number of `LiquidJS` filters available to you from within the templates: - -- `page` - returns the page definition for the given path -- `field` - returns the component definition for the given name -- `href` - returns the page href for the given page path -- `answer` - returns the user's answer for a given component -- `evaluate` - evaluates and returns a Liquid template using the current context - -### Examples - -```json -"pages": [ - { - "title": "What's your name?", - "path": "/full-name", - "components": [ - { - "name": "WmHfSb", - "title": "What's your full name?", - "type": "TextField" - } - ] - }, - // This example shows how a component can use an answer to a previous question (What's your full name) in it's title - { - "title": "Are you in England?", - "path": "/are-you-in-england", - "components": [ - { - "name": "TKsWbP", - "title": "Are you in England, {{ WmHfSb }}?", - "type": "YesNoField" - } - ] - }, - // This example shows how a Html (guidance) component can use the available filters to get the form definition and user answers and display them - { - "title": "Template example for {{ WmHfSb }}?", - "path": "/example", - "components": [ - { - "title": "Html", - "type": "Html", - "content": "

- // Use Liquid's `assign` to create a variable that holds reference to the \"/are-you-in-england\" page - {%- assign inEngland = \"/are-you-in-england\" | page -%} - - // Use the reference to `evaluate` the title - {{ inEngland.title | evaluate }}
- - // Use the href filter to display the full page path - {{ \"/are-you-in-england\" | href }}
- - // Use the `answer` filter to render the user provided answer to a question - {{ 'TKsWbP' | answer }} -

\n" - } - ] - } -] -``` - -## Templates and views - -### Extending the default layout - -TODO - -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. +## Installation -For vision this is done through the `path` [plugin option](https://github.com/hapijs/vision/blob/master/API.md#options) -For nunjucks it is configured through the environment [configure options](https://mozilla.github.io/nunjucks/api.html#configure). +[See our getting started developer guide](./docs/GETTING_STARTED.md). -The `forms-engine-plugin` path to add can be imported from: +## Documentation -`import { VIEW_PATH } from '@defra/forms-engine-plugin'` +DXT has a mix of configuration-driven and code-based features that developers can utilise. -Which can then be appended to the `node_modules` path `node_modules/@defra/forms-engine`. +[See our documentation folder](./docs/INDEX.md) to learn more about the features of DXT. -The main template layout is `govuk-frontend`'s `template.njk` file, this also needs to be added to the `path`s that nunjucks can look in. +## Contributing -### Custom page view +[See our contribution guide](./docs/CONTRIBUTING.md). -## Publishing the Package +## Publishing the package 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: diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 343b140b5..9cb2c45d2 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -7,13 +7,13 @@ Thank you for considering making a contribution to DXT! Our goal is to make DXT This guide aims to set clear expectations for everyone involved in our project, to make collaborating a smooth and enjoyable experience. -# I have a question +## I have a question If you are within Department for Environment, Food & Rural Affairs, please primarily direct your questions to our Slack channel [#defra-forms-support](https://defra-digital-team.slack.com). Our team monitors this channel during working hours and will provide assistance. -# I want to request something +## I want to request something -## Reporting bugs +### Reporting bugs Report bugs on the [#defra-forms-support](https://defra-digital-team.slack.com) slack channel. If you are not a member of Defra, [submit a GitHub issue](https://github.com/DEFRA/forms-engine-plugin/issues). @@ -36,11 +36,11 @@ If your bug is with the plugin, ensure you are running the plugin in a supported - An estimated timeframe for a resolution - An update once the issue is resolved -## Suggesting features +### Suggesting features Feature suggestions are welcomed from teams within Defra Group only. Our roadmap is continually updated as new requirements emerge. Suggest new features on our [#defra-forms-support](https://defra-digital-team.slack.com) slack channel. -# I want to contribute something +## I want to contribute something All code contributed to this repository should meet the [Defra software development standards](https://defra.github.io/software-development-standards/). Our codebase, by exception, allows modification of Typescript files where appropriate. However, new code that is contributed should be Javascript with types via JSDoc, not Typescript. @@ -50,10 +50,10 @@ Our GitHub Workflows will mark each pull request with a pass/fail based on tests Draft pull requests are accepted if you are not yet finished, but would like early feedback. Pull requests that remain as a draft for over 30 days will be closed. -## Fixing bugs +### Fixing bugs If you would like to fix the bug yourself, contributions are accepted through pull requests. -## Adding features +### Adding features 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. diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 000000000..4df24b681 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,127 @@ +# Getting started with DXT + +1. [Foundational knowledge](#foundational-knowledge) +2. [Add forms-engine-plugin as a dependency](#step-1-add-forms-engine-plugin-as-a-dependency) +3. [Register DXT as a hapi plugin](#step-2-register-dxt-as-a-hapi-plugin) +4. [Handling static assets](#step-3-handling-static-assets) +5. [Environment variables](#step-4-environment-variables) +6. [Creating and loading a form](#step-5-creating-and-loading-a-form) + +## Foundational knowledge + +DXT's forms engine is a 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 DXT used as a simple presentation layer. + +You should aim, wherever possible, to utilise the existing behaviours of DXT. Our team puts a lot of effort into development, user testing and accessibility testing to ensure the forms created with DXT 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! DXT is designed to be extended, just be wise with how you spend your efforts. + +When developing with DXT, 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. + +1. Use out-of-the box DXT 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) + +### Contributing back to DXT + +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.md) to learn how to contribute this back to DXT for everyone to benefit from. + +## Step 1: Add forms-engine-plugin as a dependency + +### Installation + +`npm install @defra/forms-engine-plugin --save` + +### Dependencies + +The following are [plugin dependencies]() that are required to be registered with hapi: + +`npm install hapi-pino @hapi/crumb @hapi/yar @hapi/vision --save` + +- [hapi-pino](https://github.com/hapijs/hapi-pino) - [Pino](https://github.com/pinojs/pino) logger for hapi +- [@hapi/crumb](https://github.com/hapijs/crumb) - CSRF crumb generation and validation +- [@hapi/yar](https://github.com/hapijs/yar) - Session manager +- [@hapi/vision](https://github.com/hapijs/vision) - Template rendering support + +Additional npm dependencies that you will need are: + +`npm install nunjucks govuk-frontend --save` + +- [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 + +`npm install @hapi/inert --save` + +- [@hapi/inert](https://www.npmjs.com/package/@hapi/inert) - static file and directory handlers for serving GOV.UK assets and styles + +## Step 2: Register DXT as a hapi plugin + +```javascript +import plugin from '@defra/forms-engine-plugin' +await server.register({ + plugin, + options: { + // if applicable + } +}) +``` + +Full example: + +```javascript +import hapi from '@hapi/hapi' +import yar from '@hapi/yar' +import crumb from '@hapi/crumb' +import inert from '@hapi/inert' +import pino from 'hapi-pino' +import plugin from '@defra/forms-engine-plugin' + +const server = hapi.server({ + port: 3000 +}) + +// Register the dependent plugins +await server.register(pino) +await server.register(inert) +await server.register(crumb) +await server.register({ + plugin: yar, + options: { + cookieOptions: { + password: 'ENTER_YOUR_SESSION_COOKIE_PASSWORD_HERE' // Must be > 32 chars + } + } +}) + +// Register the `forms-engine-plugin` +await server.register({ + plugin +}) + +await server.start() +``` + +## Step 3: Handling static assets + +TODO + +## Step 4: Environment variables + +TODO + +## Step 5: Creating and loading a form + +Forms in DXT are represented by a JSON configuration file. The configuration defines several top-level elements: + +The `form-engine-plugin` uses JSON configuration files to serve form journeys. +These files are called `Form definitions` and are built up of: + +- `pages` - includes a `path`, `title` +- `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/) + +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. + +TODO - Link to wiki for `Form metadata` + +TODO - Link to wiki for `Form definition` diff --git a/docs/INDEX.md b/docs/INDEX.md new file mode 100644 index 000000000..460a4813f --- /dev/null +++ b/docs/INDEX.md @@ -0,0 +1,12 @@ +# DXT documentation + +- [Getting started with DXT](./GETTING_STARTED.md) +- [Plugin registration options](./PLUGIN_OPTIONS.md) +- Configuration-based features + - [Page templates (dynamic content)](./features/configuration-based/PAGE_TEMPLATES.md) + - [Page events (triggering an action on an event)](./features/configuration-based/PAGE_EVENTS.md) +- Code-based features + - [Page views (customisable views with Nunjucks)](./features/code-based/PAGE_VIEWS.md) + - [Custom Nunjucks/liquid filters](./PLUGIN_OPTIONS.md#custom-filters) + - [Custom services (modifying the out-of-the-box behaviour of DXT)](./features/code-based/CUSTOM_SERVICES.md) +- [Contributing to DXT](./CONTRIBUTING.md) diff --git a/docs/PLUGIN_OPTIONS.md b/docs/PLUGIN_OPTIONS.md new file mode 100644 index 000000000..f5ef14dd2 --- /dev/null +++ b/docs/PLUGIN_OPTIONS.md @@ -0,0 +1,66 @@ +# Plugin options + +The forms plugin is configured with [registration options](https://hapi.dev/api/?v=21.4.0#plugins) + +- `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) +- `filters` (optional) - A map of custom template filters to include +- `cacheName` (optional) - The cache name to use. Defaults to hapi's [default server cache]. Recommended for production. See [here] + (#custom-cache) for more details +- `viewPaths` (optional) - Include additional view paths when using custom `page.view`s +- `pluginPath` (optional) - The location of the plugin (defaults to `node_modules/@defra/forms-engine-plugin`) + +## Services + +TODO + +## Custom controllers + +TODO + +## Custom filters + +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. + +``` +const formatter = new Intl.NumberFormat('en-GB') + +await server.register({ + plugin, + options: { + filters: { + money: value => formatter.format(value), + upper: value => typeof value === 'string' ? value.toUpperCase() : value + } + } +}) +``` + +## Custom cache + +The plugin will use the [default server cache](https://hapi.dev/api/?v=21.4.0#-serveroptionscache) to store form answers on the server. +This is just an in-memory cache which is fine for development. + +In production you should create a custom cache one of the available `@hapi/catbox` adapters. + +E.g. [Redis](https://github.com/hapijs/catbox-redis) + +``` +import { Engine as CatboxRedis } from '@hapi/catbox-redis' + +const server = new Hapi.Server({ + cache : [ + { + name: 'my_cache', + provider: { + constructor: CatboxRedis, + options: {} + } + } + ] +}) +``` diff --git a/docs/features/code-based/CUSTOM_SERVICES.md b/docs/features/code-based/CUSTOM_SERVICES.md new file mode 100644 index 000000000..a83c65697 --- /dev/null +++ b/docs/features/code-based/CUSTOM_SERVICES.md @@ -0,0 +1,38 @@ +# Overriding DXT logic with custom services + +## Customising where forms are loaded from + +The engine plugin registers several [routes](https://hapi.dev/tutorials/routing/?lang=en_US) on the hapi server. + +They look like this: + +``` +GET /{slug}/{path} +POST /{slug}/{path} +``` + +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. + +The [plugin registration options](../../PLUGIN_OPTIONS.md) have a `services` setting to provide a `formsService` that is responsible for returning `form definition` data. + +WARNING: This below is subject to change + +A `formsService` has two methods, one for returning `formMetadata` and another to return `formDefinition`s. + +```javascript +const formsService = { + getFormMetadata: async function (slug) { + // Returns the metadata for the slug + }, + getFormDefinition: async function (id, state) { + // Returns the form definition for the given id + } +} +``` + +The reason for the two separate methods is caching. +`formMetadata` is a lightweight record designed to give top level information about a form. +This method is invoked for every page request. + +Only when the `formMetadata` indicates that the definition has changed is a call to `getFormDefinition` is made. +The response from this can be quite big as it contains the entire form definition. diff --git a/docs/features/code-based/PAGE_VIEWS.md b/docs/features/code-based/PAGE_VIEWS.md new file mode 100644 index 000000000..481d8b067 --- /dev/null +++ b/docs/features/code-based/PAGE_VIEWS.md @@ -0,0 +1,22 @@ +# Templates and views + +## Extending the default layout + +TODO + +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. + +For vision this is done through the `path` [plugin option](https://github.com/hapijs/vision/blob/master/API.md#options) +For nunjucks it is configured through the environment [configure options](https://mozilla.github.io/nunjucks/api.html#configure). + +The `forms-engine-plugin` path to add can be imported from: + +`import { VIEW_PATH } from '@defra/forms-engine-plugin'` + +Which can then be appended to the `node_modules` path `node_modules/@defra/forms-engine`. + +The main template layout is `govuk-frontend`'s `template.njk` file, this also needs to be added to the `path`s that nunjucks can look in. + +## Using page views with data from your own API + +Page templates have access to `{{ context.data }}`, which is an attribute made available when a page event is triggered. It represents the entire response body from your API. To learn more about this, [see our guidance on page events](./PAGE_EVENTS.md). diff --git a/docs/features/configuration-based/PAGE_EVENTS.md b/docs/features/configuration-based/PAGE_EVENTS.md new file mode 100644 index 000000000..d3836a5d8 --- /dev/null +++ b/docs/features/configuration-based/PAGE_EVENTS.md @@ -0,0 +1,144 @@ +# Page events + +Page events are a configuration-based way of triggering an action on an event trigger. For example, when a page loads, call an API and retrieve the data from it. + +DXT's forms engine is a frontend service, which should remain as lightweight as possible with business logic being implemented in a backend/BFF API. Using page events, DXT can call your API and use the tailored response downstream, such a page templates to display the response value. + +The downstream API response becomes available under the `{{ context.data }}` view model attribute for view templates, so it can be used when rendering a page. This attribute is directly accessible by our [page templates](./../configuration-based/PAGE_TEMPLATES.md) feature and our Nunjucks-based views. + +## Architecture + +DXT will call any API of your choosing, so ultimately the architecture is up to you. As long as that API accepts the DXT payload, returns HTTP 200 and returns a valid JSON document as the response body, you will be able to use page events. + +Our recommendation is that you create a lightweight backend service called a "BFF" (backend for frontend). This is a common pattern that allows you to decouple your backend service from the frontend implementation, allowing you to tailor your existing backend API to a different frontend. To learn more about this pattern, see [Microsoft's guide](https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends). + +![Architecture diagram showing the usage of a frontend, a BFF, and a backend API interacting with each other](images/page-events-architecture.png) + +If DXT is the only consumer of your API, it may make sense to omit the BFF and have DXT directly call your backend. + +## Setting up a page event + +A page event is configured by defining the event trigger, then the action configuration. For example, to call an API on page load: + +```json +{ + "onLoad": { + "type": "http", + "options": { + "url": "https://my-api.defra.gov.uk" + } + } +} +``` + +See [supported events](#supported-events) to learn more about the supported triggers and actions. + +## Supported events + +### Supported triggers + +Currently supported event types: + +- `onLoad`: Called on load of a page (e.g. the initial GET request to load the page) + +Planned event types: + +- `onSave`: Called on save of a page, after the data has been validated by DXT. For example, when a user presses "Continue", which triggers a POST request. + +### Supported actions + +- `http`: Makes a HTTP(S) call to a web service. This service must be routable on DXT (e.g. by configuring CDP's squid proxy), must accept DXT's standardised payload, return HTTP 200 and a valid JSON document. + - Options: + - `url`: A fully formed HTTP(S) URL, e.g. `https://my-api.defra.gov.uk` or `https://my-api.prod.cdp-int.defra.cloud` + +## Payload + +DXT sends a standardised payload to each API configured with page events. The latest version of our payload [can be found in our outputFormatters module by opening the latest version, e.g. `v2.ts`](https://github.com/DEFRA/forms-engine-plugin/tree/main/src/server/plugins/engine/outputFormatters/machine). Our payload contains some metadata about the payload, along with a "data" section that contains the main body of the form as a JSON object, an array of repeatable pages, and a file ID and download link for all files submitted. + +As of 2025-03-25, the payload would look something like this: + +```jsonc +{ + "meta": { + "schemaVersion": "2", + "timestamp": "2025-03-25T10:00:00Z", + "definition": { + // This object would be a full copy of the form definition at the time of submission. It is excluded for brevity. + } + }, + "data": { + "main": { + "componentName": "componentValue", + "richComponentName": { "foo": "bar", "baz": true } + }, + "repeaters": { + "repeaterName": [ + { + "textComponentName": "componentValue" + }, + { + "richComponentName": { "foo": "bar", "baz": true } + } + ] + }, + "files": { + "fileComponentName": [ + { + "fileId": "123-456-789", + "link": "https://forms-designer/file-download/123-456-789" + } + ] + } + } +} +``` + +## Using the response from your API in DXT + +If the API call is successful, the JSON response your API returns will be attached to the page `context` under the `data` attribute. Liquid/Nunjucks can then use this data however it would normally use variables, for example: + +Your API response: + +```json +{ + "awardedGrantValue": "150" +} +``` + +Page template: + +```jinja2 +{% if context.data.awardedGrantValue %} +

Congratulations. You are likely to receive up to £{{ context.data.awardedGrantValue }}.

+{% endif %} +

You have not been awarded any funding for this application.

+{% endif %} +``` + +Results in: + +```jinja2 +

You have been awarded £150.

+``` + +## Authenticating a HTTP request from DXT in your API + +This is not currently supported, but is planned. If you cannot wait for page events to support authentication, we recommend you create a page controller using Javascript which can implement your unique authentication requirements. + +DXT plans to send a signature in the headers, similar to a JWT, that would allow teams to verify that an incoming payload is indeed from DXT. It would look something like this: + +``` +POST https://my-api.defra.gov.uk/page-submit` +{ + "headers": { + "X-Signature": "9d17656adf3654f2579fb49b7dcb53556df08e166cad84f8646a497fc75746bd" + }, + "payload": { + // See the page event payload schema for details. + } +} +``` + +The signature would be verified by comparing it against the payload and DXT's public key. A successful verification would mean the request originated from DXT and can be trusted. + +See our [current sample implementation](https://github.com/alexluckett/js-signed-webhooks-demo/blob/master/index.js) to understand this in practice. diff --git a/docs/features/configuration-based/PAGE_TEMPLATES.md b/docs/features/configuration-based/PAGE_TEMPLATES.md new file mode 100644 index 000000000..405567181 --- /dev/null +++ b/docs/features/configuration-based/PAGE_TEMPLATES.md @@ -0,0 +1,154 @@ +# Page templates + +Page templates are a configuration-based way of adding dynamic content to the form UI, such as displaying the answer to a question, or some data from your API. This feature is only used for presentation purposes. + +Certain elements of your form, such as content blocks or page titles, allow for the use of LiquidJS. LiquidJS is a templating engine that runs alongside Nunjucks to dynamically insert data into the rendered page. + +A simple example of page templates is to print the answer to a previous question. + +For example: + +```json +{ + "title": "What's your name?", + "path": "/full-name", + "components": [ + { + "name": "applicantFullName", + "title": "What's your full name?", + "type": "TextField" + } + ] + }, + { + "path": "/greeting-page", + "title": "Hello, {{ applicantFullName }}" + }, +``` + +The above snippet would ask the user for their full name, e.g. "Joe Bloggs". The following page would be titled "Hello, Joe Bloggs". + +## Why LiquidJS? + +Nunjucks is the templating library of choice for frontends within Defra. Nunjucks templates are ultimately compiled down into Javascript code and executed, which opens the risk of user-defined code being executed. + +LiquidJS is a safe alternative that provides very similar functionality to Nunjucks, but is not passed to `eval` or invoked as a function. This allows us to insert dynamic content into our forms using our configuration files, rather than requiring each team to hardcode it into the codebase. + +The core codebase for DXT uses Nunjucks. LiquidJS is only used from the form definition JSON files. + +## Where page templates are supported + +The following elements support [LiquidJS templates](https://liquidjs.com/): + +- Page **title** + - jq path: `.title` +- Form component **title** + - jq path: `.pages[].components[].title` + - Support for fieldset legend text or label text + - This includes when the title is used in **error messages** +- Html (guidance) component **content** + - jq path: `.pages[].components[].content` +- Summary component **row** key title (check answers and repeater summary) + - Derived from component title + +## Template data + +The data the templates are evaluated against is the raw answers the user has provided up to the page they're currently on. +For example, given a YesNoField component called `TKsWbP`, the template `{{ TKsWbP }}` would render "true" or "false" depending on how the user answered the question. + +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`. + +Templates should be single line JSON strings, where line breaks are not rendered and are defined as `\n`. Our recommendation is that template strings are edited separately to the form JSON, before being minified and copied into the JSON. + +## DXT filters + +There are a number of filters available to you from within the templates: + +- `page` - returns the page definition for the given path +- `field` - returns the component definition for the given name +- `href` - returns the page href for the given page path +- `answer` - returns the user's answer for a given component +- `evaluate` - evaluates and returns a Liquid template using the current context + +## Examples + +### Substituting a page title + +Below is what a form may look like using page templates. It asks the user for their full name, then renders the following page with their name in the title. For example, "Are you in England, Joe Bloggs?". + +```jsonc +"pages": [ + { + "title": "What's your name?", + "path": "/full-name", + "components": [ + { + "name": "WmHfSb", + "title": "What's your full name?", + "type": "TextField" + } + ] + }, + { + // This example shows how a page/component can use an answer to a previous question (What's your full name) in its title + "title": "Are you in England, {{ WmHfSb }}?", + "path": "/are-you-in-england", + "components": [ + { + "name": "TKsWbP", + "title": "Are you in England, {{ WmHfSb }}?", + "type": "YesNoField" + } + ] + } +] +``` + +### Fully dynamic HTML snippets + +You may want to do more than just add a dynamic value to the page title. Using the below example, you can create an entire HTML snippet and add it to a page. + +Here is an example of a Liquid template that renders a page title, displays a link to a page called "are you in england" and prints out the answer to a question. + +```jinja2 +

+ {# Use Liquid's `assign` to create a variable that holds reference to the \"/are-you-in-england\" page #} + {%- assign inEngland = "/are-you-in-england" | page -%} + + {# Use the reference to `evaluate` the title #} + {{ inEngland.title | evaluate }}
+ + {# Use the href filter to display the full page path #} + {{ "/are-you-in-england" | href }}
+ + {# Use the `answer` filter to render the user provided answer to a question #} + {{ 'TKsWbP' | answer }} +

+``` + +The above template should be minified and inserted into the content field in the form definition example. To make it valid JSON, quotes should be either replaced with `'` or escaped `\"`. Your IDE should do this automatically when pasting the into a JSON string, or a tool like https://www.freeformatter.com/json-escape.html can do it manually. + +Full example of the minified and escaped component, which can be appended to [the first example's JSON snippet](#substituting-a-page-title). + +```jsonc +{ + // This example shows how a Html (guidance) component can use the available filters to get the form definition and user answers and display them + "title": "Template example for {{ WmHfSb }}?", + "path": "/example", + "components": [ + { + "title": "Html", + "type": "Html", + "content": "

\r\n {# Use Liquid's `assign` to create a variable that holds reference to the \\\"\/are-you-in-england\\\" page #}\r\n {%- assign inEngland = \"\/are-you-in-england\" | page -%}\r\n\r\n {# Use the reference to `evaluate` the title #}\r\n {{ inEngland.title | evaluate }}
\r\n\r\n {# Use the href filter to display the full page path #}\r\n {{ \"\/are-you-in-england\" | href }}
\r\n\r\n {# Use the `answer` filter to render the user provided answer to a question #}\r\n {{ 'TKsWbP' | answer }}\r\n<\/p>" + } + ] +} +``` + +## Providing your own filters + +Whilst DXT offers some out of the box filters, teams using the plugin have the capability to provide their own. See [PLUGIN_OPTIONS.md](../../PLUGIN_OPTIONS.md#custom-filters) for more information. + +## Using page templates with data from your own API + +Page templates have access to `{{ context.data }}`, which is an attribute made available when a page event is triggered. It represents the entire response body from your API. To learn more about this, [see our guidance on page events](./PAGE_EVENTS.md). diff --git a/docs/features/configuration-based/images/page-events-architecture.png b/docs/features/configuration-based/images/page-events-architecture.png new file mode 100644 index 000000000..d88e30b2f Binary files /dev/null and b/docs/features/configuration-based/images/page-events-architecture.png differ