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.
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:
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