How pages get served and how form data gets saved in this prototype. Read this before adding a page, a form or a route. Most of the time you need a template and nothing else.
The prototype kit runs these in order on every request:
- Auto-store data - everything in the POST body and the query string is merged into
req.session.data, using the fieldnameas the key. Fields starting with_are skipped. - App locals (app/locals.js) - sets
currentUrl,referrerChain,query,flash,currentUser,currentBSUfor every view. - Our routes (app/routes.js and the files in app/routes/) - middleware that loads the participant, appointment, clinic or episode named in the URL, plus the handful of routes that do real processing.
- Redirect POST to GET - any POST that no route handled is redirected to a GET of the same URL.
- Auto-routes - a GET for
/foo/barrendersapp/views/foo/bar.html, orapp/views/foo/bar/index.html.
The consequences of that order are the rules below.
A form can post to any URL. Its fields are already in the session before any route runs, so the receiving page just renders with the new data. There is nothing to write to "receive" a form.
{# app/views/participants/questionnaire/health-status.html #}
{% set formAction = "./current-symptoms" %}
{% block pageContent %}
{{ radios({
small: true,
name: "participant[healthStatus]",
value: participant.healthStatus,
...
}) }}
{{ button({ text: "Continue" }) }}
{% endblock %}Posting this saves data.participant.healthStatus and lands on current-symptoms, which is served by the next template. No route.
Rules that follow:
- Do not write a route whose only job is to receive a form. If the next page is a template, post straight to it.
- Read submitted values from
req.session.data, notreq.body. By the time a route runs, the body has already been merged into the session. Readingreq.bodybypasses the session and breaks the auto-store behaviour everything else relies on. There are two exceptions. JSON endpoints called from client-side JavaScript must usereq.body, because JSON is parsed after the auto-store runs. One-off action parameters, such as a submit button's value or the hidden ids and optional flags on a fragment action, are better read fromreq.bodytoo (orreq.querywhen a link sends them): in the session they stay after the request, so a later request that leaves them out would pick up an old value. Starting such a field's name with an underscore (_exitAction) keeps it out of the session altogether, because the kit skips those. - Name fields for where the data should live.
name="participant[healthStatus]"stores todata.participant.healthStatus. Prefer a structured object likeappointment[...]orparticipant[...]over new top-level keys ondata. - Prefill from the same place. Give the component
value(orvaluesfor checkboxes) from the object the field writes to. - Unchecked checkboxes send nothing, so the kit's hidden
_uncheckedvalue is what clears a previously ticked box. The NHS macros handle this for you.
The generated records (data.participants, data.appointments, data.clinics, data.episodes) are shared and read only. Forms write to a working copy, data.participant or data.appointment, which the URL middleware sets up. A save route then copies the working copy back through the update helpers. See data-conventions.md for the rules and the helper names.
So the shape of a multi-page form is: pages post to each other, all writing into the working copy, and one final "save" route at the end.
Write a route only for something a template cannot do:
- Branching - the next page depends on what was submitted. Read the answer from
req.session.data, thenres.redirect(). - Saving - copying a working copy back to the shared record, usually via the update helpers.
- Validation - checking required data and redirecting back with
req.flash('error', ...). Only on pages where missing data would break what follows. See validation.md. - Processing - calculations, generating records, changing status, anything that is not a straight copy of the form.
- JSON endpoints for client-side JavaScript.
Conventions for routes:
- Put them in the file for their area under app/routes/, not in
app/routes.js. - Post to a separate URL from the page (
foo-answer,save,complete), never to the page itself. - Use
res.redirect()afterwards, neverres.render()from a POST. - Match the URL to the journey, for example
/clinics/:clinicId/appointments/:appointmentId/....
Before writing one, check whether a template already covers the URL (next section) and whether an existing route or helper already does the processing.
Sections whose URLs carry an id do not need a template per id. The area's middleware loads the record, then a wildcard route renders whatever template matches the rest of the path:
| URL prefix | Middleware loads | Templates under |
|---|---|---|
/participants/:participantId/... |
participant, participantId, participantUrl, contextUrl, originalParticipant |
app/views/participants/ |
/clinics/:clinicId/appointments/:appointmentId/... |
appointment, appointmentData, appointmentUrl, appointmentId, clinic, clinicId, participant, participantId, episode, unit, contextUrl, pageContext |
app/views/appointments/ |
/reading/session/:sessionId/appointments/:appointmentId/... |
readingCase, session, sessionId, appointment, appointmentId, participant, clinic, unit, progress, isReadingWorkflow |
app/views/reading/ |
So /participants/abc123/show renders app/views/participants/show.html with participant already set, and /participants/abc123/questionnaire/summary renders app/views/participants/questionnaire/summary.html. To add a page to one of these sections, add the template. The wildcard route is createDynamicTemplateRoute in app/lib/utils/dynamic-routing.js; the middleware is at the top of each routes file.
Use the provided URL locals when building links: participantUrl, appointmentUrl, contextUrl. Relative links (./edit, ../) also work well within a section.
Layouts live in app/views/_templates/ and are extended by bare name:
layout-app.html- the default. Flash messages, optional page navigation block, grid column (two thirds by default, setgridColumn), and a form wrapper.layout-appointment.html- the appointment workflow, with its step navigation.layout-reading.html- the reading workflow.layout-medical-history-form.html,layout-modal-form.html- specialised forms. See modal-system.md for modal forms.layout-base.html- the bare shell the others build on; rarely extended directly.
Page conventions:
- Start the file with a comment naming it:
{# app/views/participants/show.html #}. {% set pageHeading = "..." %}and put content in{% block pageContent %}. Back links go in{% block beforeContent %}.- Setting
formActionmakes the layout wrap the page in a form posting there. SettingisFormdoes the same with the current URL. Do not add a second<form>inside. - Reuse
_includes/and_components/before writing new markup. The style guide at/style-guide/shows every custom component.
| Mistake | Instead |
|---|---|
A router.post that only reads fields and redirects |
Post the form straight to the next template |
const foo = req.body.foo |
const foo = req.session.data.foo (or the working copy it was named into) |
A router.get that renders a template under /participants/:id/ |
Just add the template; dynamic routing serves it |
Building /clinics/${clinicId}/appointments/${appointmentId} in a view |
Use appointmentUrl |
Manually adding ?referrerChain= or ?returnTo= to links |
Use the referrer filters, see referrers.md |
Writing straight to data.participants[...] |
Write to the working copy, then save through the update helpers |