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
wizardtype withvalidatePagecalled 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: true
Touched: false
Valid: true
// form values:
{
"company": {},
"projectContacts": [
{}
],
"launchApproach": {
"selfServe": {
"field-0": true
},
"guidedRollout": {}
}
}
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: '',
type: 'wizard',
fieldOptions: { label: formName },
description: 'New client setup',
submitButtonText: 'Submit onboarding',
validatePage,
submitForm: async (resolve: LoadingResolve) => {
handleSubmit(
(values) => {
submitted.value = values;
resolve(true);
},
() => resolve(false),
)();
},
children: [
{
name: 'company',
type: 'wizardPage',
fieldOptions: { label: 'Company details' },
description: 'Core account information for the rollout.',
helpText: 'Who are we onboarding?',
computedProps: [registerWizardPagePath],
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: 'wizardPage',
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',
computedProps: [registerWizardPagePath],
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: 'wizardPage',
fieldOptions: { label: 'Launch approach' },
description: `Pick one. You can switch later if plans change — we'll keep the fields you've filled in.`,
helpText: 'How will we go live?',
fullWidth: true,
computedProps: [registerWizardPagePath],
choiceShowChoiceSelect: true,
changeChoice: key => selectedLaunchApproach.value = key as LaunchApproach,
choice: [
{
name: 'selfServe',
fieldOptions: { label: 'Self-serve launch' },
description: 'The client drives the rollout themselves. Fastest path to go-live.',
iconName: 'bolt',
fullWidth: true,
computedProps: [
(thisField, thisValue) => {
if (selectedLaunchApproach.value !== 'selfServe') {
thisField.hide = true;
thisValue.value = {};
}
},
],
children: [
{
hide: true,
computedProps: [
(_, thisValue) => {
// The choice fields are "enabled" once 1 of the values is filled in, we simulate this here
thisValue.value = selectedLaunchApproach.value === 'selfServe' ? true : undefined;
},
],
},
{
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,
computedProps: [
(thisField, thisValue) => {
if (selectedLaunchApproach.value !== 'guidedRollout') {
thisField.hide = true;
thisValue.value = {};
}
},
],
children: [
{
hide: true,
computedProps: [
(_, thisValue) => {
// The choice fields are "enabled" once 1 of the values is filled in, we simulate this here
thisValue.value = selectedLaunchApproach.value === 'guidedRollout' ? true : undefined;
},
],
},
{
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: 'wizardPage',
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,
computedProps: [registerWizardPagePath],
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',
minOccurs: 0,
computedProps: [registerWizardPagePath, (field) => { field.wizardSummary = wizardSummary.value; }],
},
],
},
];