HTMXの正体が分からないので、オレオレHTMXを作ってみた

## HTMX とは？

https://qiita.com/twrcd1227/items/7bce18167fb02ec22729

の記事を見ればわかるが、Javascript の知識なしで ajax通信や高度なUIの実装ができるライブラリになっています。

賛否両論はあると思いますが、自分はあまり実装がどうなっているか分からず気になったので、ソースコードを少し見てオレオレHTMXを作ってみることにしました（誰にでも分かるように簡易な実装にとどめています）。

ではまずは、ソースコードを見てみましょう！

## HTMX のソースコードを読んでみる
HTMX のソースコードは、主に

https://github.com/bigskysoftware/htmx/blob/master/src/htmx.js

の 3900行程度のコード（2024年1月27日現在）になっています。
最初からこのコードを読むのは骨が折れますが、どうやらエントリーポイントは、

```diff_javascript:htmx.js 3862行目
        // initialize the document
        ready(function () {
            mergeMetaConfig();
            insertIndicatorStyles();
            var body = getDocument().body;
++          processNode(body);
```
らへんになっているようです。
簡単に流れを説明すると、

1. `hx-` から始まるそれっぽい attribute がある HTML を抽出
2. 1で抽出した要素に Event を追加

の２段階で、HTMX は ユーザーから与えられた attributeのみを参考に Ajax通信などを実現しているようです。それぞれを説明していきます。

#### 1 : `hx-` から始まるそれっぽい attribute がある HTML を抽出
先ほどの上のソースコードの `processNode` を見ると、

```diff_javascript:htmx.js 2150行目
        function processNode(elt) {
            elt = resolveTarget(elt);
            if (closest(elt, htmx.config.disableSelector)) {
                cleanUpElement(elt)
                return;
            }
            initNode(elt);
++          forEach(findElementsToProcess(elt), function(child) { initNode(child) });
            // Because it happens second, the new way of adding onHandlers superseeds the old one
            // i.e. if there are any hx-on:eventName attributes, the hx-on attribute will be ignored
            forEach(findHxOnWildcardElements(elt), processHxOnWildcard);
        }

```

`findElementsToProcess` という関数で要素を取り、それぞれの見つけた DOM に `initNode` を適用しているように見えます。
この `findElementsToProcess` を見ると以下のようになっています。

```diff_javascript: htmx.js 1965行目
        function findElementsToProcess(elt) {
            if (elt.querySelectorAll) {
                var boostedSelector = ", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";
++              var results = elt.querySelectorAll(VERB_SELECTOR + boostedSelector + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," +
++                  " [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");
                return results;
            } else {
                return [];
            }
        }
```

ここで `querySelectorAll` を使って `hx-trigger` などのattribute を持った要素を取得しているのが分かります。
ここで取得した要素に、イベントが割り振られます。

#### 2 : 1で抽出した要素に Event を追加
Eventを追加するのは上で出てきた `initNode` という関数になります。
この `initNode` で重要な部分は 2116・2117行目の部分でして、

```javascript
                var triggerSpecs = getTriggerSpecs(elt);
                var hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs);
```

の部分になります。
`getTriggerSpecs` で hx-trigger の情報を取得（once、delayなど含め）し、`processVerbs` の中の `addTriggerHandler` がイベント追加を担い、そのコールバック引数の中の `issueAjaxRequest` で ajax通信（xhrを使用、古いブラウザに対応するため fetchは使っていない）を行っています。

```diff_javascript:htmx: 1834行目
        function processVerbs(elt, nodeData, triggerSpecs) {
            var explicitAction = false;
            forEach(VERBS, function (verb) {
                if (hasAttribute(elt,'hx-' + verb)) {
                    var path = getAttributeValue(elt, 'hx-' + verb);
                    explicitAction = true;
                    nodeData.path = path;
                    nodeData.verb = verb;
                    triggerSpecs.forEach(function(triggerSpec) {
++                      addTriggerHandler(elt, triggerSpec, nodeData, function (elt, evt) {
++                          if (closest(elt, htmx.config.disableSelector)) {
++                              cleanUpElement(elt)
++                              return
++                          }
++                          issueAjaxRequest(verb, path, elt, evt)
++                      })
                    });
                }
            });
            return explicitAction;
        }
```

issueAjaxRequest は結構長くなるので、今回は触れませんが、大まかな流れは結構簡単だったのではなかったでしょうか？

ではこれを参考にオレオレHTMXを作ってみましょう！

## オレオレHTMXを作ってみる
今回作る オレオレHTMX の仕様は以下のようにします（腕に自信がある方は intersect や once, delay などもっと難しい仕様を詰めてもいいかもしれませんね。）。
~~HTMX も 8割の要求を満たせばいいと言っているので、HTMX で一番使いそうな 6割〜8割の仕様を詰め込もうと思います。~~

```
hx-post / hx-get
tx-target (#id / this / find)
hx-swap (innerHTML / beforebegin / beforeend / afterbegin / afterend)
hx-trigger (click / load / change)
```

では作ってみましょう！
ここでは、上の流れと同じように、

1. 要素を抽出する
2. 1で抽出した各要素に、イベントを追加する

の流れで実装したいと思います。ちなみに xhr とか使うのはだるかったので、fetch とか使っています。
完成品(簡易版)はこちらです。

https://github.com/coffeecupjapan/mini-htmx/blob/main/htmx_sample.js

開発中のブランチがこちらです（outerHTML, revealedなど実装しました、一応 JSON から値を取得できるようにもしています）。

https://github.com/coffeecupjapan/mini-htmx/tree/my_dev

まずは、`[hx-trigger]` attribute がついた 要素 を取得する関数を作ります。

```javascript
function findAllTriggers(ele) {
    const result = ele.querySelectorAll("[hx-trigger]");
    return result;
}
```

