Laravel + Alpine.js で再利用できるコンポーネントを作ってみた

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

さてさて、ホントにIT業界はいろいろと変化が多いので、少しの時間が経つだけで「この技術」が「あの技術」になっていたりします。

そして、そんな感じで多く技術が生まれているJavaScript界はLaravelのように1強という状態ではなく、

  • やっぱReactでしょ
  • それでもVueが好き
  • Svelteだって負けてないぞ

とように戦国時代のような群雄割拠した状態が長く続いています…(どれが徳川家康になるのか注目してます❗)

そんな中、とある開発で使ったのがAlpine.jsでした。

正直あまり使っていなかったのですが、使ってみて素直にこう思いました。

もう、これでいんじゃない(所ジョージさん風)

ただし、お仕事としてはAlpine.jsのシェアは多いとは言えないので、やはりReactVueがベターだと思います(開発者が多いのでリクルートしやすいはず🤔)が、個人開発やそれほど複雑な構成ではないサイトは、十分すぎるぐらい高機能だと感じました。

(それでも学習コストが低いのですぐ覚えられるとは思います)

しかし、ひとつ問題がありました。
それは・・・・・・

再利用可能なコンポーネントを作りにくい

です。

というのも、例えばVueだとコンポーネントが簡単につくる仕組みがありますが、Alpine.jsではちょっとした工夫が必要でした。

そこで❗

今回はAlpine.jsを使ってVueのようなリアクティブなコンポーネントを作ってみたいと思います。

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

「Alpine って『アルプスの』という
意味らしいです。
空気薄いから読まなくていいってこと❗❓」

開発環境: Laravel 10.x、Alpine.js 3

やりたいこと

今回は以下のようなコンポーネントをJavaScript側で再利用可能にします。

もちろんLaravelBladeでもループさせれば表示することはできるのですが、Ajax化したいとき(リアクティブ性を高めたいとき)に毎回ページを更新しないといけなくなるので、それを解決したいのが今回のメインどころです。

なお、今回は以下の条件が全て可能になるようにします

  • (Ajaxを想定して)データが変更されたら表示が自動で変更になる
  • パラメータは <script></script>内で指定できる
  • 同じくパラメータは、直接(オブジェクトで) { … } としても指定できる
  • あるデータによって表示/非表示を自動切り替えする

では楽しんでやっていきましょう❗

Laravelでコンポーネントをつくる

まずは、Laravelでコンポーネントを作って、そこにAlpine.js側で再利用できるコンポーネントを作っていきます。

今回、コンポーネント名は「ItemCard」にします。

では、以下のコマンドを実行してください。

php artisan make:component ItemCard

すると、以下2ファイルが作成されます。(ビューまで作成してくれて嬉しい限りです👍)

  • app/View/Components/ItemCard.php
  • resources/views/components/item-card.blade.php

では、それぞれ中身を変更していきましょう。

Laravel側

app/View/Components/ItemCard.php

<?php

namespace App\View\Components;

use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

class ItemCard extends Component
{
    /**
     * Create a new component instance.
     */
    public function __construct(
        public string $xData = '{}',
    ){}

    /**
     * Get the view / contents that represent the component.
     */
    public function render(): View|Closure|string
    {
        return view('components.item-card');
    }
}

変更したのは太字部分だけですが、これはAlpine.js側で以下のようにx-dataパラメータが使えるようにするためです。(⚠ キャメルケースになっていることに注意してください)

<x-item-card x-data="data" />

※ なお、もしこのコンストラクタの書き方を見たことがない方はPHP 8から使えるようになった省略バージョンですので、ぜひ省コード化に役立ててください(この場合、メンバ変数の定義と$this->xData = $xData;は書かなくてよくなりました😄)

これでLaravel側は完了です。

Alpine.js側

続いてAlpine.js側です。

resources/views/components/item-card.blade.php

<div x-data="{ ...{{ $xData }}, ...itemCardMixin }">
    <div class="max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow mb-5">
        <h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900" x-text="item.title"></h5>
        <p class="mb-3 font-normal text-gray-700" x-text="item.content"></p>
        <template x-if="hasUrl()">
            <a :href="item.url" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg">
                続きはこちら
            </a>
        </template>
    </div>
</div>

この中で重要なのが、x-dataの部分です。{{ $xData }}の部分は、先ほどのパラメータが入ってくるため、例えばレンダリングすると以下のようになります。

<div x-data="{ ...itemData, ...itemCardMixin }">

そして、このitemDataは以下のように<script> 〜 </script>内に定義することになります。

<script>

    const itemData = {
        item: {
            id: 999,
            title: 'タイトル',
            content: 'コンテンツ',
            url: 'https://specific.example.com',
        }
    };

</script>

ただしパラメータの指定はこれだけじゃなく、以下のようなループにも対応しています。

