AMP
  • websites

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>
Step 1
Here is some content ...
<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 or step-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:

What's your favorite animal?

Cat
Dog
Horse
 
<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:

Sign in

Welcome

Submitting ...

Success

You did it!

Something went wrong.

<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>
需要进一步说明?

如果此页面上的说明未能涵盖您的所有问题,欢迎与其他 AMP 用户取得联系,讨论您的具体用例。

前往 Stack Overflow
一项无法解释的功能?

AMP 项目强烈鼓励您参与并做出贡献!我们希望您能成为我们开放源代码社区的持续参与者,但我们也欢迎您对所热衷问题做出一次性贡献。

编辑 GitHub 上的示例