JavaScriptを使えば、簡単にインタラクティブなWebアプリケーションが開発できます。しかし、JavaScriptに不慣れな方にとっては、ちょっとした操作のためにプログラミングを書くのは敷居が高く感じるでしょう。

そこで使ってみたいのがAlpine.jsです。以前紹介したhtmxが通信処理に特化しているのに対して、Alpine.jsはUIの操作に特化しています。

今回は、このAlpine.jsを使って、簡単にインタラクティブなUIを作る方法を紹介します。

基本形

Alpine.jsで紹介されている、最も基本的な形は以下の通りです。

<html>
<head>
    <title>Alpine.js CDN</title>
    <!-- utf-8 -->
    <meta charset="utf-8">
    <!-- CDNリンクは小文字の "alpinejs" を使用することに注意 -->
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
</head>
<body>
  <div x-data="{ count: 0 }">
    <button x-on:click="count++">Increment</button>
    <span x-text="count"></span>
  </div>
</body>
</html>
カウンターの例

x-* で記述されている要素がAlpine.jsのディレクティブです。x-data はデータを定義するディレクティブで、x-on はイベントを定義するディレクティブです。なお、x-on は @ で省略できます。

<button @click="count++">Increment</button>

そして、x-text は要素のテキストをバインドするディレクティブです。これにより、count の値が変更されると、その値が自動的に表示されます。

たとえば、以下のようなDOMを書けば、カウントダウンしたり、リセット処理を追加できます。

<button @click="count--">Decrement</button>
<button @click="count=0">Reset</button>

他のディレクティブ

Alpine.jsには、他にも多くのディレクティブが用意されています。たとえば、以下のようなディレクティブがあります。

x-show

x-show ディレクティブは、条件に応じて要素を表示・非表示するディレクティブです。以下はボタンのクリックで表示・非表示を切り替えられます。ボタンを押すと open の値が反転し、open = true の場合は表示され、open = false の場合は非表示になります。

<html>
<head>
    <title>Alpine.js CDN</title>
    <!-- utf-8 -->
    <meta charset="utf-8">
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
</head>
<body>
  <div x-data="{ open: false }">
    <button @click="open = ! open">トグル</button>
    <div x-show="open" @click.outside="open = false">内容...</div>
  </div>
</body>
</html>
トグルの例

@click.outside は、要素外をクリックしたときに処理 open = false が実行されるディレクティブです。これは特にドロップダウンメニューやモーダルウィンドウの実装に便利です。

x-effect

x-effect ディレクティブは、データが変更されたときに処理を実行するディレクティブです。初期化時にも一度実行される点に注意してください。以下は、label の値が変更されるたびにコンソールに出力されます。これはデバッグする際に便利です。

<html>
<head>
    <title>Alpine.js CDN</title>
    <!-- utf-8 -->
    <meta charset="utf-8">
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
</head>
<body>
  <div x-data="{ label: "Hello" }" x-effect="console.log(label)">
    <button @click="label += " World!"">Change Message</button>
  </div>
</body>
</html>
エフェクトの例

x-transition

x-transition ディレクティブは、要素の表示・非表示時にアニメーションを付けるディレクティブです。以下は、要素の表示・非表示時にフェードイン・フェードアウトのアニメーションが付けられます。デフォルトはフェード・スケールが変更されて表示・非表示を行います。

<div x-data="{ open: false }">
  <button @click="open = ! open">Toggle</button>
  <div x-show="open" x-transition>
      Hello 
  </div>
</div>

より詳細な制御をしたい場合は、以下のような修飾子を使用できます:

<!-- エントリーアニメーション用のディレクティブ -->
<div 
  x-show="open" 
  x-transition:enter="transition ease-out duration-300"
  x-transition:enter-start="opacity-0 scale-90"
  x-transition:enter-end="opacity-100 scale-100"
