Skip to content

Steps on your product page

A form laid out as steps (layout: 'steps', see Page or steps) shows one group of the template at a time. Your page can follow where it stands, and put its own fields and its cart row inside the steps.

While the generated form is shown as steps, the designer’s state carries step, so onChange reports each move:

designer.getStep();
// { index: 1, count: 3, groupId: 'message', isLast: false,
// steps: [{ groupId: 'pet', label: 'Your pet' },
// { groupId: 'message', label: 'Message' },
// { groupId: 'sign', label: 'Sign' }] },
// or undefined on a page
designer.setStep(0); // back to the first step

When the template lets customers add layers of their own, the last step is the one they add them in, after the template’s groups. Its groupId is $added.

index counts from 0 among the steps the form shows, and count is how many there are; isLast is index === count - 1, the last step: the place to show your own finishing button. steps names each of them in order, as their headings read, for a stepper of your own beside the form’s. setStep(index) moves to a step, and is ignored on a page or for an index that names no step. On a page, and with showForm: false (you draw your own form, so there is no step on screen), the state has no step and setStep does nothing.

A product page often has fields of its own beside the designer, such as a shop’s variation selects, and its own price, quantity and Add to cart. Two options draw them inside the steps, so the customer goes through one flow:

mount(el, {
publishableKey,
templateId,
layout: 'steps',
optionsStep: { content: document.querySelector('.variations') },
lastStepContent: document.querySelector('.cart'),
});

optionsStep adds a first step before the template’s. It holds the blank’s colour and size picker and the print size picker, which are then no longer drawn above the form, and after them content, your own element. Its heading is title, or the form.step-options message (“Options”) without one, and getStep() names it with the group id "$options". The step bar counts it like any other step. With no picker drawn and no content, there is no Options step. The view switcher is not part of it: it stays above the form, or in viewsTarget when you pass one.

lastStepContent is shown on the last step only, under Back. Watch the step event (or isLast in getStep()) if your page needs to know when it is shown.

Both elements are moved into the designer as they are, never copied, so the listeners on them and on the controls in them keep working, and destroy() puts each back where it was. A control moved out of the <form> around it, such as a variation select taken from your cart form, stays that form’s: the designer ties it to the form with its form attribute (giving the form an id if it has none), so its value is still submitted with the form, and takes the tie off again on destroy(). A handler delegated from an ancestor, such as one listening on the cart form for a change in its selects, is no longer reached while the element is in the designer: listen on the element you pass, or on its controls. Pass elements from elsewhere in your page, not from inside the elements the designer is drawn into, which it empties. On a page, the options block opens the form, above the first numbered group and with no heading of its own, and lastStepContent ends it. Without either option, the form is drawn as it always was.

formGroups lays the template’s groups out for your page, without changing the template: which groups are shown and in what order, which fields each holds, your own elements placed among them, and the words they show. It works in both layouts: a group is a step, or a numbered heading on a page.

const d = await mount(el, {
publishableKey,
templateId,
layout: 'steps',
formGroups: {
groups: [
{
id: 'shirt',
title: 'Your shirt',
items: [
{ host: 'size', element: document.querySelector('#pa_size') },
{ field: 'name:content', label: 'Name on the back', placeholder: 'Your name' },
{ field: 'name:fontFamily', options: { Georgia: 'Classic' } },
],
},
],
},
});
document.querySelector('#pa_size').addEventListener('change', (e) =>
d.setHostItemComplete('size', e.target.value !== ''),
);
  • A group’s id is a template group’s, as getFormSchema() names it, or a new one for a group of your own, which needs a title. title replaces the group’s heading.
  • A field item names a field as "<layerId>:<prop>", from getFormSchema(). Its label, help, placeholder and options (an offered value’s words, by the value) replace the template’s for this mount, in every language, and getFieldTexts() uses them too.
  • A host item, { host, element }, moves element into the group, as optionsStep’s content is moved: never copied, put back on destroy(), and with the same caveat about handlers delegated from an ancestor.
  • In the steps layout, Next waits on a step until each host item in it is marked complete with setHostItemComplete(id, true); false holds Next again. On a page nothing waits.

Groups and fields you do not name are still shown, after yours, in the template’s order, so a template that later gains a group or a field keeps working. A field you name leaves its own group; a template group left with nothing to show is left out. An id the template does not have is passed over, with a console warning, and a malformed formGroups fails the mount with a TypeError. A host item takes over from optionsStep: the pickers are then drawn above the form. The step event and getStep() keep their shape, naming each group by its id.

Questions? Write to [email protected].

Status · Terms of service · Privacy policy · Subprocessors