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