>
<!-- 退出アニメーション用のディレクティブ -->
<div 
  x-show="open" 
  x-transition:leave="transition ease-in duration-300"
  x-transition:leave-start="opacity-100 scale-100"
  x-transition:leave-end="opacity-0 scale-90"
>

オプションとして durationdelayopacityscale などを指定することができます。

<div ... x-transition:enter.duration.500ms></div>
<div ... x-transition:leave.duration.400ms></div>
<div ... x-transition.delay.50ms></div>
<div ... x-transition.opacity></div>
<div ... x-transition.scale.80></div>

HTML表示

x-text では、指定した文字列がそのまま表示されます。HTMLを使いたい場合には、x-html を使います。ただし、ユーザー入力を直接表示する場合はXSS攻撃のリスクがあるため注意が必要です。

<div x-data="{ username: "<strong>calebporzio</strong>" }">
  Username: <span x-html="username"></span>
</div>

x-model

入力欄を使う際には、x-model を使って値を取得します。これは双方向バインディングを提供し、入力値が自動的にデータに反映され、逆にデータが変更されると入力欄の表示も更新されます。以下は、入力した値を取得して表示する例です。

<div x-data="{ message: "" }">
    <input type="text" x-model="message">
    <span x-text="message"></span>
</div>

x-model はテキスト入力だけでなく、チェックボックスやラジオボタン、セレクトボックスなど様々な入力要素でも使えます:

<!-- チェックボックスの例 -->
<div x-data="{ isChecked: false }">
    <input type="checkbox" x-model="isChecked">
    <span x-show="isChecked">チェックされています!</span>
</div>

<!-- セレクトボックスの例 -->
<div x-data="{ selectedOption: "" }">
    <select x-model="selectedOption">
        <option value="">選択してください</option>
        <option value="option1">オプション1</option>
        <option value="option2">オプション2</option>
    </select>
    <span x-text="selectedOption"></span>
</div>

x-if

条件に応じて要素を表示する際には、x-if を使って条件分岐を行います。x-show との違いは、x-show はCSSで表示・非表示を切り替えるのに対し、x-if はDOM自体を追加・削除します。以下は、open の値が true の場合に要素を表示します。

<template x-if="open">
  <div>Contents...</div>
</template>

x-for

リストを表示する際には、x-for を使って繰り返し処理を行います。以下は、リストを表示する例です。表示する内容は <template /> タグで囲みます。

<ul x-data="{ colors: ["Red", "Orange", "Yellow"] }">
    <template x-for="color in colors">
        <li x-text="color"></li>
    </template>
</ul>

オブジェクトの配列の場合は key を指定します。これは効率的なDOMの更新とレンダリングのために重要です。

<ul x-data="{ colors: [
    { id: 1, label: "Red" },
    { id: 2, label: "Orange" },
    { id: 3, label: "Yellow" },
]}">
    <template x-for="color in colors" :key="color.id">
        <li x-text="color.label"></li>
    </template>
</ul>

インデックスを使用したい場合は、次のようにします:

<template x-for="(color, index) in colors" :key="index">
    <li x-text="${index + 1}: ${color}"></li>
</template>

x-ignore

x-ignore ディレクティブは、要素をAlpine.jsの処理から除外するディレクティブです。以下は、x-ignore が指定された要素はAlpine.jsによって処理されません。これは他のJavaScriptライブラリと競合する部分がある場合に便利です。

<div x-data="{ label: "From Alpine" }">
    <div x-ignore>
        <!-- 以下は無視されます -->
        <span x-text="label"></span>
    </div>
</div>

x-ref

x-ref ディレクティブは、要素を参照するディレクティブです。これによりDOMを直接操作することができます。以下は、ボタンを押すと x-ref で指定した text のDOMを削除します。

<button @click="$refs.text.remove()">Remove Text</button>
<span x-ref="text">Hello </span>

追加のディレクティブと修飾子

x-cloak

ページの読み込み中にテンプレートが一瞬表示されるのを防ぐためのx-cloakディレクティブです。CSSと組み合わせて使用します:

