Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const paths = [join(config.get('appDir'), 'views')]
await server.register({
plugin,
options: {
cacheName: 'session', // must match a session you've instantiated in your hapi server config
cache: 'session', // must match a session you've instantiated in your hapi server config. Also accepts a CacheService instance for advanced use-cases.
/**
* Options that DXT uses to render Nunjucks templates
*/
Expand Down
51 changes: 26 additions & 25 deletions docs/PLUGIN_OPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ The forms plugin is configured with [registration options](https://hapi.dev/api/
- `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
- `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
- `cache` (optional) - Caching options
- `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 [here](#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.md). See [here](./features/configuration-based/PAGE_EVENTS.md#authenticating-a-http-page-event-request-from-dxt-in-your-api) for details
- `saveAndReturn` (optional) - Configuration for custom session management including key generation, session hydration, and persistence. See [save and return documentation](./features/code-based/SAVE_AND_RETURN.md) 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.md) for details
- `onRequest` (optional) - A function that will be invoked on each request to any form route e.g `/{slug}/{path}`. See [here](#onrequest) for more details

## Services
Expand Down Expand Up @@ -80,7 +83,7 @@ If provided, the `onRequest` plugin option will be invoked on each request to an

```ts
export type OnRequestCallback = (
request: FormRequest | FormRequestPayload,
request: AnyFormRequest,
params: FormParams,
definition: FormDefinition,
metadata: FormMetadata
Expand All @@ -104,37 +107,35 @@ await server.register({
})
```

## saveAndReturn
## saveAndExit

The `saveAndReturn` plugin option enables custom session handling to enable "Save and Return" functionality. It consists of three optional functions:
The `saveAndExit` plugin option enables custom session handling to enable "Save and Exit" functionality. It is an optional route handler function that is called with the hapi request and response toolkit in addition to the last argument which is the [form context](./REQUEST_LIFECYCLE.md) of the current page from which the save and exit button was pressed:

- `keyGenerator` - Generates unique cache keys for session storage
- `sessionHydrator` - Retrieves saved session data from external sources
- `sessionPersister` - Stores session data to external systems
```ts
export type SaveAndExitHandler = (
request: FormRequestPayload,
h: FormResponseToolkit,
context: FormContext
) => ResponseObject
```

```js
await server.register({
plugin,
options: {
saveAndReturn: {
keyGenerator: (request) => {
const { userId, applicationId } = fetchSubmissionAttributes(request)
return `${userId}:${applicationId}`
},

sessionHydrator: async (request) => {
// Fetch saved state from database/API
const savedState = await fetchUserSession(request)
return savedState || null
},

sessionPersister: async (state, request) => {
// Save state to database/API
await saveUserSession(state, request)
}
saveAndExit: (
request: FormRequestPayload,
h: FormResponseToolkit,
context: FormContext
) => {
const { params } = request
const { slug } = params

// Redirect user to custom page to handle saving
return h.redirect(`/custom-magic-link-save-and-exit/${slug}`)
}
}
})
```

For detailed documentation and examples, see [Save and Return](./features/code-based/SAVE_AND_RETURN.md).
For detailed documentation and examples, see [Save and Exit](./features/code-based/SAVE_AND_EXIT.md).
88 changes: 88 additions & 0 deletions docs/features/code-based/SAVE_AND_EXIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
layout: default
title: Save and exit
parent: Code-based Features
grand_parent: Features
render_with_liquid: false
---

# Save and Exit

The forms engine supports save and exit capabilities through the `saveAndExit` plugin option. This feature enables applications to support end users saving their current answers and returning to the form at a later date.

It does this by displaying a secondary button on each question page when the feature is enabled. When the button is clicked the form is submitted in the usual way and once the page data is validated, the provided `saveAndExit` handler is called. This is a standard hapi route handler with an additional `FormContext` parameter passed that contains the [current state of the users progression through the form](../../REQUEST_LIFECYCLE.md).

> **Note:** it is your responsibility to ensure any state that exists outside of the form engine is captured upon persistence and available during hydration, e.g. file uploads via CDP.

## Configuration

The `saveAndExit` option is configured when registering the forms engine plugin:

```ts
await server.register({
plugin,
options: {
// ... other options
saveAndExit: (
request: FormRequestPayload,
h: FormResponseToolkit,
context: FormContext
): ResponseObject => {}
}
})
```

It is down to you to provide the mechanism by which you want to store the users data and provide them a means by which they can return to it at a later data. The `saveAndExit` handler simply activates the additional button, gives you the hook point in to the framework and provides you the data you need to know where the user had progressed to.

One common approach is ask end users for their email and send them a "magic link" that they can use to return with 28 days.

```
// This example shows how you can support custom UI flows to allow an end user to save their form progress and return at a later date.
// The save and exit method is called like other hapi route handlers and expects a similar return value.
// Here we're redirecting the user to another page where we might be providing a magic link or similar that the user can use to return to the form with.
await server.register({
plugin,
options: {
saveAndExit: (
request: FormRequestPayload,
h: FormResponseToolkit,
context: FormContext
) => {
const { params } = request
const { slug } = params
const usersAnswers = context.state

// Redirect user to custom page to handle saving
return h.redirect(`/custom-magic-link-save-and-exit/${slug}`)
}
}
})
```

## Data Structure

The `FormSubmissionState` object can be found at `context.state` and contains all the answers the user has provided so far.

This is the data you'll need to save to allow users to pick up from where they left.

```typescript
interface FormSubmissionState {
// User's form field values
[fieldName: string]: FormStateValue

// Special system fields
$$__referenceNumber?: string

// File upload state (if applicable)
upload?: Record<string, TempFileState>
}
```

## Restore session data

To restore a user's previous state use the `cacheService.setState` method.
The current request is passed in order to generate the cache key as so should include the correct form `slug` and `status` (if using the draft/live feature)

```js
await cacheService.setState(request, state)
```
162 changes: 0 additions & 162 deletions docs/features/code-based/SAVE_AND_RETURN.md

This file was deleted.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"./services/*": "./.server/server/plugins/engine/services/*",
"./engine/*": "./.server/server/plugins/engine/*",
"./helpers.js": "./.server/server/plugins/engine/components/helpers.js",
"./schema.js": "./.server/server/schemas/index.js",
"./templates/*": "./.server/server/plugins/engine/views/*",
"./cache-service.js": "./.server/server/services/cacheService.js",
"./package.json": "./package.json"
},
"scripts": {
Expand Down
39 changes: 0 additions & 39 deletions src/server/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,42 +639,3 @@ describe('prepareEnvironment', () => {
)
})
})

describe('Exit route handlers', () => {
let server: Server

beforeAll(async () => {
server = await createServer({
services: defaultServices
})
await server.initialize()
})

afterAll(async () => {
await server.stop()
})

beforeEach(() => {
jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata)
server.app.models.clear()
})

test('GET /exit returns 200 with exit page content', async () => {
jest.mocked(getFormMetadata).mockResolvedValueOnce({
...fixtures.form.metadata,
live: fixtures.form.state
})

jest.mocked(getFormDefinition).mockResolvedValue(fixtures.form.definition)

const options = {
method: 'GET',
url: `${FORM_PREFIX}/slug/exit`
}

const res = await server.inject(options)

expect(res.statusCode).toBe(StatusCodes.OK)
expect(res.result).toContain('Your progress has been saved')
})
})
Loading
Loading