4.2.6.2 Configure an event declaration form

Configuring an event declaration form requires you to:

  • Define the structure of the form into pages using defineDeclarationForm

  • Build each page from the available form fields, validations, conditionals etc

Define form structure using defineDeclarationForm()

In the v2 events engine, every event must declare its Declaration Form — the form a registrar completes when declaring the event (birth, death, marriage, etc.).

To make this easier and strongly typed, OpenCRVS provides the helper:

defineDeclarationForm(...)

This constructs a validated, typed declaration form configuration that can be assigned to:

declarationForm: defineDeclarationForm({...})

inside your EventConfigInput.


1. What defineDeclarationForm() does

defineDeclarationForm():

  • Ensures your declaration form follows the required v2 structure

  • Guarantees consistent typing for pages, groups and fields

  • Applies Zod validation so invalid form structures fail at service startup rather than in production

  • Produces the exact shape the events engine expects in EventConfigInput

It’s essentially a form builder with strong compile-time and runtime validation.


2. Structure of defineDeclarationForm()

It accepts a single config object:

Let’s break down the example:


3. label — The translated heading of the form

This is a TranslationConfig object.

It defines the name that appears at the top of the declaration flow.

Important notes:

  • defaultMessage is what appears in English (or fallback language)

  • id should be a proper translation key (e.g. "event.birth.declarationForm.label")

  • description is for translators

This label is shown in various parts of the UI, including on page headers.


4. pages — The ordered list of pages in the declaration form

The array contains Page Config Objects, each representing a “page” of the declaration form.

A Page represents:

  • One screen in the UI

  • Containing one or more groups

  • Each group containing one or more fields

Pages are defined separately (e.g. in deathIntroduction.ts, deceased.ts, etc.) so you can re-use them or swap them between events.

Why the order matters

The order you put the pages in this array determines how the registrar navigates the form:

  1. deathIntroduction

  2. deceased

  3. eventDetails

  4. informant

  5. documents

The form wizard/navigation follows this order exactly.


5. What a page looks like

A page config typically looks like:

defineDeclarationForm() collects all pages and produces the full declaration form schema.


Build each page from the available form fields

To configure fields for a page, you’re basically building an array of FieldConfig objects, one per UI control, using the FieldType enum to pick the widget and then layering on:

  • label & translation

  • required/optional

  • when it shows (conditionals)

  • how it’s validated

  • extra config (options, address config, etc.)

I’ll use your mother’s page as a worked example and generalise as we go.


1. What a field actually is (FieldConfig + FieldType)

Each entry in the fields array is a FieldConfigarrow-up-right — the schema that the v2 engine uses to render and validate the control. The core type lives in FieldConfig.ts, and the allowed type values are defined by FieldType.tsarrow-up-right (e.g. TEXT, CHECKBOX, NAME, AGE, ADDRESS, COUNTRY, SELECT, ID, DIVIDER, PARAGRAPH, etc.)

At minimum you’ll always set:

  • id – unique within the form; also becomes the path used in conditionals and validators (field('mother.dob'))

  • type – one of FieldType (or 'DATE' string in your example – effectively the same)

  • label – a TranslationConfig for the UI text

Then you add optional behaviour:

  • required, hideLabel, analytics, secured

  • conditionals – show/hide and review behaviour

  • validation – per-field and cross-field rules

  • configuration – extra settings depending on field type

  • options – for selects

  • defaultValue


2. Step-by-step: designing the fields for a page

Step 1 – Decide the data + IDs

For each piece of data you want on the page, pick a stable ID.

Using the mother’s page for birth as an example:

  • mother.detailsNotAvailable

  • mother.reason

  • mother.name

  • mother.dob

  • mother.dobUnknown

  • mother.age

  • mother.nationality

  • mother.idType

  • mother.nid, mother.passport, mother.brn

  • mother.address, mother.addressHelper, mother.addressDivider1, mother.addressDivider2

  • mother.maritalStatus, mother.educationalAttainment, mother.occupation, mother.previousBirths

This mother.* convention is what makes the conditions and validators readable (field('mother.age'), field('mother.idType'), etc.).

When you define a page for another role (father, informant, deceased, spouse), follow the same pattern: father.*, informant.*, etc.


Step 2 – Choose the FieldType for each input

Given the ID + domain meaning, pick a FieldType:

  • Booleans / togglesFieldType.CHECKBOX

    • e.g. mother.detailsNotAvailable, mother.dobUnknown

  • Free textFieldType.TEXT

    • e.g. mother.reason, mother.occupation

  • NamesFieldType.NAME (composite widget with first/middle/last etc.)

    • mother.name

  • DatesFieldType.DATE (in your example it’s 'DATE', but in code you’d normally use the enum)

    • mother.dob

  • Numeric valuesFieldType.AGE or FieldType.NUMBER

    • mother.age (AGE), mother.previousBirths (NUMBER)

  • Select listsFieldType.SELECT

    • mother.idType, mother.maritalStatus, mother.educationalAttainment

  • CountryFieldType.COUNTRY

    • mother.nationality

  • ID-like textFieldType.ID or FieldType.TEXT depending on validation needs

    • mother.nid (ID), mother.passport (TEXT), mother.brn (TEXT)

  • AddressFieldType.ADDRESS (composite with nested validators)

    • mother.address

  • Layout / helpers (no data)FieldType.DIVIDER, FieldType.PARAGRAPH

    • mother.details.divider, mother.addressDivider1, mother.addressDivider2, mother.addressHelper

