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()
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() doesdefineDeclarationForm():
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()
defineDeclarationForm()It accepts a single config object:
Let’s break down the example:
3. label — The translated heading of the form
label — The translated heading of the formThis is a TranslationConfig object.
It defines the name that appears at the top of the declaration flow.
Important notes:
defaultMessageis what appears in English (or fallback language)idshould be a proper translation key (e.g."event.birth.declarationForm.label")descriptionis 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
pages — The ordered list of pages in the declaration formThe 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:
deathIntroductiondeceasedeventDetailsinformantdocuments
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)
FieldConfig + FieldType)Each entry in the fields array is a FieldConfig — 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.ts (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 ofFieldType(or'DATE'string in your example – effectively the same)label– aTranslationConfigfor the UI text
Then you add optional behaviour:
required,hideLabel,analytics,securedconditionals– show/hide and review behaviourvalidation– per-field and cross-field rulesconfiguration– extra settings depending on field typeoptions– for selectsdefaultValue
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.detailsNotAvailablemother.reasonmother.namemother.dobmother.dobUnknownmother.agemother.nationalitymother.idTypemother.nid,mother.passport,mother.brnmother.address,mother.addressHelper,mother.addressDivider1,mother.addressDivider2mother.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
FieldType for each inputGiven the ID + domain meaning, pick a FieldType:
Booleans / toggles →
FieldType.CHECKBOXe.g.
mother.detailsNotAvailable,mother.dobUnknown
Free text →
FieldType.TEXTe.g.
mother.reason,mother.occupation
Names →
FieldType.NAME(composite widget with first/middle/last etc.)mother.name
Dates →
FieldType.DATE(in your example it’s'DATE', but in code you’d normally use the enum)mother.dob
Numeric values →
FieldType.AGEorFieldType.NUMBERmother.age(AGE),mother.previousBirths(NUMBER)
Select lists →
FieldType.SELECTmother.idType,mother.maritalStatus,mother.educationalAttainment
Country →
FieldType.COUNTRYmother.nationality
ID-like text →
FieldType.IDorFieldType.TEXTdepending on validation needsmother.nid(ID),mother.passport(TEXT),mother.brn(TEXT)
Address →
FieldType.ADDRESS(composite with nested validators)mother.address
Layout / helpers (no data) →
FieldType.DIVIDER,FieldType.PARAGRAPHmother.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.
Step 3 – Add labels (TranslationConfig)
Every field gets a label:
Patterns worth keeping:
defaultMessageis human-readable Englishdescriptionhelps translatorsidfollows 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 optionale.g.
mother.name,mother.dobormother.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 Metabasee.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 linee.g.
mother.name
This is where you encode business importance and privacy for each field.
Step 5 – Conditional visibility (conditionals)
conditionals)This is the big one: when should each field show?
You use conditionals: [] with:
type: ConditionalType.SHOW– controls visibility in the main formtype: ConditionalType.DISPLAY_ON_REVIEW– controls visibility in the review paneconditional: ...– expression built using helpers likefield(),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 mothermother.reason– only visible ifdetailsNotAvailableistrue
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.dobshows when:mother.dobUnknownis not true, andrequireMotherDetailsis true
mother.ageshows when:mother.dobUnknownis true, andrequireMotherDetailsis true
So users either give an exact date of birth or an age as of child.dob.
5.4. ID type switching
mother.nidshows whenmother.idType === NATIONAL_IDmother.passportshows whenmother.idType === PASSPORTmother.brnshows whenmother.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)
validation)Each field can have multiple validation rules:
Patterns in the mother page:
Simple date rule –
mother.dobmust be before today.Cross-field date rule –
mother.dobmust be beforechild.dob.
Age field:
National ID:
Here you see:
A reusable
nationalIdValidatorA 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
configuration and optionsSome field types accept extra configuration:
NAME:AGE:ADDRESS:
The mandatory selects for administrative location hierarchy in an Address component are always required and configured in 4.2.5.1 Set up application settings, using the 4.2.2 administrative divisions
PARAGRAPH:
For selects:
idTypeOptions,maritalStatusOptions,educationalAttainmentOptionsare 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)
defaultValue)Finally, you can pre-fill fields:
mother.nationalitydefaults to'FAR':mother.addressdefaults to:country:
'FAR'addressType:
DOMESTICadmin 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.):
List the information you need for that actor.
Choose a stable ID for each piece (
<actor>.<property>).Pick a
FieldTypefromFieldType.tsthat best matches the UI shape.Add a
labelwith properdefaultMessageand translationid.Set behaviour flags:
required,secured,analytics,hideLabelwhere appropriate.
Configure conditionals:
SHOW+ logical helpers (field(),and,or,not,never)optionally
DISPLAY_ON_REVIEWif review view should differ
Add validation rules:
simple type rules (date, number, length)
cross-field rules (mother vs child, uniqueness constraints)
nested rules for ADDRESS, etc.
Add
configurationandoptionsfor complex widgets:NAME, AGE, ADDRESS, SELECT, PARAGRAPH styling, etc.
Set
defaultValuewhere it makes sense (country, address, etc.).
That’s essentially how you configure form fields for each page.
Last updated