<style>
  [x-cloak] { display: none !important; }
</style>

<div x-data="{ ready: false }" x-cloak>
  <!-- ページが完全に読み込まれるまで表示されません -->
</div>

x-init

コンポーネントの初期化時に実行されるコードを指定できます:

<div x-data="{ message: "Hello" }" x-init="console.log("Component initialized")">
  <span x-text="message"></span>
</div>

イベント修飾子

Alpine.jsでは、イベントハンドラに様々な修飾子を追加できます:

<!-- Enter キーが押されたときだけ発火 -->
<input @keyup.enter="submitForm">

<!-- 右クリック時のイベント -->
<button @click.right="showContextMenu">右クリック</button>

<!-- イベントの伝播を止める -->
<button @click.stop="handleClick">クリック</button>

<!-- デフォルトの挙動を防ぐ -->
<a href="/some-url" @click.prevent="doSomething">リンク</a>

<!-- 一度だけ実行 -->
<button @click.once="doOnce">一度だけ</button>

その他のTips

DOMの更新を待ってから処理をする

DOMの更新を待ってから処理をするには、$nextTick を使います。これはUIが更新された後に何か処理を行いたい場合に便利です。以下は、ボタンを押すと message の値が変更され、その後に console.log が実行されます。

<div x-data="{ message: "Hello" }">
  <button @click="
    message = "World";
    $nextTick(() => console.log(message))">
    Change Message
  </button>
</div>

ネットワーク処理を行う

ネットワーク処理を行うには、async/await を使います。Alpine.jsでは、非同期処理をシンプルに扱うことができます。

<script>
  // JavaScriptで、非同期処理を行う関数を定義
  async function getLabel() {
      let response = await fetch("/api/label");
      return await response.text();
  }
</script>
<!-- Alpine.js側の記述 -->
<div x-data>
  <span x-text="await getLabel()"></span>
</div>

Alpine.jsでは、プロパティに関数を割り当て、最後のカッコを外すことで、自動的にasync関数であると判定し、await を省略できます。この機能は便利ですが、使い方に注意が必要です。

<div x-data="{ 
  getLabel: async function() {
    let response = await fetch("/api/label");
    return await response.text();
  }
}">
  <span x-text="getLabel"></span>
</div>

データ監視

$watch を使って、データの変更を監視することができます:

<div x-data="{ count: 0 }" x-init="$watch("count", value => console.log(value))">
  <button @click="count++">増加</button>
</div>

API

Alpine.jsのオブジェクトに対して、JavaScriptから操作もできます。より複雑なUI/UXを実現する際に使えそうです。

$store

$store は、Alpine.jsのグローバルなデータストアにアクセスするためのオブジェクトです。以下は、ダークモードの切り替えを行う例です。

<button x-data @click="$store.darkMode.toggle()">ダーク・ライトモードの切り替え</button>

<div x-data :class="$store.darkMode.on && "bg-black"">
  <!-- HTMLのコンテンツ -->
</div>

<script>
  document.addEventListener("alpine:init", () => {
    Alpine.store("darkMode", {
      on: false, // デフォルトはライトモード
      toggle() { // ダーク・ライトモードの切り替え
        this.on = ! this.on;
      },
    });
  });
</script>

$dispatch

カスタムイベントを発火させるための $dispatch メソッドも用意されています:

<div x-data>
  <button @click="$dispatch("custom-event", { message: "Hello!" })">
    イベント発火
  </button>
</div>

<div x-data @custom-event="alert($event.detail.message)">
  <!-- カスタムイベントをリッスン -->
</div>

$root

$root は、Alpine.jsのルート要素を取得するオブジェクトです。以下は、ルート要素のデータを取得してアラートを表示する例です。

<div x-data data-message="Hello World!">
  <button @click="alert($root.dataset.message)">Say Hi</button>
</div>

Alpine.data()

コンポーネントのロジックを再利用するための Alpine.data() メソッドも提供されています:

