Multi Page Flow
Introduction
This sample demonstrates different approaches for how to implement a multi-step flow in AMP. These could be used for checkout flows, sign-ups or surveys.
Setup
We use amp-bind
to coordinate the page transitions...
<script async custom-element="amp-bind" src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"></script>
... and amp-selector
for implementing a simple survey.
<script async custom-element="amp-selector" src="https://cdn.ampproject.org/v0/amp-selector-0.1.js"></script>
A simple Dialog
We use an implicit state variable currentPage
to keep track of the current view. Views are identified by numbers starting with 0
in their order of appearance. The view state is bound to each view using AMP's hidden
attribute:
<section [hidden]="currentPage > 0"> ... </section>
...and for initially hidden views we add the hidden
attribute as a default:
<section hidden [hidden]="currentPage != 1"> ... </section>
We update the currentPage
variable to progress forward in the dialog. In this sample we're using AMP.pushState(...)
instead of AMP.setState(...)
. AMP.pushState(...)
pushes a new entry onto the browser history stack, which allows the user to navigate back in the to use the browser's back button to move backwards in the dialog:
<button on="tap:AMP.pushState({ currentPage: currentPage + 1 })"> next </button>
<div class="stepper simple">
<section [hidden]="currentPage > 0">
<div class="top-bar">
Step 1
</div>
<div class="content">Here is some content ...</div>
<div class="bottom-bar">
<div class="step-dots">
<i class="step-dot active"></i>
<i class="step-dot"></i>
<i class="step-dot"></i>
</div>
<button on="tap:AMP.pushState({ currentPage: currentPage + 1 })" class="button-next">next</button>
</div>
</section>
<section hidden [hidden]="currentPage != 1">
<div class="top-bar">
Step 2
</div>
<div class="content">Here is some more content ...</div>
<div class="bottom-bar">
<button class="button-prev" on="tap:AMP.pushState({ currentPage: currentPage - 1 })">back</button>
<div class="step-dots">
<i class="step-dot active"></i>
<i class="step-dot active"></i>
<i class="step-dot"></i>
</div>
<button on="tap:AMP.pushState({ currentPage: currentPage + 1 })" class="button-next">next</button>
</div>
</section>
<section hidden [hidden]="currentPage != 2">
<div class="top-bar">
Step 3
</div>
<div class="content">Done!</div>
<div class="bottom-bar">
<button class="button-prev" on="tap:AMP.pushState({ currentPage: currentPage - 1 })">back</button>
<div class="step-dots">
<i class="step-dot active"></i>
<i class="step-dot active"></i>
<i class="step-dot active"></i>
</div>
</div>
</section>
</div>
A vertical Stepper
Vertical steppers work well if steps depend on each other. The stepper is implemented similar to the first sample using a currentStep
variable to keep track of the currently active step. We additionally define a step title button which is always visible and shows the status of the current step. The title needs to reflect three different states (active, complete, disabled). To avoid too complex amp-bind expressions, the three states are split into three different bindings:
- The title class gets updated if the current step is active.
- The title gets a
disabled
attribute if the previous step has not yet been completed - The nested icon's class gets set to
step-complete
orstep-incomplete
based on whether the step has finished.
Clicking on the title will go to the corresponding step (if already possible):
<button class="step-title" [class]="currentStep != 1 ? 'step-title' : 'step-title step-active'" disabled [disabled]="!animalSelected" on="tap:AMP.pushState({ currentStep: 1 })"> <i class="step-incomplete" [class]="colorSelected ? 'step-complete' : 'step-incomplete'" data-step-nr="2"></i> Color </button>
By default, the Continue button is disabled. Only when the step is completed (in this case when a selection has been made), will the button be enabled:
<button disabled [disabled]="!animalSelected" on="tap:AMP.pushState({ currentStep: currentStep + 1 })"> continue </button>
We've enclosed the vertical stepper in a submittable form. Upon Submit
, the user will see that their selections are in the URL parameter string.
Here is the full example:
<form class="multistep-form" method="get" action="#" target="_top">
<div class="stepper vertical">
<button class="step-title step-active" [class]="currentStep > 0 ? 'step-title' : 'step-title step-active'" on="tap:AMP.pushState({ currentStep: 0 })">
<i class="step-incomplete" [class]="animalSelected ? 'step-complete' : 'step-incomplete'" data-step-nr="1"></i>
Animal
</button>
<section [hidden]="currentStep > 0">
<div class="content">
<p>What's your favorite animal?</p>
<amp-selector on="select:AMP.setState({animalSelected: true})" class="poll" name="animal-poll">
<div option="cat">Cat</div>
<div option="dog">Dog</div>
<div option="horse">Horse</div>
</amp-selector>
<button disabled [disabled]="!animalSelected" on="tap:AMP.pushState({ currentStep: currentStep + 1 })">
continue
</button>
</div>
</section>
<button class="step-title" [class]="currentStep != 1 ? 'step-title' : 'step-title step-active'" disabled [disabled]="!animalSelected" on="tap:AMP.pushState({ currentStep: 1 })">
<i class="step-incomplete" [class]="colorSelected ? 'step-complete' : 'step-incomplete'" data-step-nr="2"></i>
Color
</button>
<section hidden [hidden]="currentStep != 1">
<div class="content">
<p>What's your favorite color?</p>
<amp-selector on="select:AMP.setState({colorSelected: true})" class="poll" name="color-poll">
<div option="blue">Blue</div>
<div option="green">Green</div>
<div option="yellow">Yellow</div>
</amp-selector>
<button disabled [disabled]="!colorSelected" on="tap:AMP.pushState({ currentStep: currentStep + 1 })">
continue
</button>
</div>
</section>
<button class="step-title" [class]="currentStep != 2 ? 'step-title' : 'step-title step-active'" disabled [disabled]="!colorSelected" on="tap:AMP.pushState({ currentStep: 2 })">
<i class="step-incomplete" [class]="fruitSelected ? 'step-complete' : 'step-incomplete'" data-step-nr="3"></i>
Fruit
</button>
<section hidden [hidden]="currentStep != 2">
<div class="content">
<p>What's your favorite fruit?</p>
<amp-selector on="select:AMP.setState({fruitSelected: true})" class="poll" name="fruit-poll">
<div option="apple">Apple</div>
<div option="banana">Banana</div>
<div option="cheery">Cherry</div>
</amp-selector>
<button disabled [disabled]="!fruitSelected" on="tap:AMP.pushState({ currentStep: currentStep + 1 })">
continue
</button>
</div>
</section>
</div>
<input type="submit" value="Submit">
</form>
Stepper with Sliding Animation
This sample demonstrates a simple sliding animation visualising the dialog progress. The basic approach is the same as in the previous two samples: a variable keeps track of the current page. The only difference is that here we can't use the hidden
attribute as we want to transition between the different pages. The hidden
attribute uses display: none
, which cannot be animated in CSS. Instead we use three different CSS classes (active
, next
and previous
) to slide between the different pages:
.page.active { transform: translateX(0%); pointer-events: auto; } .page:not(.active) { opacity: 0.5; pointer-events: none; } .page.next { transform: translateX(100%); } .page.previous { transform: translateX(-100%); }
For each page we assign the matching class based on whether the page index is smaller, same or larger:
<section class="page next" [class]="slidingStepperPage < 1 ? 'page next' : (slidingStepperPage > 1 ? 'page previous' : 'page active')"> ...</section>
To avoid accidentally revealing hidden steps via keyboard focus, we need to make sure to explicitly disable all input elements
in hidden steps, e.g.:
<input type="text" value="" name="password" disabled [disabled]="slidingStepperPage != 1" ...>
We sync the entered email address between the two steps using an amp-state variable email
:
<input type="email" value="" name="email" on="input-debounced: AMP.setState({ email: event.value })" ...> ... <button class="back" [text]="email" ...></button>
Here is the full example:
<form class="stepper sliding" method="post" action-xhr="/documentation/examples/api/submit-form-input-text-xhr" novalidate on="submit: AMP.setState({ slidingStepperPage: 2 });
submit-success: AMP.setState({ slidingStepperPage: 3 });
submit-error: AMP.setState({ slidingStepperPage: 4 });
">
<section class="page active" [class]="slidingStepperPage > 0 ? 'page previous' : 'page active'">
<h3>Sign in</h3>
<div class="input">
<input type="email" value name="email" autocomplete="email" id="id1" placeholder="Enter your Email" on="input-debounced: AMP.setState({ email: event.value })">
<label for="ip1" aria-hidden="true">
Enter your Email
</label>
</div>
<button type="button" on="tap:AMP.pushState({ slidingStepperPage: slidingStepperPage + 1 })" disabled [disabled]="!email">next</button>
</section>
<section class="page next" [class]="slidingStepperPage < 1 ? 'page next' :
(slidingStepperPage > 1 ? 'page previous' : 'page active')">
<h3>Welcome</h3>
<button class="back" [text]="email" on="tap:AMP.pushState({ slidingStepperPage: slidingStepperPage - 1 })" disabled [disabled]="slidingStepperPage != 1" type="button"></button>
<div class="input">
<input type="password" value name="password" id="id2" placeholder="Enter your Password" disabled [disabled]="slidingStepperPage != 1" on="input-debounced: AMP.setState({ password: event.value })">
<label for="ip2" aria-hidden="true">
Enter your Password
</label>
</div>
<button disabled [disabled]="slidingStepperPage != 1 || !password" type="submit">submit</button>
</section>
<section class="page next" [class]="slidingStepperPage < 2 ? 'page next' :
(slidingStepperPage > 2 ? 'page previous' : 'page active')">
<p>Submitting ...</p>
</section>
<section class="page next" [class]="slidingStepperPage < 3 ? 'page next' :
(slidingStepperPage > 3 ? 'page previous' : 'page active')">
<h3>Success</h3>
<p>You did it!</p>
</section>
<section class="page next" [class]="slidingStepperPage < 4 ? 'page next' :
(slidingStepperPage > 4 ? 'page previous' : 'page active')">
<h3>Something went wrong. </h3>
<button on="tap:AMP.setState({ slidingStepperPage: 0 })" type="button">Try again</button>
</section>
</form>
If the explanations on this page don't cover all of your questions feel free to reach out to other AMP users to discuss your exact use case.
Go to Stack Overflow An unexplained feature?The AMP project strongly encourages your participation and contributions! We hope you'll become an ongoing participant in our open source community but we also welcome one-off contributions for the issues you're particularly passionate about.
Edit sample on GitHub-
Written by @sebastianbenz