Alpine.jsでドロップダウンメニューをスクラッチから作成

Alpine.jsはドロップダウンメニューやタブの切り替え、モーダルなどの機能をVue.jsやReactなど利用せず手軽に実装したい場合に利用できるJavaScriptのフレームワークです。実際に利用してみるとその手軽さを実感することができます。

本文書ではLaravelフレークワークでも利用されているドロップダウンメニューの作成に必要なAlpine.jsの基礎を説明し実際にスクラッチからドロップダウンメニューを作成します。
目次
Alpine.jsを使うための準備
index.htmlファイルを作成してcdnを使ってalpine.jsをロードします。これでAlpline.jsを使うための準備は完了です。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.1/dist/alpine.js" defer></script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
初めてAlpine.js
Alpine.jsを使ってh1タグで表示されているHello Worldを表示させてみましょう。
divタグにx-dataを設定します。Alpine.jsのドキュメントでは”x-dataは新しいコンポーネントスコープを宣言します”と記述されていますがもし意味がわからなければAlpine.jsを利用するために必要な宣言と理解しておきましょう。x-dataの中では、messageにHello Worldを設定しているのでx-dataを設定したdivの内側であればこのmessageプロパティを自由に利用することができます。
<body>
<div x-data="{message: "Hello World"}"></div>
</body>
messageをブラウザに表示させるためにx-textディレクティブを追加します。x-textにmessageを設定することでh1タグにmessageの内容が挿入され表示されます。
<div x-data="{message: 'Hello World'}">
<h1 x-text="message"></h1>
</div>
ブラウザで確認するとmessageに設定されたHello Worldが表示されます。

x-dataのdivの内側(スコープの範囲)であれば複数のv-textを利用してもmessageを表示させることができますがその範囲外になるとv-textは利用できません。
<body>
<div x-data="{message: 'Hello World'}">
<h1 x-text="message"></h1>
<h1 x-text="message"></h1>
<h1 x-text="message"></h1>
</div>
<h1 x-text="message"></h1>
</body>
ブラウザで見るとスコープの範囲を超えたx-textは表示されないことが確認できます。

1つのHTMLページの中に複数のx-dataを宣言することも可能です。messageに異なる値を設定しています。
<body>
<div x-data="{message: 'Hello World'}">
<h1 x-text="message"></h1>
</div>
<div x-data="{message: 'Hello Alpine.js'}">
<h1 x-text="message"></h1>
</div>
</body>

messageに対してStringとしてJavaScriptのメソッドを実行することも可能です。
<h1 x-text="message.split('').reverse().join('')"></h1>

clickイベントの設定
clickイベントを利用して他の処理を行うことができます。ここではclickイベントを検知してmessageの文字列を別の文字列で上書きします。button要素を追加しclickイベントに対してイベントリスナーを設定することができます。clickイベントを設定する時はx-on:をclickの前に追加します。
<div x-data="{message: 'Hello World'}">
<h1 x-text="message.split('').reverse().join('')"></h1>
<button x-on:click="message = 'Hello Alpine.js'">クリック</button>
</div>
ボタンをクリックするとmessageが上書きされます。

x-on:clickと記述しましたが@clickと記述することもできます。clickイベントを利用することでユーザはインタラクティブな処理を行うことができます。
<button @click="message = 'Hello Alpine.js'">クリック
clickイベントとx-showの設定
Alpine.jsはclickイベントを使って要素の表示・非表示によく利用されます。ドロップダウンメニューやモーダルなどです。表示・非表示はx-showディレクティブを利用して制御します。x-showにはtrue or falseを設定します。
x-dataの中でisShowプロパティを設定しtrueとします。h1タグにx-show=”isShow”を設定していますがisShowがfalseなのでh1タグは表示されません。
<div x-data="{isShow: false}">
<h1 x-show="isShow">Hello World</h1>
</div>
isShowの値をfalseからtrueに変更するとHello Worldがブラウザに表示されます。
<div x-data="{isShow: true}">
<h1 x-show="isShow">Hello World</h1>
</div>
clickイベントを使ってmessageの文字列を上書きできたようにisShowの値を更新できるようにclickイベントをボタンに設定します。
<div x-data="{isShow: false}">
<h1 x-show="isShow">Hello World</h1>
<button @click="isShow = !isShow">表示・非表示</button>
</div>
最初はisShowがfalseなのでボタンのみが表示されている状態です。