<script>
  document.addEventListener("alpine:init", () => {
    Alpine.data("dropdown", () => ({
      open: false,
      toggle() {
        this.open = !this.open;
      },
      close() {
        this.open = false;
      }
    }));
  });
</script>

<div x-data="dropdown">
  <button @click="toggle">トグル</button>
  <div x-show="open" @click.outside="close">ドロップダウン内容</div>
</div>

簡単なTodoアプリの例

以下はAlpine.jsだけを使って作成した、簡単なTodoアプリの例です。JavaScriptは使わずに、Alpine.jsだけで実装しています。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Alpine.js Todoアプリ</title>
    <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        h1 {
            color: #2c3e50;
        }
        ul {
            list-style-type: none;
            padding: 0;
        }
        li {
            padding: 8px;
            background-color: #ecf0f1;
            margin-bottom: 4px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        button {
            background-color: #e74c3c;
            border: none;
            padding: 4px 8px;
            color: white;
            cursor: pointer;
        }
        button:hover {
            background-color: #c0392b;
        }
    </style>
</head>
<body>
    <div x-data="{ newTodo: "", todos: [], addTodo() { if (this.newTodo.trim()) { this.todos.push(this.newTodo.trim()); this.newTodo = ""; } }, removeTodo(index) { this.todos.splice(index, 1); } }">
        <h1>Alpine.js Todoアプリ</h1>
        <input x-model="newTodo" @keyup.enter="addTodo" placeholder="新しいタスクを入力">
        <button @click="addTodo">追加</button>
        <ul>
            <template x-for="(todo, index) in todos" :key="index">
                <li>
                    <span x-text="todo"></span>
                    <button @click="removeTodo(index)">削除</button>
                </li>
            </template>
        </ul>
    </div>
</body>
</html>
Todoアプリの例

長いx-dataブロックに改行を入れると動作に問題が生じることがあります。これはHTMLの属性値にある特殊文字や改行の扱いによるものです。この問題を回避するためには、次のように外部関数を使うのがお勧めです:

<div x-data="todoApp()">
    <h1>Alpine.js Todoアプリ</h1>
    <input x-model="newTodo" @keyup.enter="addTodo" placeholder="新しいタスクを入力">
    <button @click="addTodo">追加</button>
    <ul>
        <template x-for="(todo, index) in todos" :key="index">
            <li>
                <span x-text="todo"></span>
                <button @click="removeTodo(index)">削除</button>
            </li>
        </template>
    </ul>
</div>

<script>
    function todoApp() {
        return {
            newTodo: "",
            todos: [],
            addTodo() {
                if (this.newTodo.trim()) {
                    this.todos.push(this.newTodo.trim());
                    this.newTodo = "";
                }
            },
            removeTodo(index) {
                this.todos.splice(index, 1);
            }
        }
    }
</script>

もっと高度な例として、ローカルストレージにTodoを保存するバージョンを考えてみましょう:

<div x-data="persistentTodoApp()">
    <h1>永続化Todoアプリ</h1>
    <input x-model="newTodo" @keyup.enter="addTodo" placeholder="新しいタスクを入力">
    <button @click="addTodo">追加</button>
    <ul>
        <template x-for="(todo, index) in todos" :key="index">
            <li>
                <span x-text="todo.text"></span>
                <div>
                    <button @click="toggleCompleted(index)" x-text="todo.completed ? "完了" : "未完了"" 
                            :style="todo.completed ? "background-color: green;" : """></button>
                    <button @click="removeTodo(index)">削除</button>
                </div>
            </li>
        </template>
    </ul>
</div>

<script>
    function persistentTodoApp() {
        return {
            newTodo: "",
            todos: [],
            init() {
                this.todos = JSON.parse(localStorage.getItem("todos") || "[]");
                this.$watch("todos", () => {
                    localStorage.setItem("todos", JSON.stringify(this.todos));
                });
            },
            addTodo() {
                if (this.newTodo.trim()) {
                    this.todos.push({ 
                        text: this.newTodo.trim(),
                        completed: false 
                    });
                    this.newTodo = "";
                }
            },
            toggleCompleted(index) {
                this.todos[index].completed = !this.todos[index].completed;
            },
            removeTodo(index) {
                this.todos.splice(index, 1);
            }
        }
    }
</script>

Alpine.jsディレクティブ一覧

ディレクティブ 説明 使用例
x-data コンポーネントのデータを定義 <div x-data="{ count: 0 }">
x-text 要素のテキスト内容をバインド <span x-text="count"></span>
x-html 要素のHTML内容をバインド(XSS注意) <div x-html="username"></div>
x-show 条件に応じて要素の表示/非表示を切り替え <div x-show="open">内容...</div>
x-if 条件に応じてDOM要素を追加/削除 <template x-if="open"><div>内容...</div></template>
x-for 配列要素を繰り返し表示 <template x-for="item in items" :key="item.id"><li x-text="item.name"></li></template>
x-model フォーム要素との双方向バインディング <input type="text" x-model="message">
x-on (@) イベントリスナーを設定 <button @click="count++">クリック</button>
x-transition 要素の表示/非表示時のアニメーション <div x-show="open" x-transition>内容...</div>
x-effect データ変更時に処理を実行(初期化時も実行) <div x-effect="console.log(message)">
x-ref 要素への参照を作成 <span x-ref="text">Hello</span>
x-cloak 初期化前の要素を非表示 <div x-cloak>Loading...</div>
x-init コンポーネント初期化時に処理を実行 <div x-init="$el.textContent = 'Ready'">
x-ignore Alpine.jsの処理から要素を除外 <div x-ignore><span x-text="label"></span></div>

イベント修飾子

修飾子 説明 使用例
.prevent デフォルトのイベント動作を防止 <form @submit.prevent="handleSubmit">
.stop イベントの伝播を停止 <button @click.stop="handleClick">
.outside 要素外のクリックを検知 <div @click.outside="open = false">
.window ウィンドウレベルのイベントをリッスン <div @scroll.window="handleScroll">
.once イベントを一度だけ実行 <button @click.once="doThis">
.self イベントが要素自体から発生した場合のみ実行 <div @click.self="handleClick">
.enter Enterキー押下時のみ実行 <input @keyup.enter="submit">
.escape Escapeキー押下時のみ実行 <input @keyup.escape="close">

マジックプロパティ

プロパティ 説明 使用例
$el 現在の要素への参照 <button @click="$el.textContent = 'Clicked'">
$refs x-refで定義した要素への参照 <button @click="$refs.text.remove()">
$event 現在のDOMイベントオブジェクト <button @click="console.log($event.target)">
$dispatch カスタムイベントを発火 <button @click="$dispatch('custom-event')">
$nextTick DOM更新後に処理を実行 <button @click="$nextTick(() => { ... })">
$watch プロパティ変更を監視 <div x-init="$watch('count', value => ...)">
$store グローバルストアにアクセス <div x-text="$store.user.name">

まとめ

Alpine.jsはちょっとしたインタラクティブなUI/UXであれば、JavaScriptを書かずに実現できます。また、さらに複雑な場合にも、JavaScriptからAlpine.jsのAPIを使って操作することができます。

Alpine.jsの主な利点は:

  1. 軽量:ファイルサイズが小さく、パフォーマンスへの影響が少ない
  2. 学習コストが低い:Vue.jsなどに似た構文で、シンプルで直感的
  3. プログレッシブエンハンスメント:既存のHTMLに簡単に追加できる
  4. 依存関係なし:他のライブラリに依存せず単独で動作する

より複雑なアプリケーションには、Vue.jsやReactなどのフレームワークが適している場合もありますが、ちょっとしたインタラクティブ要素や、既存のサイトへの機能追加には、Alpine.jsが最適な選択肢になり得ます。

ぜひ、Alpine.js公式サイトも参照しながら、試してみてください。