Alpine.jsでそれっぽくコードとマークアップを分離する

## TL;DR

addEventListenerの``apline:init``で色々するとJSとマークアップを分離できそう。

## Alpine.jsってなんぞ

私とAlpine.jsの出会いは [Laravel Livewire](https://laravel-livewire.com/)でした。
Laravelフレームワーク上でSPAライクな実装ができるようになるフレームワークなのですが、低レベルなところで色々やっている部分の基底技術がこの``Alpine.js``です。

特徴としては以下のような感じです。

- 15の属性、6のプロパティ、 2のメソッドから構成（※正確には18の属性、9のプロパティ、3のメソッド）。すごくシンプル
- 紐づけするためのオブジェクトもフィールドまたはgetterプロパティでシンプル

兎にも角にもシンプルさが目立つイメージです。けれどもデータバインド周りの機能は十分なので、jQueryの代替として利用することもできそうです。

## そのサンプル、インラインじゃね？

Alpine.jsの簡単なサンプルを見ると、以下のようなものが多いです。

``` html
<div x-data="{ count: 0 }">
    <button x-on:click="count++">Increment</button>
 
    <span x-text="count"></span>
</div>
```

上記のサンプルは[Alpine.js start-here; building a counter](https://alpinejs.dev/start-here#building-a-counter)のコードですが……

__Alpine.jsってインラインじゃないと使えない……？__

サンプルコードでは``x-data``属性で使用するオブジェクト定義をしていて、``x-on``属性には``count``フィールドのインクリメント、``x-text``属性で``innerHTML``に対するバインディングをしています。

``x-text``属性は分かる。表示するのに紐付けないと何にもできないから。

しかし。

__``x-data``のオブジェクト指定、``x-on``属性のロジックはなんとかできない？ 分離したくない？__

複雑なデータを扱おうとすると、``x-data``属性に対して１行でいろんなことを書かないといけないため、シッチャカメッチャカになる未来が見えます。

```html
<!-- こんな未来が見える -->
<div x-data="{fieldA: '', fieldB: '', fieldC: '', fieldD: '', fieldE: '', doSomething() { console.log('なにかする');}, }" >
```

## マークアップとAlpine.jsの世界を分ける

### バージョンなど

| ライブラリ | バージョン |
| --: | :-: |
| Alpine.js | 3.10.5 |
| Pure.css | 3.0.0 |

Pure.cssはおまけです。サンプルのUIにグリッドを導入したかったので。

### 実装サンプル in Codepen


<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="eYKydBa" data-user="filunK" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/filunK/pen/eYKydBa">
  Alpinejs code behind</a> by FilunK (<a href="https://codepen.io/filunK">@filunK</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

上記サンプルは以下のことをしています。

- windowサイズを追跡、表示
- チェックボックスのチェック状態をバインド、チェックボックスやらボタンやらでいじくれるように

### 詳説

HTML、JSのソースを説明していきます。

#### HTML

``` html
<div x-data="alpineCodebehind">
  <div class="pure-g">
    <div class="pure-u-1">
      <label>
        <input type="checkbox" x-model="checked"/>
        チェックボックス
      </label>
      <button class="mx" x-on:click="toggle">チェックをイジる</button>
      <span x-text="checked"></span>
    </div>
    <div class="pure-u-1">
      <label>
        WIDTH:&nbsp;<span x-text="width"></span>
      </label>
    </div>
    <div class="pure-u-1">
      <label>
        HEIGHT:&nbsp;<span x-text="height"></span>
      </label>
    </div>
  </div>
</div>
```

インラインでよく見るサンプルとの違いは以下のポイントです。

- ``x-data``で指定するのはJSのオブジェクトではなく、データの名前
- ``x-on``系統の属性にはロジックではなくメソッド名を指定


#### JS

``` Javascript
// ES モジュール方式でのロード
import Alpine from "https://cdn.skypack.dev/alpinejs@3.10.5";

// Alpinejs INITイベントで初期化
document.addEventListener('alpine:init', () => {
  
  // Alpinejsの外部からAlpinejsの世界にバインドする変数を定義
  const windowSize = Alpine.reactive({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  
  // Alpinejsの外部世界: window.resizeイベント
  window.addEventListener('resize', (e) => {
    windowSize.width = window.innerWidth;
    windowSize.height = window.innerHeight;
  });
  
  // 画面のデータバインディング
  Alpine.data('alpineCodebehind', () => ({
    
    // チェック？
    checked: false,

    // computed: 幅
    get width () {
      return windowSize.width;
    },

    // computed: 高さ
    get height () {
      return windowSize.height;
    },

    // 振る舞い: トグル
    toggle () {
      this.checked = ! this.checked;
    },
  }));
});

// Alpinejsの開始
Alpine.start();
```

インラインでよく見るサンプルとの違いは以下のポイントです。

- CDN直読み込みではない。
- ``Alpine.start()``の前に``alpine:init``イベントに対するイベントリスナーを実装
    - イベントリスナー内で``Alpine.data()``メソッドにてデータ名とマークアップの後ろに控えるフィールド・メソッドを実装

``Alpine.data()``メソッドの第１引数が、メソッドの表すデータの名前になります。これがマークアップ側での``x-data``属性に指定する値です。

要は明示的に``Alpine.start()``メソッドが実行する前に予めイベントリスナーを実装することが必要なわけです。
CDN直読み込みだと、その時点で初期化のイベントが終了してしまうので、``x-data``属性にデータ名を指定できなくなってしまうんですね。知らんけど。

>
> コレはおそらく、CodepenにおけるHTMLやJSのロードに関係する問題と思われます。
> 私自身が試しているわけではありませんが、CDN直読み込みでも``alpine:init``でいい感じに実装しているコードもネット上では見受けられるので。
>

今回、サンプルではESモジュール方式で``Alpine.js``をインポートしています。このあたりは色々と選択肢があると思います。``vite``なり``webpack``なりでバンドルする、なんてこともありでしょう。

### 分離した際の思わぬメリット

このコードだとHTMLとJSを分離することに成功しているわけですが、別のメリットもあります。

__Alpine.jsの外の世界のイベントをAlpine.jsの世界にバインドできる__ 点です。

サンプルのコードでは、以下のステップでAlpine.jsの外からAlpine.jsの中の世界に情報を持ち込んでいます。

1. ``Alpine.reactive()``メソッドでバインド用のデータ※以下『_REACTIVE_』を用意
1. 他のイベントリスナーの中で _REACTIVE_ を弄る
1. ``Alpine.data()``メソッドの世界の中で _REACTIVE_ を使用する

該当のコードをサンプルから抜き出すと以下のコードになります。

``` Javascript
// Alpinejsの外部からAlpinejsの世界にバインドする変数を定義
const windowSize = Alpine.reactive({
    width: window.innerWidth,
    height: window.innerHeight,
});

// Alpinejsの外部世界: window.resizeイベント
window.addEventListener('resize', (e) => {
    windowSize.width = window.innerWidth;
    windowSize.height = window.innerHeight;
});

// 画面のデータバインディング
Alpine.data('alpineCodebehind', () => ({
    // computed: 幅
    get width () {
      return windowSize.width;
    },

    // computed: 高さ
    get height () {
      return windowSize.height;
    },
}));

```

サンプルではwindowサイズの変更をリアルタイムに表示するため、``window.resize``イベントで幅と高さを取得して、算出プロパティとして実装しています。

## 最後に

Javascriptの世界はいろんな技術が生まれては廃れてを繰り返していて、技術を追っかけるのが大変な一方で楽しい領域でもあります。しかも思いもしないところから

> 実はな、これ、JSのライブラリを利用してるんだよ

みたいなところがあって（特にWebアプリ・システムを構築しようとすると）、新鮮な刺激になります。

``Alpine.js``も``Laravel Livewire``きっかけで知ったものですが、コレ単体でも色々とできそうな可能性を感じました。

IEが一応亡き者となった今、こういった新しい技術を取り込んでゆくにはよい機会でしょう。