Advanced: Client Onboarding Wizard
A production-style wizard demonstrating the full feature set: multi-step navigation with per-page validation, conditional required/hidden fields, dynamic option loading, repeatable contacts, a choice-based launch path, and a read-only summary page before submit.
What It Demonstrates
- Multi-step forms via the
wizardmetadata property, withnext()validating the current page before advancing computedPropsmaking fields conditionally required or hidden at runtimechoicebranches with a custom change handler and icon labels- Repeatable array pages (
minOccurs,maxOccurs,arrayItemName) - Dynamic options loaded from an external source, cascaded between fields
wizardSummaryPagefor a pre-submit review with edit links- Animated submission timeline after a successful submit
Example
Client Onboarding
A concrete demo for a SaaS team collecting everything needed to onboard a new client. It highlights reusable templates, grouped fields, repeatable contacts, mutually exclusive launch paths, and computed fields that become required only when they matter.
IsDirty: false
Touched: false
Valid: true
// form values:
{
"company": {},
"projectContacts": [
{}
]
}
Conditional Required / Hidden Field
computedProps can flip minOccurs and hide at runtime based on the value of another field. Here the migration deadline becomes mandatory only when the user enables the migration checkbox:
ts
{
name: 'migrationDeadline',
fieldOptions: {
label: 'Migration deadline',
},
description: 'When must all historical records be live in the new system?',
minOccurs: 0,
fullWidth: true,
validation: dateValidation,
placeholder: 'dd/mm/yyyy',
computedProps: [(field) => {
if (requiresMigration.value)
field.minOccurs = 1; // make the field mandatory
if (!requiresMigration.value)
field.hide = true;
}],
},Full Metadata
ts
const metadata: Metadata[] = [
{
name: 'wizard',
path: '',
wizard: true,
fieldOptions: { label: formName },
description: 'New client setup',
submitButtonText: 'Submit onboarding',
children: [
{
name: 'company',
type: 'heading',
fieldOptions: { label: 'Company details' },
description: 'Core account information for the rollout.',
helpText: 'Who are we onboarding?',
children: [
{
name: 'companyName',
fieldOptions: { label: 'Company name' },
restriction: { minLength: 2 },
fullWidth: true,
},
{
name: 'industry',
type: 'select',
fieldOptions: { label: 'Industry' },
description: 'Options are requested from an external source.',
computedProps: [(field, value) => {
field.options = props.optionStore.industry;
// Load the teamSize options, depending on the selected industry
requestOptions('teamSize', { industry: value.value as string });
}],
},
{
name: 'teamSize',
type: 'select',
fieldOptions: { label: 'Team size' },
minOccurs: 0,
computedProps: [(field) => {
const industry = industryValue.value;
field.options = props.optionStore.teamSize;
if (industry)
field.minOccurs = 1; // Make the field mandatory
if (!industry) {
field.dependentOnMessage = 'Choose an industry to see team size options';
}
field.description = `Available options for the ${industry} industry are loaded by an external source using an extra parameter.`;
}],
},
{
name: 'requiresMigration',
type: 'checkbox',
minOccurs: 0,
fieldOptions: { label: 'Migrate existing customer data' },
description: 'Turn this on when records need to be imported from another system.',
fullWidth: true,
},
{
name: 'migrationDeadline',
fieldOptions: {
label: 'Migration deadline',
},
description: 'When must all historical records be live in the new system?',
minOccurs: 0,
fullWidth: true,
validation: dateValidation,
placeholder: 'dd/mm/yyyy',
computedProps: [(field) => {
if (requiresMigration.value)
field.minOccurs = 1; // make the field mandatory
if (!requiresMigration.value)
field.hide = true;
}],
},
],
},
{
name: 'projectContacts',
type: 'heading',
fieldOptions: { label: 'Project contacts' },
description: 'Anyone who should be looped in on the rollout.',
helpText: 'Who should we work with?',
arrayItemName: 'contact',
arrayItemNamePlural: 'contacts',
arrayItemFieldForTitle: 'fullName',
minOccurs: 1,
maxOccurs: 3,
fullWidth: true,
children: [
{
name: 'fullName',
fieldOptions: { label: 'Full name' },
},
{
name: 'email',
fieldOptions: { label: 'Work email' },
restriction: { pattern: '^.+@.+\\..+$' },
},
{
name: 'role',
type: 'select',
fullWidth: true,
fieldOptions: { label: 'Role' },
computedProps: [(field) => {
field.options = props.optionStore.role;
}],
},
],
},
{
name: 'launchApproach',
type: 'heading',
fieldOptions: { label: 'Launch approach' },
description: 'Pick one. Only the selected option ends up in the submitted values, but what you filled in for the other option is kept and restored when you switch back.',
helpText: 'How will we go live?',
fullWidth: true,
choiceShowChoiceSelect: true,
explicitChoiceSelection: true,
preserveOnSwitch: true,
choice: [
{
name: 'selfServe',
fieldOptions: { label: 'Self-serve launch' },
description: 'The client drives the rollout themselves. Fastest path to go-live.',
iconName: 'bolt',
fullWidth: true,
children: [
{
name: 'goLiveDate',
fieldOptions: { label: 'Target go-live date' },
validation: dateValidation,
placeholder: 'dd/mm/yyyy',
},
{
name: 'internalOwner',
fieldOptions: { label: 'Client-side owner' },
},
],
},
{
name: 'guidedRollout',
fieldOptions: { label: 'Guided rollout' },
description: 'We run kickoff, training, and launch alongside the client team.',
iconName: 'users',
fullWidth: true,
children: [
{
name: 'kickoffDate',
fieldOptions: { label: 'Kickoff call date' },
fullWidth: true,
validation: dateValidation,
placeholder: 'dd/mm/yyyy',
},
{
name: 'needsTraining',
type: 'checkbox',
minOccurs: 0,
fieldOptions: { label: 'Include team training' },
description: 'Turn on to schedule a training session during onboarding.',
fullWidth: true,
falseAsUndefined: true,
},
{
name: 'trainingFormat',
type: 'select',
minOccurs: 0,
fieldOptions: {
label: 'Training format',
},
description: 'Required because guided rollout includes team training.',
fullWidth: true,
computedProps: [(field) => {
field.options = props.optionStore.trainingFormat;
if (needsTraining.value)
field.minOccurs = 1; // Make the field mandatory
if (!needsTraining.value)
field.hide = true;
}],
},
],
},
],
},
{
name: 'systems',
type: 'heading',
fieldOptions: { label: 'Systems to connect' },
description: `Any external tools we'll integrate with during onboarding.`,
helpText: 'What needs to connect?',
arrayNoItemsMessage: `Skip this step if there's nothing to integrate.`,
arrayItemName: 'system',
arrayItemNamePlural: 'systems',
arrayItemFieldForTitle: 'systemName',
minOccurs: 0,
maxOccurs: 4,
autoAddMinOccurs: false,
fullWidth: true,
children: [
{
name: 'systemName',
fieldOptions: { label: 'System name' },
placeholder: 'e.g. Salesforce, HubSpot',
},
{
name: 'purpose',
fieldOptions: { label: 'Purpose in the rollout' },
placeholder: 'e.g. source of CRM records',
},
],
},
{
name: 'summaryPage',
type: 'wizardSummaryPage',
fieldOptions: { label: 'Review & submit' },
description: 'Quick recap before we kick this off. Anything needs tweaking? Jump back to the step.',
helpText: 'Check and submit',
wizardSummaryConfirmation: 'We\'ll create the onboarding workspace and email each contact an invite.',
minOccurs: 0,
computedProps: [(field) => { field.wizardSummary = wizardSummary.value; }],
},
],
},
];