amp-bindで独自のインタラクティブ機能を作成する

UI開発者 木村

これまで試験的に公開されていたamp-bindというコンポーネントが一般に公開されました。

amp-bindは別々のコンポーネントを連動させたり、独自のインタラクティブな機能を作成したりすることができ、カルーセルにインジゲーターを実装したり、ボタンのタップイベントなどに応じてテキストや画像を入れ替えることが可能となりました。

AMP Projectはamp-bindをECサイトで使用することを意識しており、Advanced Interactivity in AMPにECサイトの商品詳細ページを実装するチュートリアルが掲載されています。チュートリアルではamp-bindをはじめ、amp-selectoramp-mustacheといったまだ実例が多くはないコンポーネントを実際に手を動かしながら体験できるので、気になる方はチュートリアルの体験をおすすめします。

今回はamp-bindを使用して、ECサイト以外でも比較的使用する機会が多いタブを作成してみます。シンプルな機能ですが、AMP HTMLにはタブを実装するためのコンポーネントがないため、実装することができませんでした。ですが、amp-bindを使用して独自のインタラクティブ機能を作成することによって、タブの機能を再現することができます。次が実際にタブを実装したサンプルソースです。

<!DOCTYPE html>
<html amp>
<head>
<meta charset="utf-8">
<title>amp-bind sample</title>
<link rel="canonical" href="./">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0">
<style amp-custom>
.media,.tabMenu>ul>li,h1{text-align:center}.tabContent,.tabMenu button,.tabMenu>ul{border-color:#ccc;border-style:solid}*{margin:0;padding:0;box-sizing:border-box}ol,ul{padding:0 0 0 20px}h1{font-size:28px;margin:0 0 8px;padding:8px 0}.tabMenu{display:none}html[amp-version] .tabMenu{display:block}.tabMenu>ul{border-width:1px;list-style:none;padding:0;display:-webkit-flex;display:flex;align-items:center}.tabMenu>ul>li{flex:1;display:-webkit-flex;display:flex}.tabMenu button{display:block;border-width:0 1px 0 0;width:100%;min-height:40px;background:#eee;color:#333;cursor:pointer;word-spacing:break-all;-webkit-appearance:none}.tabMenu button.current{background:#39c;color:#fff}.tabMenu>ul>li:last-child button{border-width:0}.tabContent{padding:16px;border-width:0 1px 1px}.tabContent h2{font-size:24px;margin:0 0 16px}html[amp-version] .tabContent{display:none}html[amp-version] .tabContent.current{display:block}p{margin:0 0 16px;padding:0 8px}main{max-width:640px;margin:0 auto}
</style>
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
<script async custom-element="amp-bind" src="https://cdn.ampproject.org/v0/amp-bind-0.1.js"></script>
</head>
<body>
<main>
<h1>amp-bind sample</h1>

<amp-state id="tabState">
<script type="application/json">
{
  "index": 0
}
</script>
</amp-state>

<p [text]="(tabState.index + 1) + '番目のタブ'">1番目のタブ</p>

<div class="tab">
<div class="tabMenu">
<ul>
<li><button type="button" on="tap:AMP.setState({tabState: {index: 0}})" [class]="tabState.index == 0 ? 'current' : ''" class="current">Tab 1</button></li>
<li><button type="button" on="tap:AMP.setState({tabState: {index: 1}})" [class]="tabState.index == 1 ? 'current' : ''">Tab 2</button></li>
<li><button type="button" on="tap:AMP.setState({tabState: {index: 2}})" [class]="tabState.index == 2 ? 'current' : ''">Tab 3</button></li>
</ul>
<!-- /.tabMenu --></div>

<section [class]="tabState.index == 0 ? 'tabContent current' : 'tabContent'" class="tabContent current">
<h2>Tab 1</h2>
<amp-img src="dummy-01.png" width="300" height="150" layout="responsive" alt="dummy"></amp-img>
<!-- /.tabContent --></section>

<section [class]="tabState.index == 1 ? 'tabContent current' : 'tabContent'" class="tabContent">
<h2>Tab 2</h2>
<amp-img src="dummy-02.png" width="300" height="240" layout="responsive" alt="dummy"></amp-img>
<!-- /.tabContent --></section>

<section [class]="tabState.index == 2 ? 'tabContent current' : 'tabContent'" class="tabContent">
<h2>Tab 3</h2>
<amp-img src="dummy-03.png" width="300" height="480" layout="responsive" alt="dummy"></amp-img>
<!-- /.tabContent --></section>
<!-- /.tab --></div>
</main>
</body>
</html>

amp-bindは、StateExpressionsBindingsという3つ構成要素で成り立っています。それぞれサンプルソースを例に解説します。

State

Stateはコンポーネントの状態や、amp-bindによって差し替えるコンテンツを定義します。

定義するためには、amp-state要素を使用しJSON形式で値を格納します。サンプルソースでは現在表示しているタブのインデックスを保持させるために、次のように記述します。

<amp-state id="tabState">
<script type="application/json">
{
  "index": 0
}
</script>
</amp-state>

またamp-state要素は、次のように外部のJSONファイルを読み込むことも可能で、複数ページで使用する共通データの場合は外部ファイルにしてしまうのも一つの手です。ですが、外部ファイルの読み込みはパフォーマンス低下の原因の一つなので、AMPの特性をより生かすためにはできるだけ使用しない方がよいと思います。

<amp-state id="tabState" src="tabState.json"></amp-state>

amp-stateの値を更新するためには、次のようにon属性のイベントでAMP.setState()を実行します。更新する対象とその値を引数にオブジェクト形式で記述します。引数のtabStateの部分は対象のamp-state要素のid属性値を記述します。

on="tap:AMP.setState({tabState: {index: 1}})"

今回はtapイベントで値を更新していますが、slideselectといった他のコンポーネント独自のイベントも使用できるため、様々なタイミングでamp-stateの値を更新することができます。

Expressions

Expressionsはamp-state要素の値を使用した式をHTML属性の値に記述することで、テキストや属性の値をamp-state要素の値に応じて変更する機能です。

まず、サンプルソースを次のように記述することで現在表示しているタブのインデックス番号をテキストで表示しています。tabStateは参照するamp-state要素のid属性を記述します。

<p [text]="(tabState.index + 1) + '番目のタブ'">1番目のタブ</p>

さらに次のように、三項演算子を使用することでamp-stateの値に応じて固有の属性値を持たせることも可能です。今回のサンプルソースでは、amp-state要素のindexの値が自身のインデックス番号と一致していればcurrentというclassが付与され、タブの現在位置の見た目やコンテンツの表示切り替えを行っています。

[class]="tabState.index == 2 ? 'current' : ''"
Bindings

Bindingsは、StateとExpressionsを結びつけるための構成要素で、amp-state要素内の値が更新された時に、その値を使用している式を再評価する機能です。Expressionsの例ですでに登場しましたが、[class]のように属性名を角括弧で囲むことによって、その属性値の式を自動的に再評価します。

今回のタブ機能では、button要素をタップするとamp-state要素のindexの値が更新され、Bindings機能によって[text]を持つp要素と[class]の各種要素が自動的に更新され、タブの切り替えを実現しています。

まとめ

今回タブを実装したように、AMP HTMLのコンポーネントにない機能でも、amp-bindを使用すれば独自のインタラクティブ機能として実装可能な場合があります。

注意点として、amp-state要素はHTML内もしくは外部のJSONファイルに定義する必要があるため、ページの更新を静的に行う必要がある場合は運用や管理が難しくなる点が挙げられます。そのため、ECサイトのようにシステムで動的にJSONの値を書き換えられるページの方がより大きな効果を発揮できます。

ですが、静的ページであってもキャンペーン物のランディングページや特設サイトであれば、運用や管理のコストは比較的かからないと思われます。さらに、機能によってはamp-bindを用いることによってJavaScriptで1から作成するよりも簡単に実装できる可能性があります。amp-bindの登場によりAMPで実現できる機能が大きく広がりました。これを機に、記事ページなどだけではなくランディングページなどでもAMP対応を検討してみてはいかがでしょうか。