This list is not exhaustive, but a good example of common fields used in the field palette you get from FieldType.ts — you pick whichever widget makes sense for that piece of data.

circle-info

A full recipe of how to use each field isnt available at present, but we will build each recipe out in documentation over time, starting with the Address component.


Step 3 – Add labels (TranslationConfig)

Every field gets a label:

Patterns worth keeping:

  • defaultMessage is human-readable English

  • description helps translators

  • id follows your event + section naming convention

For layout-only fields (DIVIDER) you reuse emptyMessage; for helper headings (PARAGRAPH) you give a more presentational label and optional configuration.styles.


Step 4 – Required / secured / analytics / hideLabel

You then decide how the field behaves e.g.:

  • required: true – must be filled when it is mandatory or optional

    • e.g. mother.name, mother.dob or mother.age, mother.idType, mother.address

  • secured: true – treated as sensitive PII (e.g. DOB, address)

    • e.g. mother.dob, mother.address

  • analytics: true – flagged for metrics / reporting meaning the field will be accessible in Metabase

    • e.g. mother.dob, mother.age, mother.maritalStatus, mother.educationalAttainment, mother.occupation, mother.previousBirths

  • hideLabel: true – when the control has an implicit label (e.g. NAME with inline placeholders) and you don’t want an extra label line

    • e.g. mother.name

This is where you encode business importance and privacy for each field.


Step 5 – Conditional visibility (conditionals)

This is the big one: when should each field show?

You use conditionals: [] with:

  • type: ConditionalType.SHOW – controls visibility in the main form

  • type: ConditionalType.DISPLAY_ON_REVIEW – controls visibility in the review pane

  • conditional: ... – expression built using helpers like field(), user(), and(), or(), not(), never()

Some patterns from the mother example:

5.1. Entire “mother details not available” branch

Key ideas:

  • If the informant is the mother, you don’t even show the “details not available” checkbox (we assume they know their own details).

  • On the review screen, only show this checkbox when it is true.

Same logic is reused for:

  • mother.details.divider – only visible when informant is not the mother

  • mother.reason – only visible if detailsNotAvailable is true

5.2. “Mother details must be captured” branch

Most fields use a requireMotherDetails helper:

Where requireMotherDetails is (roughly) “mother details are required for this record”, factoring in:

  • whether informant is mother, and

  • whether the “details not available” checkbox is ticked

This keeps field configs readable and DRY.

5.3. Mutually exclusive DOB / Age logic

  • mother.dob shows when:

    • mother.dobUnknown is not true, and

    • requireMotherDetails is true

  • mother.age shows when:

    • mother.dobUnknown is true, and

    • requireMotherDetails is true

So users either give an exact date of birth or an age as of child.dob.

5.4. ID type switching

  • mother.nid shows when mother.idType === NATIONAL_ID

  • mother.passport shows when mother.idType === PASSPORT

  • mother.brn shows when mother.idType === BIRTH_REGISTRATION_NUMBER

This is a classic pattern you’ll reuse elsewhere: pick an ID type via a SELECT, then show the relevant input field.


Step 6 – Validation rules (validation)

Each field can have multiple validation rules:

Patterns in the mother page:

  • Simple date rulemother.dob must be before today.

  • Cross-field date rulemother.dob must be before child.dob.

Age field:

National ID:

Here you see:

  • A reusable nationalIdValidator

  • A cross-field uniqueness check vs father and informant

Address:

The address field uses:

  • a top-level “leaf level” check (ensuring the lowest administrative level is selected)

  • nested validators for street address sub-fields

When you build pages for other entities, you follow the same pattern: use core’s field helpers plus any custom validators to enforce your country’s rules.


Step 7 – Field-specific configuration and options

Some field types accept extra configuration:

  • NAME:

  • AGE:

  • ADDRESS:

circle-exclamation
  • PARAGRAPH:

For selects:

  • idTypeOptions, maritalStatusOptions, educationalAttainmentOptions are arrays of { value, label } or similar, defined elsewhere with translation support.

This is where you plug in country-specific enumerations and UI presentation tweaks.


Step 8 – Defaults (defaultValue)

Finally, you can pre-fill fields:

  • mother.nationality defaults to 'FAR':

  • mother.address defaults to:

    • country: 'FAR'

    • addressType: DOMESTIC

    • admin area: user’s district:

This improves UX and ensures fields have sensible values for analytics.


3. Recipe for configuring fields on any page

For each page (mother, father, informant, deceased, etc.):

  1. List the information you need for that actor.

  2. Choose a stable ID for each piece (<actor>.<property>).

  3. Pick a FieldType from FieldType.ts that best matches the UI shape.

  4. Add a label with proper defaultMessage and translation id.

  5. Set behaviour flags:

    • required, secured, analytics, hideLabel where appropriate.

  6. Configure conditionals:

    • SHOW + logical helpers (field(), and, or, not, never)

    • optionally DISPLAY_ON_REVIEW if review view should differ

  7. Add validation rules:

    • simple type rules (date, number, length)

    • cross-field rules (mother vs child, uniqueness constraints)

    • nested rules for ADDRESS, etc.

  8. Add configuration and options for complex widgets:

    • NAME, AGE, ADDRESS, SELECT, PARAGRAPH styling, etc.

  9. Set defaultValue where it makes sense (country, address, etc.).

That’s essentially how you configure form fields for each page.

Last updated