⚠️ NOTE ⚠️: The
next-wizard
is still experimental and can be imported at the path'@krakentech/blueprint-onboarding/next-wizard'
.
Wizard code examples
Here are some examples of how we can use the the createNextWizard()
builder function to create a workflow of pages.
Code samples can be copied to clipboard or wrapped - hover over one to see the buttons.
API Route setup
The next-wizard needs an API route to be able to submit the form values on the server side. This can be easily setup with the getFormCookieApiHandler
helper.
// pages/api/onboarding/submit-form.ts
import { getFormCookieApiHandler } from '@krakentech/blueprint-onboarding/next-wizard';
export default getFormCookieApiHandler({
encryptionSecret: process.env.NEXT_PUBLIC_ENCRYPTION_SECRET,
});
Create Wizard
The builder function createNextWizard
basically provides a set of strongly typed utils. They provide a very flexible and generic toolset to create features such as multiple page journeys.
export const {
getCookieFormData,
setCookieFormData,
deleteCookieFormData,
deserializeFormValues,
getNextStep,
getPreviousStep,
getRedirectionDestination,
getRedirectionStep,
getStep,
getSteps,
getMainSteps,
serializeFormValues,
getIsStepUnlocked,
} = createNextWizard<
typeof OnboardingStep,
OnboardingFormValues,
OnboardingSerializedFormValues
>({
cookieName: 'demo-domestic-onboarding',
// The secret has to match with the value used for the API route
encryptionSecret: process.env.NEXT_PUBLIC_ENCRYPTION_SECRET,
// This route has to point to where the api route has been setup
setCookieFormDataApiHandlerRoute: '/api/onboarding/submit-form',
initialValues: ONBOARDING_INITIAL_FORM_VALUES,
steps,
serializeFormValues: (formValues) => ({
...formValues,
[OnboardingKey.DateOfBirth]:
formValues[OnboardingKey.DateOfBirth]?.toISOString() ?? null,
}),
deserializeFormValues: (serializedFormValues) => ({
...serializedFormValues,
[OnboardingKey.DateOfBirth]: serializedFormValues[
OnboardingKey.DateOfBirth
]
? parseISO(serializedFormValues[OnboardingKey.DateOfBirth])
: null,
}),
});
Building a Page
In the getServerSideProps
function we use the wizard utils to fetch and validate the cookie form data and do things such as redirect. Any validation logic is entirely opt-in and can be setup as needed. Please note, that getServerSideProps
requires serialised form values to be able to pass them to the client.
export const getServerSideProps: GetServerSideProps<
OnboardingSerializedFormData
> = async (context) => {
const cookieFormData = await getCookieFormData(context);
try {
const isUnlocked = getIsStepUnlocked({
stepName: OnboardingStep.XY,
cookieFormData,
});
if (!isUnlocked) {
throw new Error('Step verification failed');
}
// Can also apply form values validation logic here:
// e.g., await querySchema.validate(cookieFormData.formValues);
return {
props: {
// Have to serialise again here because of how NextJS works
serializedFormValues: serializeFormValues(
cookieFormData.formValues
),
submittedSteps: cookieFormData.submittedSteps,
},
};
} catch (error) {
return {
redirect: {
destination: getRedirectionDestination(cookieFormData),
permanent: false,
},
};
}
};
We then pass through the data to the page.
export default function Page({
serializedFormValues,
submittedSteps,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<OnboardingStepXY
serializedFormValues={serializedFormValues}
submittedSteps={submittedSteps}
/>
);
}
We now deserialise the formvalues again to provide them as initial values to the formik state in the initial render. In the onSubmit function we then persist the values and get the target path for the next step.
export function OnboardingStepXY({
serializedFormValues,
submittedSteps,
}: OnboardingSerializedFormData) {
const router = useRouter();
const initialFormValues = deserializeFormValues(serializedFormValues);
return (
<Formik
initialValues={initialFormValues}
onSubmit={async (values) => {
const cookieFormData = {
formValues: values,
submittedSteps,
};
await setCookieFormData({
stepName: OnboardingStep.XY,
cookieFormData,
});
const nextStep = getNextStep({
stepName: OnboardingStep.XY,
cookieFormData,
});
await router.push({
pathname: nextStep.pathname,
query: nextStep.queryParams,
});
}}
>
<Form>...</Form>
</Formik>
);
}
Stepper
A stepper can easily be setup and configured using the generic utils. In contrast to the previous wizard, the next-wizard is highly generic and doesn't have any dependencies on Coral.
const OnboardingPage = ({
children,
cookieFormData,
stepName,
}: OnboardingPageProps) => {
const router = useRouter();
const stepperProps: StepperProps = useMemo(() => {
const currentStep = getStep({ cookieFormData, stepName });
const steps = getSteps(cookieFormData.formValues);
const stepperSteps = steps.filter(
(step) => !step.parentName && step.name !== OnboardingStep.Success
);
// Find the index of the lowest step that has not been submitted.
const lowestUnsubmittedStepIndex = steps.findIndex(
(step) => !cookieFormData.submittedSteps.includes(step.name)
);
// Total number of steps in the stepper.
const totalSteps: StepperProps['totalSteps'] = stepperSteps.length;
// Find the index of the current step in the stepper.
const activeStep: StepperProps['activeStep'] =
stepperSteps.findIndex(
(step) =>
step.name === currentStep.name ||
step.name === currentStep.parentName
) + 1;
// Generate the configuration for each step in the stepper.
const stepConfigs: StepperProps['stepConfigs'] = stepperSteps.map(
(step, i) => {
const title = {
[OnboardingStep.Quote]: 'Quote',
[OnboardingStep.Tariffs]: 'Tariffs',
[OnboardingStep.Details]: 'Details',
[OnboardingStep.Review]: 'Review',
[OnboardingStep.Success]: '',
}[step.name];
return {
title,
'aria-label': title,
/**
* Disable steps that have not been submitted yet.
* If all have been submitted then lowestUnsubmittedStepIndex will be -1.
*/
disabled:
lowestUnsubmittedStepIndex !== -1 &&
i > lowestUnsubmittedStepIndex,
};
}
);
const onStepClicked: StepperProps['onStepClicked'] = async (
stepNumber: number
) => {
const targetStep = stepperSteps[stepNumber - 1];
await router.push({
pathname: targetStep.pathname,
query: targetStep.queryParams,
});
};
return {
totalSteps,
stepConfigs,
onStepClicked,
activeStep,
};
}, [cookieFormData, router, stepName]);
return (
<Container
padding="md"
maxWidth="md"
component="section"
theme="mid"
marginX="auto"
marginBottom="lg"
>
<Stack direction="vertical" gap="sm">
{stepName !== OnboardingStep.Success && (
<Stepper {...stepperProps} />
)}
{children}
</Stack>
</Container>
);
};
export default OnboardingPage;