<div class="mb-5" x-data="data">

    <template x-for="item in items" :key="item.id">
        <x-item-card x-data="{ item }" />
    </template>

    <x-item-card x-data="directData" />

</div>

この(オブジェクトとして)直接データを指定する場合のレンダリングは以下のようになります。

<div x-data="{ ...{ item }, ...itemCardMixin }">

では、itemCardMixinはなんなのかというと、「コンポーネントで使う関数や変数」が入ってきます。

今回の場合で言うと、URLがデータ内に入っているかどうかをチェックするhasUrl()という関数がありますが、これがコンポーネントで使う関数となります。

実を言うと、当初はコンポーネント内に以下のように埋め込むように考えていました。

<div x-data="{
    hasUrl() {

        return this.item.url;

    }
}">

しかし、もしLaravel側のループで表示されるとすると、全ての場所にこのhasUrl(){ ... }が表示されることになり正直「コードが美しくない…😫」と思い変更しました。

そこで、itemCardMixinも以下のように<script> 〜 </script>タグ内で書くようにしました。

<script>

    // 省略

    const itemCardMixin = {
        hasUrl() {

            return this.item.url;

        }
    };

</script>

こうすることで、このコードは1度だけ表示されるようになります。

※ ただし、毎回呼び出す必要があるので、例えばcomponents.jsというようなファイルに外部化しておかないといけないのがデメリットですが、これが一番ベターな方法という結論になりました…何かいいほうほうはあるのでしょうか 🤔

完成したコード

では、今回やりたいことのすべての条件を満たした全コードになります。

<html>
    <head>
        <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.1/dist/cdn.min.js"></script>
        <script src="https://cdn.tailwindcss.com/3.3.2"></script>
    </head>
    <body>
        <div class="p-5">

            <div class="mb-5" x-data="data">

                <h1 class="mb-3">Card</h1>

                <template x-for="item in items" :key="item.id">
                    <x-item-card x-data="{ item }" />
                </template>

                <x-item-card x-data="directData" />

            </div>

        </div>
        <script>

            const data = {
                items: [
                    { id: 1, title: '1番目のタイトル', content: '1番目のコンテンツ', url: 'https://example.com' },
                    { id: 2, title: '2番目のタイトル', content: '2番目のコンテンツ' },
                    { id: 3, title: '3番目のタイトル', content: '3番目のコンテンツ'},
                ],
                init() {

                    // リアクティブの確認のため、3秒後にデータを更新しています
                    setTimeout(() => {

                        this.items = [
                            { id: 4, title: '4番目のタイトル', content: '4番目のコンテンツ', url: 'https://four.example.com' },
                        ];

                    }, 3000)

                }
            };

            const directData = {
                item: {
                    id: 999,
                    title: 'タイトル',
                    content: 'コンテンツ',
                    url: 'https://specific.example.com',
                }
            };

            const itemCardMixin = {
                hasUrl() {

                    return this.item.url;

                }
            };

        </script>
    </body>
</html>

テストしてみる

では、実際にテストしてみましょう❗

今回はせっかくなのでデモページを用意しました。
シンプルな内容ですが、ぜひ試してみてください。m(_ _)m

📝 デモページ

企業様へのご提案

今回ご紹介したAlpine.jsはまだまだ知名度やシェアは低いですが、Laravelの標準として採用されていますし、何より学習コストが低い(≒すぐ使えるようになりやすい)ので、もしチーム全体がフロントエンドに習熟していない場合は選択肢のひとつになると考えています。

また、これも「シンプルだけど強力な開発テクノロジー」のLivewireはバージョン3からAlpine.jsとより親密になると言われているため、今後シェアを集める可能性も秘めています。

もしAlpine.jsを使った開発をご希望の方はぜひお問い合わせからご相談ください。

お待ちしております。😄✨

おわりに

ということで、今回は「Laravel + Alpine.js」で再利用可能なコンポーネントを作ってみました。

最初は、alpinejs-component というパッケージを使うことを検討していましたが、試してみたところLaravelとの連携がうまくいかないようで断念し、今回の内容にたどり着きました。

それにしても、JavaScript界は今後どうなるのでしょうね。

正直Viteになった今でもビルドは好きになれなかったりするので、Alpine.jsVue 2のようなシンプルさがいいのですが、それだとリプレイス業務が減ってエンジニアの仕事が減ったりするんでしょうか…🤔

ちなみに、Vueはバージョン22023.12.31にサポートが切れるようですし、3はもう別のフレームワークになってしまいました。

誰かVue liteみたいなシンプルバージョンを作ってくれないでしょうか。(それがAlpine.js❓❓)

今夜も夜しか眠れなさそうです(笑)

ではでは〜❗

「けっこうよく寝る方です 💤」

開発のご依頼お待ちしております 😊✨
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly  

開発効率を上げるための機材・まとめ