Form Generator · Creating a new question component

Creating a new question component

A Form Generator “component” is a class that extends FormComponent, renders one question from JSON, and exposes a value that FormGenerator stores in answers. Registration happens through a questionType string and a matching entry in a questionRenderers map — not by editing a giant switch inside FormGenerator.

1. Choose core vs module

Placement When to use Where files live
Core Reusable field used across many journeys (radio, select, date, finders, …) components/YourComponent/
FormGenerator/questionTypes.js
FormGenerator/questionRenderers.js
Module Owned by one feature (Household member list, venue registration step, …) pages/YourModule/formQuestions/YourComponent/
pages/YourModule/formQuestions/questionTypes.js
pages/YourModule/formQuestions/questionRenderers.js

2. Implement the component

Extend FormComponent and implement at least markup(). FormGenerator mounts your class into .js-form-generator-question-{id} and keeps a reference on question.ref for validation and answer collection.

import FormComponent from '../FormComponent';

export default class YourQuestion extends FormComponent {
  markup(value, data) {
    return `
      
`; } afterRender() { const input = this.containerEl.querySelector('.js-your-question-input'); input?.addEventListener('change', () => { this.value = { fieldName: this.data.fieldName, value: input.value }; }); } validate() { if (this.data?.isOptional) return true; return Boolean(this.value?.value); } }
  • value — Setting this triggers FormGenerator’s onChange pipeline. Include fieldName so submit payloads map correctly.
  • validate() — Called by formGenerator.formValidation() / stepValidator(). Respect isOptional when present.
  • dependentQuestion — For radio/checkbox, set on the option object; FormGenerator handles show/hide. Other types can use visible: false until a parent answer reveals them.
  • Reference — Simple core field: components/RadioComponent/index.js. Complex module field: pages/HouseholdAccounts/formQuestions/HouseHoldMemberList/.

3. Register the question type

Add a stable string constant and map it to a factory that receives FormGenerator context:

// questionTypes.js (core or module)
export const QuestionTypes = {
  YourType: 'yourQuestionType'   // must match Steps API `questionType`
};

// questionRenderers.js
import YourQuestion from './YourQuestion';
import { QuestionTypes } from './questionTypes';

const withQuestionData = (question, commonConfig) => ({
  data: { ...question, ...commonConfig }
});

export const yourQuestionRenderers = {
  [QuestionTypes.YourType]: (_ctx, question, container, commonConfig) =>
    new YourQuestion(container, withQuestionData(question, commonConfig))
};

Core types: export from coreQuestionRenderers in FormGenerator/questionRenderers.js. Module types: export a separate map (e.g. householdQuestionRenderers) and pass it into FormGenerator on each page that needs it.

4. Use it with FormGenerator

On the page that owns the journey, merge your renderers and ensure the Steps JSON uses your questionType string:

import FormGenerator, { FormTypes } from '../../components/FormGenerator';
import { yourQuestionRenderers } from '../YourModule/formQuestions/questionRenderers';

const formGenerator = new FormGenerator(container, {
  questions: stepsFromApi,
  totalSteps: 2,
  formType: FormTypes.StepForm,
  questionRenderers: yourQuestionRenderers   // merged over coreQuestionRenderers
});

formGenerator.onSubmit((answers) => {
  // answers[step][questionId] contains each question.ref.value shape
});

Backend contract: each step in the Steps API returns objects with id, step, fieldName, questionType: 'yourQuestionType', visible, plus any props your component reads (options, supportingProps, subComponents, …).

Checklist

  1. Component class extends FormComponent with markup + validate.
  2. Type string added to local questionTypes.js.
  3. Renderer factory added to questionRenderers.js (core or module map).
  4. Page passes questionRenderers when using module-owned types.
  5. Steps API returns the same questionType string.
  6. Playground sample added (see Adding a sample to the playground).