”表示・非表示”ボタンをクリックするとHello Worldが表示されます。

ボタンにv-textと三項演算子を利用することでisShowの値によってボタンに表示される文字も切り替えることができます。
<button @click="isShow = !isShow" x-text="isShow ? '非表示':'表示'"></button>
たったこれだけの設定で表示・非表示を繰り返すことができるToggle機能を実装することが可能です。

実践編ドロップダウンメニューを作成
ここまでのAlpine.jsの機能を利用してAlpine.jsが頻繁に利用させるドロップダウンメニューを作成することができます。Step By Stepでドロップダウンメニューを作成していきますがCSSにはtailwindcssを利用します。tailwindcssを使うとJavaScriptにおけるAlpine.jsのように手軽にCSSを設定することができます。
cdnを経由してtailwindcssを読み込むので以下のlinkタグをheaderタグに追加してください。
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.1/dist/alpine.js" defer></script>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
ドロップダウンメニューをスクラッチから作成といってもほとんどの時間はドロップダウンメニューのCSSの設定に費やします。Alpine.jsを設定する処理はわずかです。それぐらいAlpine.jsは手軽に利用することができます。
ヘッダーとメインコンテンツ領域を作成
ヘッダーの背景色は黒に設定し、classでh-20の高さを設定します。
<body>
<header class="bg-black h-20">
</header>
<main class="m-4">
<p>ここからがコンテンツ</p>
</main>
</body>

左側にログ、右側にドロップダウンメニューを表示させるためにflexを利用します。両端に表示させるためjustify-betweenを設定し、items-centerで中央揃えにしています。
<body>
<header class="bg-black h-20 flex items-center justify-between p-4">
<div class="text-white font-bold">ロゴ</div>
<div>
<button class="text-white">ドロップダウンメニュー</button>
</div>
</header>
<main class="m-4">
<p>ここからがコンテンツ</p>
</main>
</body>

ドロップダウンにメニューを追加
ドロップダウンにメニューを追加します。3つのメニューをaタグで追加しています。
<div>
<button class="text-white">ドロップダウンメニュー</button>
<div>
<a href="#">Link1</a>
<a href="#">Link2</a>
<a href="#">Link3</a>
</div>
</div>
aタグはインライン要素なので横並びに表示されていますが背景色が黒でフォントの色もデフォルトの黒なので何も表示されませんがドロップメニューの下には存在しています。

メニューをインライン要素からブロック要素に変更します。メニューの外側のdivに背景色の白を設定します。
<div>
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white">
<a href="#" class="block">Link1</a>
<a href="#" class="block">Link2</a>
<a href="#" class="block">Link3</a>
</div>
</div>
メニューが表示されるようになりました。

メニューにabsoluteを設定し基準となる要素である外側の要素にrelativeを設定します。
<div class="relative">
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white absolute">
<a href="#" class="block">Link1</a>
<a href="#" class="block">Link2</a>
<a href="#" class="block">Link3</a>
</div>
</div>
ドロップダウンメニューが開い時の状態に近づいてきました。

padding、margin、メニューの横幅、表示位置が右側の0が記述になるように変更します。
<div class="relative">
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1">
<a href="#" class="block p-2">Link1</a>
<a href="#" class="block p-2">Link2</a>
<a href="#" class="block p-2">Link3</a>
</div>
</div>
横幅やmargin, paddingを設定したのでよりドロップダウンメニューらしくなりました。

