Thinking web wizards

Understanding Wizards

Nielsen Norman Group defines the web wizard as

A wizard is a step-by-step process that allows users to input information in a prescribed order and in which subsequent steps may depend on information entered in previous ones.

As described by NNG in their article about the wizards, they are supposed to make the experience easier for the users by breaking down a huge form into smaller chunks so it becomes meaningful for the customers to fill and we can create a curated path for them by skipping unnecessary parts of the form.

Requirements

After developing a large number of web applications that have wizards over the years and spending much time fixing the tech debt on existing applications, I usually think about following KPI while writing a piece of software.

  • Dev workflow should be very easy and quick

  • Code should be so simple that even a 1-week experienced engineer should be able to contribute

  • Confidence with test cases should be so high that we should be able to merge a PR with all checks green without even testing on a preview canary build.

  • We should never say NO to the product because of the tech-limitation we created

While building a wizard one should think about following things

  • Wizards should be able to share/uplift the entire state to a single source of truth

  • Each step should be able to read and see through the entire wizard state but should only be able to mutate its state

  • Each step should define what capabilities are needed by them i.e. having Next, Skip, Submit, Cancel and Pause buttons

  • Wizard navigation/progress bar should be independent of what goes inside the steps

  • Each step should be able to change the default behaviour of the next i.e. if at step 3 we feel we can directly jump to step 5, step 3 should be able to tell the wizard manager that

Breaking it all down

So we can break down a big wizard into the following components

Wizard Manager

The entity that binds everything together and provides a container shell on UI to render things. If I say things in a simple language it is the UI component that the consumer of the library imports to render the wizard. If we look at the structure it can have the following look, the placement of different things can alter but it more or less would be have similar components

It can have the following structure where we only cover the core behaviour of the wizard without thinking much about what goes inside each step.

class UIWizard {

    /** handle navigation buttons */
    public onNext() {}
    public onPrevious() {}
    public onCancel() {}
    public onSubmit() {}
    public onSkip() {}

    /** update the step shell & progress bar state */
    public loadStep(stepIdentifier: string) {}
}

State Manager

Unified entity object with proper setters and getters to save and read the wizard state.

class WizardStateManager {
    private wizardState: Record<string, Object> = {};
    private wizardNavigationState: Record<string, boolean> = {};
    private activatedStep: string;

    private activateStep(stepIdentifier: string) { }
    private updateState(currentStepState: Object) { }
    private readState() { }
    private readWizardState() { }

    private enableNext() { }
    private disableNext() { }

    private enablePrevious() { }
    private disablePrevious() { }

    private enableSkip() { }
    private disableSkip() { }

    private enableSubmit() { }
    private disableSubmit() { }

    private enableCancel() { }
    private disableCancel() { }
}

Step Definition

UI code to present the current step with all the things it needs to run provided by the hooks or props.



const IntroductionStep = () => {
    const wizardState = useWizardState();
    useNext(async () => {    });
    usePrevious(async () => {    });
    useSubmit(async () => {    });
    useCancel(async () => {    });


    return /* your jsx */
}

// boolean flags to show / hide navigation buttons
IntroductionStep.hasNext = true;
IntroductionStep.hasPrevious = true;
IntroductionStep.hasCancel = true;
IntroductionStep.hasSubmit = true;

// static pure function to show hide the label on the progress bar
IntroductionStep.isVisibleOnProgressBar = (wizardState) => {

}

Rejecting a navigation from the step

Each step should be able to prevent the navigation from happening in case of a form validation, backend call or some blocking event that should prevent the user from going away from the current step. This can be done using async characteristics of the callback function passed to the hooks.

useNext(async () => {
    if (someConditionNotMet) {
        return Promise.reject({ message: 'some reason' });
    }
})

Progress Definition

Is the most basic component that can be used to display the current state of wizard progress

const WizardProgressBar = () => {
    const wizardState = useWizardState();

    return /* jsx to render wizard navigation progress bar */
}

Configuration

The thing that binds it all is the configuration, it needs to be very simple and declarative and can look like the following

interface WizardConfiguration {
    steps: WizardStepConfiguration[];
    progress: React.FC;
}

interface ComponentNavigationOptions {
    hasNext?: boolean;
    hasPrevious?: boolean;
    hasCancel?: boolean;
    hasSubmit?: boolean;
    isVisibleOnProgressBar(): boolean;
}

interface WizardStepConfiguration {
    component: React.FC & ComponentNavigationOptions;
    label: string;
}