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.jsFormGenerator/questionRenderers.js
|
| Module | Owned by one feature (Household member list, venue registration step, …) |
pages/YourModule/formQuestions/YourComponent/pages/YourModule/formQuestions/questionTypes.jspages/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. IncludefieldNameso submit payloads map correctly. -
validate()— Called byformGenerator.formValidation()/stepValidator(). RespectisOptionalwhen present. -
dependentQuestion— For radio/checkbox, set on the option object; FormGenerator handles show/hide. Other types can usevisible: falseuntil 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
- Component class extends
FormComponentwithmarkup+validate. - Type string added to local
questionTypes.js. - Renderer factory added to
questionRenderers.js(core or module map). - Page passes
questionRendererswhen using module-owned types. - Steps API returns the same
questionTypestring. - Playground sample added (see Adding a sample to the playground).