Favorite Button

Edit on Github
Open in Playground

Introduction

This sample demonstrates how to implement a favorite/like/bookmark button in AMP. Our implementation:

  • shows the correct icon based on whether the user already liked an item or not. This works if the AMP is served from an AMP Cache or the original origin.
  • shows a placeholder while the current state is loaded asynchronously.
  • falls back to the original state and displays an error message if the request fails, for example when the user is offline.

Setup

We use the amp-list component to dynamically render the initial state of the favorite button.

<script async custom-element="amp-list" src="https://cdn.ampproject.org/v0/amp-list-0.1.js"></script>

The mustache component is required by amp-list.

<script async custom-template="amp-mustache" src="https://cdn.ampproject.org/v0/amp-mustache-0.1.js"></script>

We need amp-form to submit the favorite request.

<script async custom-element="amp-form" src="https://cdn.ampproject.org/v0/amp-form-0.1.js"></script>

amp-bind enables us to dynamically change the button state when we submit the form.

<script async custom-element="amp-bind" src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"></script>

Managing State

We initialize the button state from a JSON endpoint using amp-state. As we use cookies to identify the user, we need to add the credentials="include" attribute.

Example

<amp-state id="favorite"
  credentials="include"
  src="/favorite">
</amp-state>

A simple favorite Button

The button is embedded inside an amp-list, which enables us to dynamically render the button based on whether the user already liked something or not. We don't make use of amp-list's templating mechanism, but instead we use amp-bind to bind the button state to the favorite state initialized above.

Inside the amp-list we declare a placeholder icon using the placeholder attribute that is shown while the amp-list is loading.

There are a few things that are happening when the user presses the favorite button:

  • The form sends a toggle request when the user presses the button.
  • We implement an optimistic UX that will instantly update the button state when the button is pressed.
  • If the form submission fails (submit-error:...), we revert the favorite state to the original version and show an error message (favorite-failed-message.show).
  • We hide any existing error messages (favorite-failed-message.hide).

Example

<form method="post"
  action-xhr="/favorite"
  target="_top"
  on="submit:AMP.setState({favorite: !favorite}),favorite-failed-message.hide; 
        submit-error:AMP.setState({favorite: !favorite}),favorite-failed-message.show">
  <amp-list width="56"
    height="56"
    credentials="include"
    items="."
    single-item
    src="/favorite">
    <template type="amp-mustache">
      <input type="submit"
        [class]="favorite ? 'heart-fill' : 'heart-border'"
        value=""
        aria-label="Favorite Toggle">
    </template>
    <input type="submit"
      disabled
      class="heart-loading"
      value=""
      aria-label="favorite placeholder"
      placeholder>
  </amp-list>
</form>

A simple snackbar that we show when the form submission fails.

Example

<div id="favorite-failed-message"
  hidden>Error: Could not favorite.
  <div on="tap:favorite-failed-message.hide"
    tabindex="0"
    role="button">CLOSE</div>
</div>

A favorite Button with Counter

This is a more sophisticated version of the previous sample that also includes the number of favorites. Our JSON endpoint returns two values: value and count.

Example

<amp-state id="favoriteWithCount"
  credentials="include"
  src="/favorite-with-count">
</amp-state>

The implementation is similar to the previous sample, but also updates the count when the button is clicked

  AMP.setState({
  ...,
  count: favoriteWithCount.count + (favoriteWithCount.value ? -1 : 1)
  })

We use a temporary variable previousFavoriteWithCount to store the previous value in order to be able to revert the button state in case the form submission fails.

Example

0
<form method="post"
  action-xhr="/favorite-with-count"
  target="_top"
  on="submit:AMP.setState({
          previousFavoriteWithCount: favoriteWithCount,
          favoriteWithCount: {
          value: !favoriteWithCount.value,
          count: favoriteWithCount.count + (favoriteWithCount.value ? -1 : 1),
        }
        }),favorite-failed-message.hide; 
          submit-error:AMP.setState({
          favoriteWithCount: previousFavoriteWithCount
        }),favorite-failed-message.show">
  <amp-list width="200"
    height="56"
    credentials="include"
    items="."
    single-item
    noloading
    src="/favorite-with-count">
    <template type="amp-mustache">
      <div class="favorite-container">
        <input type="submit"
          [class]="favoriteWithCount.value ? 'heart-fill' : 'heart-border'"
          value=""
          aria-label="Favorite Toggle">
        <div class="favorite-count"
          [text]="favoriteWithCount.count ? favoriteWithCount.count : ''">{{count}}</div>
      </div>
    </template>
    <div placeholder>
      <div class="favorite-container">
        <input type="submit"
          disabled
          class="heart-loading"
          value=""
          aria-label="favorite placeholder">
        <div class="favorite-count loading">0</div>
      </div>
    </div>
  </amp-list>
</form>