これで、１つ目の「要素を抽出する」の部分は実装できました。
次に「1で抽出した各要素に、イベントを追加する」を実装してきます。
今回は、

1. hx-get/hx-post
2. hx-taget
3. hx-swap
4. hx-trigger

の４つのattributeとしか使いませんが、それぞれの attribute でどのような返り値が必要になるか考えてみます。

まず `hx-get` `hx-post` ですが下のように使います。

```html
<sample hx-get="https://jsonplaceholder.typicode.com/posts/1"></sample>
```
ここでは、getかpostかの情報と URL が必要に見えますね。なので、`hx-get` `hx-post` からgetかpostかの情報と URL を取得する関数を作りましょう。

```javascript
function parseMethodAttribute(ele) {
    const getContent = ele.getAttribute("hx-get");
    const postContent = ele.getAttribute("hx-post");
    if (getContent) return ["get", getContent];
    if (postContent) return ["post", postContent];
    return ["get", undefined];
}
```

次に、`hx-taget` です。

```html
<sample hx-target="#target" hx-get="https://jsonplaceholder.typicode.com/posts/1"></sample>
```

ここでは、target を #id 形式でする他に、this（このノード）と find（最初に見つかったクラスを取得）を定義してみましょう。
では、この返り値は何でしょうか？
ここではイベント内での使い方を鑑みて、一旦DOMで取得してみましょう。すると以下のようなコードになります。

```javascript
function parseTargetAttribute(ele) {
    const targetContent = ele.getAttribute("hx-target");
    if (targetContent[0] === "#") {
        const id = targetContent.slice(1);
        const targetEle = document.getElementById(id);
        return targetEle;
    } else if (targetContent === "this") {
        return ele;
    } else if (targetContent.slice(0, 5) === "find ") {
        const className = targetContent.slice(5);
        // TODO : id や selector で取得できるようにする
        const targetEle = document.getElementsByClassName(className);
        if (targetEle.length) return targetEle[0];
        return null;
    }
    return ele;
}
```

３つ目に、`hx-swap` を見てみましょう。ここでは、

```html
<sample hx-swap="beforeend" hx-target="#target" hx-get="https://jsonplaceholder.typicode.com/posts/1"></sample>
```

HTML への挿入方法を指定します。

https://developer.mozilla.org/ja/docs/Web/API/Element/insertAdjacentHTML

などを参考にすると、beforebegin / beforeend / afterbegin / afterend と innerHTML の方法があるみたいです。
ここで必要になる情報は、イベント内で `insertAdjacentHTML` を使いますので、文字列だけで良さそうです。実装すると以下のような感じです。

```javascript
function parseSwapAttribute(ele) {
    const swapContent = ele.getAttribute("hx-swap");
    const swapEles = ["innerHTML", "beforebegin", "beforeend", "afterbegin", "afterend"];
    if (swapEles.includes(swapContent)) return swapContent;
    return "innerHTML";
}
```

最後に、`hx-trigger` ですが、これもイベントリスナーの

```javascript
addEventListner("hoge", () => {})
```

の hoge の部分に入れるだけなので、文字列を取得するだけですね。以下のような実装になります。

```javascript
function parseTriggerAttribute(ele) {
    const triggerContent = ele.getAttribute("hx-trigger");
    const triggerEles = ["load", "click", "change"];
    if (triggerEles.includes(triggerContent)) return triggerContent;
    return "load";
}
```

そして、これらの hx-get/hx-post, hx-taget, hx-swap, hx-trigger の返り値から、イベントを作っていきます。
ここの流れは、

1. イベントを加える要素に addEventListener("hoge") を定義する
2. 1のコールバック引数に、url から fetch するコードを書く
3. 2の結果を innerHTML か insertAdjacentHTML するかで分岐する

のような実装の流れになります。
それを実装したのが以下のコードになります。

```javascript
function generateEvent(ele, target, method, trigger, swap) {
    ele.addEventListener(trigger, async() => {
        if (!method[1]) return;
        const result = await fetch(method[1], {method: method[0]});
        const json = await result.json();
        const stringifyJson = JSON.stringify(json);
        if (swap === "innerHTML") {
            target.innerHTML = stringifyJson;
        } else {
            target.insertAdjacentHTML(swap, stringifyJson);
        }
    });
    if (trigger === "load") {
        ele.dispatchEvent(new Event("load"));
    }
}
```

最後の部分で load の場合にみ手動で loadイベントを発火させている部分以外は結構簡単な実装だと思います。

そして最後に、DOMContentLoaded の段階で上のような、要素にイベントをつける動きを追加するようにします。

```javascript
document.addEventListener("DOMContentLoaded", () => {
    const allEles = findAllTriggers(document.body);
    allEles.forEach((ele) => {
        const target = parseTargetAttribute(ele);
        const method = parseMethodAttribute(ele);
        const trigger = parseTriggerAttribute(ele);
        const swap = parseSwapAttribute(ele);
        generateEvent(ele, target, method, trigger, swap);
    });
});
```

HTMX も結構凄そうに見えても、オレオレであれば中身は結構簡単に作れたのではないでしょうか？

## 感想
HTMX は React とかを代替する、と言われかねない勢いな気がするのですが、中身を見ると、入力文字のシリアライズや一度限りのDOMのイベント追加（もしかしたら違うかもしれませんが WS とか SSE はあまり分からなかったので）をしているに過ぎず、仮想DOM でダブルバッファリングして Fiberを 差分検知するアルゴリズムの綺麗さには及ばないと思いました（もちろん人それぞれ意見は違うかもしれませんが）。

何はともあれ、これを機に、オレオレHTMXを作る人が増えればいいなと思い末筆にしたいと思います！

Happy Hacking !