Skip to content

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 wizard metadata property, with next() validating the current page before advancing
  • computedProps making fields conditionally required or hidden at runtime
  • choice branches with a custom change handler and icon labels
  • Repeatable array pages (minOccurs, maxOccurs, arrayItemName)
  • Dynamic options loaded from an external source, cascaded between fields
  • wizardSummaryPage for 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.

Client Onboarding

New client setup

Company details

Core account information for the rollout.

Options are requested from an external source.

Choose an industry to see team size options

Available options for the undefined industry are loaded by an external source using an extra parameter.

Project contacts

Anyone who should be looped in on the rollout.

1 contact
1

New contact

Launch approach

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.

Systems to connect

Any external tools we'll integrate with during onboarding.

0 systems

No systems added yet

Skip this step if there's nothing to integrate.

Review & submit

Quick recap before we kick this off. Anything needs tweaking? Jump back to the step.

Company

Company name
—
Industry
—
Team size
—
Data migration
No

Contacts

Contact 1
—

Launch approach

Approach
Not chosen

Systems

Systems
None

Ready to submit?

We'll create the onboarding workspace and email each contact an invite.

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; }],
      },
    ],
  },
];

Released under the MIT License.