#BlackLivesMatter
Do you build things with AMP? Fill out the new AMP Developer Survey!
AMP
  • websites

Favorite Button

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.2.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.

<amp-state id="favorite"
  credentials="include"
  src="https://amp.dev/documentation/examples/interactivity-dynamic-content/favorite_button/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. Inside the template we use mustache's implicit iterator . to access the boolean value that is returned by our /favorite endpoint: {{#.}}heart-fill{{/.}}.

We also declare a placeholder icon inside the amp-list using the placeholder attribute which 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).
<form class="favorite-button"
  method="post"
  action-xhr="https://amp.dev/documentation/examples/interactivity-dynamic-content/favorite_button/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="https://amp.dev/documentation/examples/interactivity-dynamic-content/favorite_button/favorite"
    binding="always">
    <template type="amp-mustache">
      <input type="submit"
        class="{{#.}}heart-fill{{/.}}{{^.}}heart-border{{/.}}"
        [class]="favorite ? 'heart-fill' : 'heart-border'"
        value
        aria-label="Favorite Toggle">
    </template>
    <div placeholder>
      <input type="submit"
        disabled
        class="heart-loading"
        value
        aria-label="favorite placeholder">
    </div>
  </amp-list>
</form>

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

<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.

<amp-state id="favoriteWithCount"
  credentials="include"
  src="https://amp.dev/documentation/examples/interactivity-dynamic-content/favorite_button/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.

0
<form class="favorite-button"
  method="post"
  action-xhr="https://amp.dev/documentation/examples/interactivity-dynamic-content/favorite_button/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({
                 value: !favoriteWithCount.value,
                 favoriteWithCount: previousFavoriteWithCount.count
               }),
               favorite-failed-message.show">
  <amp-list width="200"
    height="56"
    credentials="include"
    items="."
    single-item
    noloading
    src="https://amp.dev/documentation/examples/interactivity-dynamic-content/favorite_button/favorite-with-count"
    binding="always">
    <template type="amp-mustache">
      <div class="favorite-container">
        <input type="submit"
          class="{{#value}}heart-fill{{/value}}{{^value}}heart-border{{/value}}"
          [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>
자세한 설명이 필요하신가요?

이 페이지의 설명만으로 궁금한 점이 모두 해결되지 않는다면 다른 AMP 사용자에게 문의하여 구체적인 활용 사례를 논의해 보세요.

Stack Overflow로 이동
설명이 부족한 기능을 발견하셨나요?

AMP 프로젝트는 여러분의 참여와 기여를 적극 환영합니다! 오픈 소스 커뮤니티를 통해 지속적으로 활동해 주셔도 좋지만 관심 있는 주제에 한 번만 기여하셔도 큰 도움이 됩니다.

GitHub에서 샘플 수정하기