Skip to content

(feat) Add form engine cookbook reference form#69

Merged
denniskigen merged 5 commits into
openmrs:mainfrom
denniskigen:feat/form-engine-cookbook
Apr 29, 2026
Merged

(feat) Add form engine cookbook reference form#69
denniskigen merged 5 commits into
openmrs:mainfrom
denniskigen:feat/form-engine-cookbook

Conversation

@denniskigen
Copy link
Copy Markdown
Member

@denniskigen denniskigen commented Apr 29, 2026

Summary

Add a Form Engine Cookbook reference form to the demo content package. The form is a working library of patterns supported by the OpenMRS 3 form engine, intended for form authors (implementers, country teams, anyone editing form JSON, in or out of Form Builder).

CleanShot 2026-04-29 at 11 55 46@2x

Each page covers one authoring task with the same shape:

  1. Scenario — what a form author is trying to achieve, in plain language ("you want a field to appear only when another answer is set").
  2. Shape — the JSON snippet to copy.
  3. Notes — gotchas, alternatives, when not to use it.

Topics covered, page by page:

  • Conditional visibility — hide.hideWhenExpression, disabled.disableWhenExpression, static isDisabled
  • Required variants — boolean, expression-based, conditionalAnswered
  • Validators — js_expression, date, conditionalAnswered, default_value
  • Computed fields — calculate.calculateExpression and the helper library (calcBMI, calcEDD, z-score family, etc.)
  • Previous-value and historical — formOptions.usePreviousValueDisabled, enablePreviousValue, historicalExpression with the HD helper
  • Special renderings — toggle, content-switcher, fixed-value, select-concept-answers, extension-widget
  • Special field types — patientIdentifier, personAttribute, testOrder, programState
  • Form-level metadata — availableIntents, behaviours overrides, postSubmissionActions, embedded translations, meta.programs, defaultPage, allowUnspecifiedAll
  • Composition — section reference and page isSubform
  • Question-option grab bag — weeksList (reserved), shownDateOptions, showComment, plus other less-common keys

Most fields use real concepts from the reference-app demo metadata so the form renders end-to-end against a dev distro. Patterns that depend on chart-side context the form-builder preview can't supply (referenced sibling forms, subforms, program states) are documented via markdown explainers rather than embedded as live fields, with a note about why.

The form is published: false by design — it will not appear in the patient chart unless that flag is flipped for local testing.

Companion change

Richer markdown rendering (inline code, fenced code blocks, lists, links, blockquotes) is gated by openmrs/openmrs-esm-form-engine-lib#733, which expands the markdown wrapper's allowlist and adds the styles to match. Without that PR the cookbook still renders but loses list bullets and code-block styling — the explainer text remains readable.

Introduces `form-engine-cookbook-core_demo.json`, a reference form that
demonstrates one canonical form-engine pattern per page. Batch 1 covers
the first five pages:

- Introduction (markdown)
- Conditional visibility: `hide.hideWhenExpression`,
  `disabled.disableWhenExpression`, `isDisabled`
- Required variants: boolean, string expression, `conditionalAnswered`
  object with `referenceQuestionId` / `referenceQuestionAnswers`
- Validators: `js_expression`, `date` (with `allowFutureDates`),
  `conditionalAnswered`
- Computed fields: `calcBMI(height, weight)`, `calcEDD(lmp)` via
  `questionOptions.calculate.calculateExpression`

The form is `published: false` so it does not appear in the patient
chart. Examples use real concept UUIDs from the existing demo metadata
so the form also renders in a dev deployment. Further pages (previous-
value controls, special renderings, special field types, form-level
metadata, composition, question-option grab bag) will follow in
subsequent commits on this branch.
Appends three more pages to the form engine cookbook:

- Previous-value and historical: `formOptions.usePreviousValueDisabled`,
  `questionOptions.enablePreviousValue`, `historicalExpression` with
  `HD.getObject('prevEnc').getValue(...)`.
- Special renderings: `toggle` with `toggleOptions`, `content-switcher`,
  `fixed-value`, `select-concept-answers`, `extension-widget` with
  `extensionId` / `extensionSlotName`.
- Special field types: `patientIdentifier` (OpenMRS ID via
  `identifierType`), `personAttribute` (telephone via `attributeType`),
  `testOrder` with `orderSettingUuid` / `orderType` / `selectableOrders`,
  `programState` with `programUuid` / `workflowUuid`.

Also adds `formOptions.usePreviousValueDisabled: false` at the form
root so the form exercises the form-level option alongside the question-
level one.
Completes the form engine cookbook with four more pages and a
companion library form.

Pages added to the cookbook:
- Form-level metadata: `availableIntents` + `behaviours` overrides per
  intent. The cookbook's root also gains `postSubmissionActions`,
  `translations`, `meta.programs`, `defaultPage`, `allowUnspecifiedAll`,
  and `referencedForms` so the form itself exercises each feature.
- Composition: a section with `reference` pulling the Vitals snippet
  from the library form.
- Subform page: `isSubform: true` with an inline `subform.form`.
- Question-option grab bag: `orientation`, `locationTag`, `weeksList`,
  `shownDateOptions`, `showComment`, `diagnosis.*`, `allowMultiple` +
  `isSearchable`, `isCheckboxSearchable`, `allowedFileTypes`,
  `isTransient`.