メニューと背景の境界がわかりくいのでshadowを設定し、hoverを設定してマウスが上にきた時は背景色が変わるように設定します。
<div class="relative">
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md">
<a href="#" class="block p-2 hover:bg-gray-100">Link1</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link2</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link3</a>
</div>
</div>

ここまででドロップダウンメニューのCSS設定は完了です。
Alpine.jsの設定
ドロップダウンメニューをクリックするとメニューが表示・非表示を切り替えられるようにAlpine.jsの設定を行います。
ドロップダウンメニューの外側のdiv要素にx-dataを追加してisOpenプロパティを追加します。
<div class="relative" x-data="{ isOpen:false }">
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md">
<a href="#" class="block p-2 hover:bg-gray-100">Link1</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link2</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link3</a>
</div>
</div>
メニューの外側のdiv要素にx-showでisOpenを設定します。
<div class="relative" x-data="{ isOpen:false }">
<button class="text-white" @click="isOpen = !isOpen">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md" x-show="isOpen">
<a href="#" class="block p-2 hover:bg-gray-100">Link1</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link2</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link3</a>
</div>
</div>
x-dataのisOpenの値がfalseの場合のみメニューが非表示になるか確認してください。次はtrueにしてメニューが表示されればAlpine.jsは正常に動作しています。
ドロップダウンメニューをクリックすると表示・非表示が切り替わるようにclickイベントを設定します。
最初はメニューが閉じているのでクリックすると表示されます。

再度クリックするとドロップダウンメニューが非表示になります。

クリックした際にボタンの外側にラインが表示されるので表示しないようにbutton要素にoutline-noneのclassを設定します。
<div class="relative" x-data="{ isOpen:false }">
<button class="text-white focus:outline-none" @click="isOpen = !isOpen">ドロップダウンメニュー</button>
away modifierの追加
設定したドロップダウンメニューはドロップダウンメニューボタンをクリックすると表示・非表示が切り替わります。x-dataの範囲外でクリックするとメニューが閉じるようにaway modifierを設定します。
<div class="relative" x-data="{ isOpen:false }" @click.away = "isOpen = false">
<button class="text-white focus:outline-none" @click="isOpen = !isOpen">ドロップダウンメニュー</button>
開いた状態でドロップダウンメニュー以外の領域をクリックするとメニューが閉じます。

そのほかに@keydown.escapeを設定するとESCキーを押してもメニューを閉じさせることができます。
<div class="relative" x-data="{ isOpen:false }" @click.away = "isOpen = false" @keydown.escape="isOpen = false>
Alpine.jsを使ってドロップメニューの完成です。CSSの調整に時間がかかるだけでAlpine.jsの設定はほとんどなく簡単にドロップダウンメニューが作成できることができました。
アニメーションの設定
ドロップダウンメニューをクリックすると表示・非表示が即座に行われるのでアニメーションを設定してスムーズに切り替えが行えるようにします。x-trantisionディレクティブを利用しますが例がhttps://github.com/alpinejs/alpineに公開されているのでそれらの値を利用します。x-showディレクティブが設定されている要素に追加します。
<div class="relative" x-data="{ isOpen:false }" @click.away = "isOpen = false" @keydown.escape="isOpen = false">
<button class="text-white focus:outline-none" @click="isOpen = !isOpen">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md"
x-show="isOpen"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-90"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-90"
>
<a href="#" class="block p-2 hover:bg-gray-100">Link1</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link2</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link3</a>
</div>
</div>
設定後はゆっくりとメニューが表示・非表示されることが確認できます。
x-transtionディレクティブを複数記述しなくても細かな設定必要ない場合は以下のように設定することも可能です。2秒をかけてゆっくりと表示・非表示されます。
x-show.transition.duration.2000ms="isOpen"
設定方法についてはドキュメントにも記載されていますがここにも貼り付けておきます。各値を変更することで最適な値を見つけてください。

以上でアニメーションを入れたドロップダウンメニューのAlpine.jsの設定は完了です。
ドロップダウンメニューを作成を通してAlpine.jsの手軽さが実感できたのではないでしょうか。