エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Alpine.js が 3.x になり、いっそう利用しやすくなった

記事にするのが遅くなりましたが、6 月に Alpine.js が 3.0 にメジャーバージョンアップしました(ちなみに 2021-08-29 現在の最新は 3.2.4)。

alpinejs.dev

色々と改善されていますが、個人的に一番大きな利点はロジックをグローバルスコープに公開しなくてもよくなった点と思っています。

Alpine.js については昨年もブログに書いていますが、この時点では ActionCable のロジックと、グローバルスコープにある Alpine.js のロジックをどう繋げるか頭を捻りました。

blog.emattsan.org

3.x では Alpine.data という関数が追加されたことで、ロジックを分離して記述することが楽になりました。

Ajax を使ったサンプル

何かしらのユーザの一覧、ID と名前とメールアドレスの一覧、を Ajax で表示するサンプルとして書いてみました。

サーバ

単純にユーザの配列を JSON 形式で返すサーバです。

Sinatra で記述しています。 ユーザデータは Faker を使って生成しています。

require 'json'
require 'sinatra'
require 'faker'

users = 20.times.map {|i| {id: i, name: Faker::Name.name, email: Faker::Internet.email} }

get '/' do
  users.to_json
end

クライアント

イベントのハンドリングと状態の管理を Alpine.js で、HTTP リクエストを Aixos で記述しています。

3.x になり、明示的にスタートを記述することが必須になりました。 これも個人的にはタイミングを自分で管理できるのでよい変更と感じています。

スタートすると document のイベント alpine:init が発火するので Alpine.data 関数で HTML の要素と Alpine.js のデータを関連付けます。

初期化時に init() が呼び出されるので、ここで Axios を利用してサーバからユーザの一覧を取得します。

import Alpine from "alpinejs"
import axios from "axios"

function User() {
  return {
    users: [],

    fetch() {
      const url = new URL("http://localhost:4567")

      axios
        .get(url)
        .then((resp) => {
          this.users = resp.data
        })
    },

    init() {
      this.fetch()
    }
  }
}

document.addEventListener("alpine:init", () => {
  Alpine.data("User", User)
})

document.addEventListener("DOMContentLoaded", () => {
  Alpine.start()
})

HTML

ユーザの一覧を表示する HTML です。

x-data="User" を指定した HTML 要素に Alpine.data で指定したデータが関連付けられます。

x-for で Alpine.js のデータの users の内容を表示します。

    <div x-data="User">
      <table>
        <thead>
          <tr>
            <th>id</th>
            <th>name</th>
            <th>email</th>
          </tr>
        </thead>
        <tbody>
          <template x-for="user in users", :key="user.id">
            <tr>
              <td x-text="user.id"></td>
              <td x-text="user.name"></td>
              <td x-text="user.email"></td>
            </tr>
          </template>
        </tbody>
      </table>
    </div>

これだけで Ajax を使ったユーザの一覧表示が実現できました。

f:id:E_Mattsan:20210829194745p:plain

フィルタを追加してみる

次に Alpine.js を使ったフィルタを追加してみます。

サーバ

サーバ側では、パラメータで受け取った文字列にマッチしたユーザデータだけを返すようにします。

require 'json'
require 'sinatra'
require 'faker'

users = 20.times.map {|i| {id: i, name: Faker::Name.name, email: Faker::Internet.email} }

get '/' do
  filter = params['filter']
  users.select {|user| user[:name].include?(filter) }.to_json
end

クライアント

クライアントでは、フィルタの文字列を格納する filterString と、フィルタリング実行のイベントを受ける filtrate 関数を追加します。

fetch 関数は、クエリ文字列にフィルタの文字列を付けてリクエストを送るように修正します。

import Alpine from "alpinejs"
import axios from "axios"

function User() {
  return {
    users: [],
    filterString: '',

    fetch() {
      const url = new URL("http://localhost:4567")
      url.search = `filter=${this.filterString}`

      axios
        .get(url)
        .then((resp) => {
          this.users = resp.data
        })
    },

    init() {
      this.fetch()
    },

    filtrate() {
      this.fetch()
    }
  }
}

document.addEventListener("alpine:init", () => {
  Alpine.data("User", User)
})

document.addEventListener("DOMContentLoaded", () => {
  Alpine.start()
})

HTML

フィルタの文字列を入力するフォームを追加します。

フォームのイベント submit に、関数 filtrate を割り当てます。 ここで preventpreventDefault 関数と同じ働きをしています。

次に input には x-modelfilterStrign を割り当てます。 これによって inputvalueflterString が連動するようになるので、JavaScript のコードから filterString を参照することで inputvalue が参照でき、filterString を変更することで inputvalue を変更するすることができるようになります。 また同じように inputvalue を参照/変更すると、filterString を参照/変更できます。

    <div x-data="User">
      <form @submit.prevent="filtrate">
        <input type="text" x-model="filterString" />
      </form>
      <table>
        <thead>
          <tr>
            <th>id</th>
            <th>name</th>
            <th>email</th>
          </tr>
        </thead>
        <tbody>
          <template x-for="user in users", :key="user.id">
            <tr>
              <td x-text="user.id"></td>
              <td x-text="user.name"></td>
              <td x-text="user.email"></td>
            </tr>
          </template>
        </tbody>
      </table>
    </div>

このように振る舞いを手順ではなく宣言で記述することができるようになっています。

f:id:E_Mattsan:20210829200249p:plain

Alpine.js の利点

フロントエンドを構築する JavaScript のライブラリは多数存在していますが、個人的に Alpine.js を推す理由は、このロジックを分離できる点です。 個人的には、ロジックはロジック、ビューはビューで分離して考えたいという考えをしています。

わたしがフロントエンドに明るいわけでないので、誤った認識をしているかもしれませんが、どうもフロントエンドのライブラリというと、ロジックからビューまでをまとめたコンポーネント単位で開発する印象があります。 その点で、Alpine.js はその辺の結合が緩いため、他の単なる JavaScript のライブラリと同じようにロジックを .js ファイルに記述し、ビューを .html ファイルに記述するだけで利用することができ、コンポーネント形式で記述されたファイルを変換するような工程がいりません。

また、わたしの場合ウェブアプリケーションの開発には主に Ruby on Rails を利用していますが、そこでも HTML のレンダリングRails にまかせた上で、JavaScript のロジックだけに注力して利用できるという利点があります。

実のところ。Ruby on Rails でアプリケーション開発をしていく中、どこかで効果的に活用しようとタイミングを見計っているところです。

いつか読むはずっと読まない:余裕なき Slack

"Slack" というチャットサービスがありますが。 個人的に相性がよくないのか、使い勝手がよくありません。

なんか、使っていると余裕がなくなると言いますか、落ち着かなくなると言いますか、気楽にメッセージをポストできる感じでないのです。

なんか…こう…気楽に使えるチャットサービスが欲しいですね…。