New file: `form-engine-cookbook-library-core_demo.json`. A minimal
one-page form containing a Vitals snippet section that the cookbook's
composition page references via `section.reference`. Also
`published: false`.

Both files validate cleanly against the aligned `form.schema.json`.
Two related batches of work on the cookbook form.

Make every page render in the form-builder preview, not just in a real
distro. Several patterns were authored in shapes that depend on chart-
side context the preview doesn't have, which was crashing the whole
form during render:

- The composition page's `reference` section pointed at a sibling form
  (`Form Engine Cookbook Library`) that isn't loaded when previewing in
  isolation; the engine threw with "Form not found" before any page
  rendered. Replaced with a markdown explainer of the pattern plus an
  inline equivalent vitals section so the shape is still discoverable.
- The composition page's subform page set `isSubform: true` with a
  nested form schema. The default schema transformer only walks
  `page.sections[].questions[]`, so subform fields never got `meta`
  initialised and the renderer threw on the first one. Replaced with a
  markdown explainer; subforms only render properly via the chart-side
  pipeline anyway.
- The `programState` field needs program/workflow data fetched from the
  backend, which the form-builder doesn't have. Replaced with a markdown
  explainer.
- The `weeksList` question option is declared in the engine's schema
  types but no renderer reads it; the field rendered as a `select`
  with no answers and crashed in `dropdown.component.tsx`. Replaced
  with a markdown note that the option is reserved.
- The `shownDateOptions` field used `rendering: "select"` with a CIEL
  concept and no inline answers. Added a small inline `answers` array
  so the dropdown has something to render in the preview while still
  exercising the `showDate` / `shownDateOptions` shape.
- Form-builder validator wanted `showComment` as a string token, not
  a boolean. Changed `true` -> `"true"`; the engine accepts both.

Rewrite the 12 explainer markdowns from "engine feature description"
voice to a Scenario -> Shape -> Notes structure aimed at form authors.
The cookbook's audience is mostly implementers and country teams, not
application developers, so the explainers now lead with the clinical or
authoring goal ("you want a field to appear only when another answer is
set"), then show the JSON shape, then call out edge cases. The actual
sample fields and their JSON are unchanged - only the prose around them.

Companion changes for richer markdown rendering ride in
openmrs-esm-form-engine-lib#733 (expanded markdown allowlist + styles
for code, lists, blockquotes, etc.). Without that PR the explainers
still render but lose list bullets and code-block styling.
@denniskigen denniskigen marked this pull request as draft April 29, 2026 08:44
The cookbook's root-level `translations` field shipped a flat map of
French strings (`{ Yes: "Oui", No: "Non", "Weight (kg)": "Poids (kg)",
"Height (cm)": "Taille (cm)" }`). The form engine reads this via
`useFormJson.ts` and registers it through
`window.i18next.addResourceBundle(language, '@openmrs/esm-form-engine-app',
formJson.translations, ...)` where `language` is whatever locale i18next
is currently in. So a flat map of French strings gets bound to whatever
locale the viewer happens to be using - including English - and
`t("Yes")` returns "Oui" for every reader, in every language, every
time they open the form.

Drop the block. The `translations` feature is still documented in the
form-level metadata explainer for anyone who wants to use it; we just
don't ship a broken-by-shape example.
@denniskigen denniskigen marked this pull request as ready for review April 29, 2026 08:57
@VeronicaMuthee
Copy link
Copy Markdown
Collaborator

@denniskigen, this is such a useful guide. Do we have such expressions?

  1. To enforce a Unique Identifier
  2. Pre-populating form field based on the patient's last encounter, so the clinician only has to update it if it has changed, for example, the date of the last cervical cancer screening or TPT, where previously captured
  3. How about calculating the date of the next appointment, say, based on the quantity of drugs dispensed (in days)

@denniskigen
Copy link
Copy Markdown
Member Author

denniskigen commented Apr 29, 2026

Thanks @VeronicaMuthee, those are good suggestions.

On the unique identifier one, the engine doesn't have anything for this. Expressions can see what's on the form plus a bit of patient context (sex, age, current visit, most recent encounter via HD), but they can't reach across to other patients to check whether an identifier is already taken. That check has to come from the backend, via the identifier source and OpenMRS's identifier validator. I'll call this out on the patientIdentifier page so it doesn't send anyone hunting for a form-side option.

For pre-filling from the last encounter, this is what "Previous-value and historical" is for, but I think I framed it too abstractly to connect. The shape is historicalExpression with HD.getObject('prevEnc').getValue('<concept-uuid>'). Clinician sees the prior value and can change it if it's stale. Your "last cervical cancer screening date" / TPT framing is a much better introduction than what I had, so I'll swap one of those in.

The next-appointment-from-days-dispensed one is a natural fit for a computed field:

"calculate": { "calculateExpression": "addDaysToDate(today(), drugDurationDays)" }

where drugDurationDays is the id of the number field holding the dispensed quantity. Computed fields only has BMI and EDD right now, neither of which exercises date math, so this slots in nicely.

@denniskigen denniskigen merged commit c4dbd1d into openmrs:main Apr 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants