Laravel で Alpine.js を使う 〜 よりシンプルを求めて3 〜

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、少し前に公開した Laravel で Svelte を使う 〜 よりシンプルを求めて2 〜 という記事では、最近よく見かけるようになったSvelteを試してみました。

すると、嬉しいことにツイッターで shirocake さんから「Alpine.js は比較的シンプルですよ」という情報をいただいたので今回は Alpine.js を使ってシンプル探索をしてみたいと思います。

ちなみに、Alpine.jsVue.jsに影響を受けてつくられたフレームワークで、書き方がホントによく似ています。

例えば、友達の家に行って年の近い弟がいたら、「うわっ、似てるなー😳」ってなるようなイメージです(笑)

実際には、v-if → x-ifになるなど、ほぼvの部分がxに置き換わっただけなのでVue.jsでの開発があればすぐに使えるようになると思いますよ。

そこで❗

今回は前回と同様にLaravel + Alpine.jsCRUD(登録、表示、変更、削除)を実装してみます。

ぜひ何かの参考になりましたら嬉しいです。😄✨

【追記:2022.02.24】
shirocake さん
からgetterを使う方法を教えていただいたのでコードを少し変更しました。getterを使うと変数のように使えます。再度ありがとうございます。m(_ _)m

「貴重な情報
ありがとうございます❗」

開発環境: Laravel 8.x、Alpine.js 3.8.1、axios 0.25.0

前提として

今回も前回(& 前々回)と同じくarticlesテーブルを使用します。
そのため、もしまだarticlesテーブルを用意していない場合は以下を参考にして作成しておいてください。

📝 参考ページ: Laravel + Livewire で CRUD を実装してみる 〜 よりシンプルを求めて 〜:DBまわりを用意する

なお、実際のテーブルはこうなります。

コントローラをつくる

コントローラも前回と全く同じになりますので、以下を参考にしてください。

📝 参考ページ: Laravel で Svelte を使う 〜 よりシンプルを求めて2 〜:コントローラーをつくる

ビューをつくる

さぁ、ここからが今回のメインになります。
実際のコードは以下になります。

※ なお、今回はVueとの比較がしやすいように、変数やメソッドはVueっぽく並べています。

resources/views/article/index.blade.php

<html>
<head>
    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
    <div class="p-5">
        <h1 class="text-3xl mb-4">
            Alpine.js のCRUDサンプル
        </h1>
        <div x-data="article" x-init="getArticles" class="grid grid-cols-2 gap-7">
            <div>
                <table class="w-full text-sm mb-5">
                    <thead>
                    <tr>
                        <th class="border p-2">タイトル</th>
                        <th class="border p-2">本文</th>
                        <th class="border p-2">操作</th>
                    </tr>
                    </thead>
                    <tbody>
                    <!-- ① x-for でループしてます -->
                    <template x-for="article in articles">
                        <tr>
                            <td class="border px-2 py-1" x-text="article.title"></td>
                            <td class="border px-2 py-1" x-text="article.content"></td>
                            <td class="border px-2 py-1 text-right">
                                <button
                                    type="button"
                                    class="bg-yellow-500 text-yellow-50 rounded p-2 text-xs"
                                    @click="onEdit(article)">
                                    変更
                                </button>
                                <button
                                    type="button"
                                    class="bg-red-600 text-red-50 rounded p-2 text-xs"
                                    @click="onDelete(article)">
                                    削除
                                </button>
                            </td>
                        </tr>
                    </template>
                    </tbody>
                </table>

                <!-- ② x-show は、直接タグにセットしても OK -->
                <button
                    type="button"
                    class="bg-blue-500 text-blue-50 rounded p-2 text-xs"
                    x-show="hasPrevPage"
                    @click="onMovePage('prev')">
                    前へ
                </button>
                <button
                    type="button"
                    class="bg-blue-500 text-blue-50 rounded p-2 text-xs"
                    x-show="hasNextPage"
                    @click="onMovePage('next')">
                    次へ
                </button>

            </div>
            <div>
                <div class="text-green-700 p-3 bg-green-300 rounded mb-3" x-show="resultMessage">
                    <!-- ③ x-text でデータを表示します -->
                    <span x-text="resultMessage"></span>
                </div>
                <div class="mb-3">
                    <label for="title">タイトル</label>
                    <br>
                    <!-- ④ x-model で双方向バインディングします -->
                    <input id="title" type="text" class="border w-full p-1" x-model="params.title">
                </div>
                <div class="mb-4">
                    <label for="content">本文</label>
                    <br>
                    <textarea id="content" rows="7" class="border w-full p-1" x-model="params.content"></textarea>
                </div>
                <!-- ⑤ @**** でイベントをセットできます -->
                <button type="submit" class="bg-purple-700 text-purple-50 p-2 rounded" x-show="isModeCreate" @click="onSubmit">登録する</button>
                <button type="submit" class="bg-blue-700 text-blue-50 p-2 rounded" x-show="isModeEdit" @click="onSubmit">変更する</button>
            </div>
        </div>
    </div>

    <!-- ⑥ ここに defer がないとうまくいきません -->
    <script defer src="https://unpkg.com/alpinejs@3.8.1/dist/cdn.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.25.0/axios.min.js"></script>
    <script>

        const article = () => {

            return {

                // data
                mode: 'create', // `create` or `edit`
                params: {
                    title: '',
                    content: '',
                },
                articles: [],
                page: 1,
                resultMessage: '',
                hasPrevPage: false,
                hasNextPage: false,

                // methods
                getArticles() {

                    this.articles = [];
                    const url = '{{ route('article.list') }}?page=' + this.page;
                    axios.get(url)
                        .then(response => {

                            this.articles = response.data.data;
                            this.hasPrevPage = response.data.prev_page_url !== null;
                            this.hasNextPage = response.data.next_page_url !== null;

                        });

                },
                onEdit(article) {

                    // ⑦ - 1 スプレッド構文を使うと省コードになります
                    this.params = { ...article }; // オブジェクトの複製
                    this.mode = 'edit';

                },
                onMovePage(mode) {

                    this.page += (mode === 'prev') ? -1 : 1;
                    this.getArticles();

                },
                onSubmit() {

                    if(confirm('送信します。よろしいですか?')) {

                        let url = '';
                        let additionalParams = {};

                        if(this.isModeCreate === true) {

                            url = '{{ route('article.store') }}';

                        } else if(this.isModeEdit === true) {

                            const articleId = this.params.id;
                            url = `{{ route('article.update', '') }}/${articleId}`;
                            additionalParams = { _method: 'PUT' };

                        }

                        // ⑦ - 2 スプレッド構文を使うと省コードになります
                        const data = { // オブジェクトの合体
                            ...this.params,
                            ...additionalParams
                        };

                        axios.post(url, data)
                            .then(response => {

                                if(response.data.result === true) {

                                    this.getArticles();

                                    this.params = {
                                        id: '',
                                        title: '',
                                        content: '',
                                    };
                                    this.resultMessage = '保存が完了しました!';

                                    setTimeout(() => { // 3 秒後にメッセージをクリア

                                        this.resultMessage = '';

                                    }, 3000);

                                }

                            });

                    }

                },
                onDelete(article) {

                    if (confirm('削除します。よろしいですか?')) {

                        const url = '{{ route('article.destroy', '') }}/' + article.id;
                        axios.delete(url)
                            .then(response => {

                                if (response.data.result === true) {

                                    this.getArticles();

                                }

                            });

                    }
                },

                // Computed
                // ⑧ 実際はちょっと違いますが Computed の代わりにしてます
                get isModeCreate() {

                    return this.mode === 'create';

                },
                get isModeEdit() {

                    return this.mode === 'edit';

                }
            };

        };

    </script>

</body>
</html>

では、少しコードが長いのでひとつずつ見ていきましょう。

① x-for でループしてます

x-forは、Vueと同じようにデータをループさせる記述です。
そのため、テーブルでリストを作るのに重宝するでしょう。

【⚠ ご注意】

なお、これはAlpine.jsのクセというか特徴なのですが、x-for<template></template>タグの中だけで有効です。

つまり、<div><li>タグには直接使うことはできません。

また、これはx-ifもこれと同じで、今回のコードで使っていないのはこれが原因です。

② x-show は、直接タグにセットしてもOKです

先ほどご紹介したように、x-if<template></template>タグにセットしなければいけませんが、x-showは直接<div>などのタグに書き込むことができます。

※ そのため、(使いどころが違うのですが)個人的にはVueで慣れているのでx-showの方が好きかな、というカンジでした。

③ x-text でデータを表示します

これもVueと同じで、タグの中身の値を指定する記述です。
また、x-htmlも存在しているので、HTMLタグを有効にすることもできます👍

④ x-model で双方向バインディングします

v-modelと同様で、<input>タグの中身が変われば、自動的に変数の中身も変更し、さらに、その逆もやってくれるという「リアクティブ」な機能です。

⑤ @***** を使ってイベントをセットできます

@click="*****"でクリックイベントをセットすることができます。
なお、これもVueと同様にx-on:clickという書き方もできます。

⑥ ここに defer がないとうまくいきません

これも特徴のひとつと言っていいと思うのですが、どうやらAlpine.jsは「先に個別コードを用意してから本体を呼び出さないとエラーになる」ようです。

