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 プロジェクトでは皆さんの参加と貢献を強くお勧めしています!当社はオープンソースコミュニティに継続的にご参加いただくことを希望しますが、特に熱心に取り組んでいる問題があれば1回限りの貢献でも歓迎します。

GitHub でサンプルを編集する