Dynamic Forms Module
This guide is specific to Web SDK 2.0. If you are still using 1.x, you can find documentation here. Contact your Incode Representative for upgrade information and check if you are a candidate for this upgrade.
Full rollout to all clients still TBD.
The Dynamic Forms module steps the user through a multi-screen questionnaire whose screens, questions, and input types come from the backend per session. Use it for jurisdiction-specific questions that change without an SDK release.
Follows the form-based pattern across multiple screens. See the patterns page for the shared lifecycle.
Availability
This module is headless-only — there is no public <incode-dynamic-forms> web component. Inside <incode-flow> the orchestrator renders it for you. To drive it yourself, use createDynamicFormsManager from @incodetech/core/dynamic-forms and render your own form.
Configuration
type DynamicFormsConfig = {
flowId?: string;
screens?: Screen[];
};| Option | Type | Required | Description |
|---|---|---|---|
flowId | string | ❌ | Fetches the screen configuration from the backend. The orchestrator sets this. |
screens | Screen[] | ❌ | Pre-loaded screens. Providing them skips the fetch. |
Supply one of the two. If both are absent the module enters misconfigured rather than failing at runtime.
Screens and questions
type Screen = {
title?: string;
hideTitle?: boolean;
questions: Question[];
};
type Question = {
questionId: string;
question: string;
inputType?: InputType;
overrides?: InputType;
isPredefined?: boolean;
isOptional?: boolean;
options?: string[];
};| Property | Type | Description |
|---|---|---|
questionId | string | Stable identifier. Use it as the key for every answer and validation call. |
question | string | The label to display. |
inputType | InputType? | Which control to render. Absent values fall back to overrides, then to TEXT. |
overrides | InputType? | Fallback type used when inputType is absent. |
isPredefined | boolean? | Whether the question comes from Incode's predefined set rather than a tenant-authored one. |
isOptional | boolean? | Whether the user can leave the answer empty. |
options | string[]? | Choices for SELECT and MULTISELECT. |
InputType is 'TEXT', 'DATE', 'COUNTRY', 'NATIONALITY', 'NUMBER', 'EMAIL', 'PHONE', 'CPF', 'NAME', 'YESNO', 'SELECT', or 'MULTISELECT'. Treat the union as open — an unrecognized value renders as text rather than breaking the screen.
State machine
DynamicFormsState is a discriminated union over status:
| Status | Description |
|---|---|
idle | Initial state, waiting for load(). |
loadingScreens | Fetching the screen configuration from the backend. |
inputting | Current screen rendered; the user answers questions. |
submitting | Sending the current screen's answers. The screen stays available so you can show a spinner. |
success | All screens submitted. Shows briefly before finished. |
finished | Terminal. Carries result. |
misconfigured | Neither screens nor flowId was supplied. A configuration problem, not a runtime error. |
closed | The user dismissed the module. |
When status === 'inputting':
| Property | Type | Description |
|---|---|---|
currentScreen | Screen | The screen to render. |
screenIndex | number | Zero-based index of the current screen. |
totalScreens | number | How many screens the form has. Use both to show progress. |
answers | Record<string, string> | Current values, keyed by questionId. |
answerValidity | Record<string, boolean> | Format validity you reported through setAnswerValidity. |
validationErrors | DynamicFormsValidationErrors? | Errors currently shown to the user, keyed by questionId. Each value is an i18n key. Render as-is. |
canSubmit | boolean | true when no validation error is displayed. See the note below. |
submitting carries the same screen, answers, and validity, frozen at submission time.
finished carries result: 'completed' when every screen was submitted, or 'skipped' when the screen configuration could not be loaded. Loading failures skip the module rather than blocking the flow.
canSubmitis optimistic. It startstrueon an empty screen and only turnsfalseafter a submit attempt populatesvalidationErrors. Binding a Continue button tocanSubmitmatches the SDK's own behavior; it does not mean every required answer is present.
API methods
| Method | Purpose |
|---|---|
load() | Starts the module. Call this before anything else. |
setAnswer(questionId, value) | Record the raw user input for one question. |
setAnswerValidity(questionId, isValid) | Report whether a format-constrained value is valid. Use for PHONE, EMAIL, and CPF. |
validateField(questionId) | Validate one question, typically on blur. Sets or clears its validationErrors entry. |
submit() | Submit the current screen, then advance to the next screen or finish. |
close() | Dismiss the module from misconfigured. Fix the configuration rather than calling reset(). |
Plus the universal lifecycle: subscribe, getState, reset, stop.
Headless example
import { setup } from '@incodetech/core';
import { createDynamicFormsManager } from '@incodetech/core/dynamic-forms';
const manager = createDynamicFormsManager({ config: { flowId } });
manager.subscribe((state) => {
if (state.status === 'inputting') {
renderScreen({
screen: state.currentScreen,
progress: `${state.screenIndex + 1} of ${state.totalScreens}`,
answers: state.answers,
// validationErrors values are i18n keys — resolve them for display.
errors: state.validationErrors,
canSubmit: state.canSubmit,
});
}
if (state.status === 'misconfigured') {
console.error('Dynamic Forms needs either screens or a flowId');
}
if (state.status === 'finished') {
console.log('Dynamic Forms', state.result);
}
});
manager.load();See also
- Module: Custom Fields: dashboard-defined single-screen field schema
- Module: eKYC: identity-data form with a fixed field catalog
- Module Patterns → form-based
- Individual Modules
Updated 1 day ago