そのため、deferをセットすることで最後にAlpine.js本体が読み込まれるようにしています。

⑦ スプレッド構文を使うと省コードになります

なお、これはAlpine.jsには直接関係はないのですが、便利な書き方なので「スプレッド構文(Spread operator」ご紹介させてください。

例えば、私は以前「オブジェクトを複製する」場合、以下のように書いていました。

const clonedData = Object.assign({}, data);

しかし、実はもっとシンプルに書くことができます。

const clonedData = { ...data };

いかがでしょう。
省コードで見やすくなりました。

ちなみに、スプレッド構文は、次のように2つのオブジェクトを合体させることもできます。

const mergedData = {
    ...data1, // 👈 1つ目のオブジェクト
    ...data2  // 👈 2つ目のオブジェクト
};

ぜひ便利なのでやってみてください😄

⑧ 実際はちょっと違いますが Computed の代わりにしてます

Vueには、値を一時的にキャッシュしてくれる(つまり、同じ処理を何回も実行しない)機能Computedがありますが、どうやらAlpine.jsには存在しないようでしたので、このような形で代用しました。

【追記:2022.02.24】
getterを使う方法へ変更しました。

getterを使うと、以下のように通常の変数のように使うことができるようになります。

if(this.isModeCreate === true) { // 👈 getter なので () はいらない

    

} else if(this.isModeEdit === true) { // 👈 getter なので () はいらない

    

}

情報ありがとうございます😄

なお、以下は通常の関数として使う方法で、投稿当時のものです。

そのため、Vueでは変数のように使うことができますが、今回はコード内では以下のように通常の関数のようにして使っています。

if(this.isModeCreate() === true) {

    // 省略

} else if(this.isModeEdit() === true) {

    // 省略

}

※ ただし、x-showの中では変数のように使えます。

x-show="isModeCreate"

ルートをつくる

では、最後にルートです。

routes/web.php

use App\Http\Controllers\ArticleController;

// 省略

Route::prefix('article')->controller(ArticleController::class)->group(function() {

    Route::get('/', 'index')->name('article.index');
    Route::get('/list', 'list')->name('article.list');
    Route::post('', 'store')->name('article.store');
    Route::put('/{article}', 'update')->name('article.update');
    Route::delete('/{article}', 'destroy')->name('article.destroy');

});

※ なお、Laravel 8.80から有効になったcontroller()メソッドを使っています。省コード、バンザイ 🎉

テストしてみる

では、テストを…と思いましたが前回と全く同じ挙動ですので、今回は割愛します。もしSvelte版でよろしければ、以下をご参照ください。

📝 参考ページ: テストしてみる

企業様へのご提案

今回のように、もしAlpine.jsを使った開発されていらっしゃったらお力になれるかと思います。

ぜひご希望でしたら、お問い合わせからご連絡ください。
お待ちしております。😄✨

ちなみに: 使ってみた感想

今回はじめてAlpine.jsを本格的に使ってみた感想をまとめてみました。(初見なので違っていたらご指摘ください。m(_ _)m)

いいなと思った部分

  • Vueに慣れていれば、学習コストはとても低い
  • Vue 3 で失ってしまった(と個人的に思ってる)シンプルさが残されている
  • ビルドなしでも使える
  • Blade 内に書けるので、PHPの変数や定数を埋め込むことができる
  • (作者が Livewire も手がけているということで)Laravel との相性がいい

気になったところ

  • x-for や x-if を使う場合、<template></template> タグを書かないといけない
  • Vue にある computed がない
  • shirocake さんもご指摘になられていましたが)知名度がまだあまりない(ただし、GitHub でのスターは 2万近くあります。2022.02.02 現在)
開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はLaravel + Alpine.jsCRUD機能をつくってみました。

今回も世界の天才がつくったフレームワークを体験することで、いろいろと勉強になることがありました。いつも感謝です。m(_ _)m

ちなみに、こういうテクノロジーを自分でつくりあげることができる天才たちはやっぱりスゴイですよね。

もし作品が人気になってもずっとメンテナンスが必要ですし、世界中の人が使い出すと、バージョンアップの際にいろんな意見が出てくるし…で、なかなか大変なんじゃないでしょうか。

faker.jscolor.jsの話じゃないですが、こういった方々は金銭的にも裕福になってほしいものです。(貧者の一灯というやつですが、私は少し前にCanonicalUbuntu)に寄付しました)

ぜひ皆さんもAlpine.jsを試してみてくださいね。

ではでは〜❗

「卵 + 粉チーズ
→ 混ぜて電子レンジ
= フワフワ卵焼きできます👍」

このエントリーをはてなブックマークに追加       follow us in feedly