htmx + Node-RED

## htmx って何？

この記事見ると海外で流行ってるらしいです。

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

簡単に言うと javascript 書かないで HTML だけで Ajax （懐かしい）できるという話。以下のような HTML だけでボタンが `/clicked` のコンテンツに置き換わる。

```html
  <script src="https://unpkg.com/htmx.org@1.9.10"></script>
  <!-- have a button POST a click via AJAX -->
  <button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
  </button>
```

上の記事では JSONPlaceholder というダミーURLを使った以下のようなサンプルがあって、これを動かすとどういうことかよくわかります。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="oNVzLQd" data-user="odlqzyes-the-sans" 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/odlqzyes-the-sans/pen/oNVzLQd">
  Untitled</a> by Tomoki Ota (<a href="https://codepen.io/odlqzyes-the-sans">@odlqzyes-the-sans</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

### これはNode-RED向きなんじゃないか？

以前、Node-RED でフロントエンドどうするのか？ってイベントやった時に [node-red-contrib-uibuilder](https://flows.nodered.org/node/node-red-contrib-uibuilder) を使った開発を紹介しました。

https://enebular.connpass.com/event/235571/

https://www.slideshare.net/joeartsea/uibuilder

（SlideShare 久々に見たけどひどい...）

この時、 React や Vue を知ってる上で多少のコーディングが前提だったんでローコードとしては不評でした（劇的にコード量減らせるんだけど）

そこに、この htmx、これってもう前提が HTML しかなくない？ということで率直に Node-RED に向いてると思いました。

## Node-RED で試してみる

### まずは上記サンプルの移植

template ノードに上記サンプルをそのまま書きます。

![スクリーンショット 2024-02-29 22.09.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/37351e32-0afc-c61a-3b2c-e4296caeaa35.png)

http in ノードと http response ノードで挟んで GET アクセスできるようにします。

![スクリーンショット 2024-02-29 22.13.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/42d45090-b9c7-a199-4947-5231967833a0.png)

`http://localhost:1880/htmx` にアクセスしてみると上記サンプルと同じ挙動になります。

![スクリーンショット 2024-02-29 22.15.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/d7fa2816-d050-b84d-751b-0aa22508ef4f.png)

### 次に JSONPlaceholder をローカルの API に置き換える

データが必要ですが、とりあえず flow コンテキストを使います。先ほど作ったページにアクセスしたら `flow.members` を初期化する change ノードを追加します。

![スクリーンショット 2024-02-29 22.21.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/9864530f-1841-6a52-0202-24be16033b81.png)

change ノードの設定はこんな感じです。

![スクリーンショット 2024-02-29 22.24.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/f370fead-5cda-46ee-b7fa-b75db7ebd000.png)

データの中身はこんな感じ。

![スクリーンショット 2024-02-29 22.26.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/eb379a5a-aff3-03de-c52f-691b3f2b5fbb.png)

このデータを使って `/members` というメンバーリストを返すエンドポイントと、 `/member?id=xxx` でリクエストのあった ID のメンバーだけを返す2つのエンドポイントを作ります。
（API 的に美しくないですがとりあえず）

### エンドポイント `/members` の作成

フロー全体は以下のような感じです。

![スクリーンショット 2024-02-29 22.33.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/2a7d3259-5cd6-13e2-aa36-14c27d37c811.png)

template ノードの設定はこんな感じです。

![スクリーンショット 2024-02-29 22.34.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/8d239769-c60b-ad80-fded-bf8e1f4284f4.png)

あとは最初に作った `/htmx` の template ノードを以下のように書き換えます。

![スクリーンショット 2024-02-29 22.37.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/97a1fa41-6399-184f-bb58-251a2baec6a3.png)

![スクリーンショット 2024-02-29 22.38.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/e6cb3d80-e987-391f-c843-23ac3e29d09e.png)

`http://localhost:1880/htmx` にアクセスしてボタンを押したら以下のようになります。

![スクリーンショット 2024-02-29 22.40.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/40a50a16-daf4-50bf-52f2-8895653a8eda.png)

### エンドポイント `/member` の作成

フロー全体は以下のような感じです。

![スクリーンショット 2024-02-29 22.40.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/0aa83407-ccfd-02cf-2836-bc9bcdb3d92c.png)

change ノードの設定はこんな感じです。

![スクリーンショット 2024-02-29 22.42.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/57ac86b5-e4de-6150-cdbb-55d6846c01e8.png)

JSONata 式が複雑ですが、とりあえず絞り込むためです...（もう function ノードでいいじゃんて言わないで）

![スクリーンショット 2024-02-29 22.43.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/ef71f340-10b4-d281-38bf-cc0474d6b228.png)

template ノードの設定はこんな感じです。

![スクリーンショット 2024-02-29 22.45.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/1431601c-be8a-b872-0dc3-470151106acb.png)

あとは最初に作った `/htmx` の template ノードを以下のように書き換えます。

![スクリーンショット 2024-02-29 22.49.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/01d1a19b-0bd8-2c80-8332-e39100dc3e0b.png)

`http://localhost:1880/htmx` にアクセスしてボタンを押したら以下のようになります。

![スクリーンショット 2024-02-29 22.49.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21109/a2cd9f6f-e9bf-8535-11d9-4371604592ec.png)

フローJSONは以下です。

```json
[
    {
        "id": "334995aafa9a54b3",
        "type": "tab",
        "label": "フロー 2",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "ace3b2921ec2a993",
        "type": "http in",
        "z": "334995aafa9a54b3",
        "name": "",
        "url": "/htmx",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 120,
        "y": 100,
        "wires": [
            [
                "d185b1236d3a130b"
            ]
        ]
    },
    {
        "id": "35a6b86280d8a98a",
        "type": "template",
        "z": "334995aafa9a54b3",
        "name": "",
        "field": "payload",
        "fieldType": "msg",
        "format": "html",
        "syntax": "mustache",
        "template": "<script src=\"https://unpkg.com/htmx.org@1.9.10\"></script>\n\n<h2>メンバーリスト取得</h2>\n<button hx-get=\"http://localhost:1880/members\">Click</button>\n\n<h2>メンバーリスト絞り込み</h2>\n<button hx-get=\"http://localhost:1880/member?id=001\">Click</button>",
        "output": "str",
        "x": 500,
        "y": 100,
        "wires": [
            [
                "38e3a17b24c944da"
            ]
        ]
    },
    {
        "id": "38e3a17b24c944da",
        "type": "http response",
        "z": "334995aafa9a54b3",
        "name": "",
        "statusCode": "",
        "headers": {},
        "x": 650,
        "y": 100,
        "wires": []
    },
    {
        "id": "d185b1236d3a130b",
        "type": "change",
        "z": "334995aafa9a54b3",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "members",
                "pt": "flow",
                "to": "[{\"id\":\"001\",\"name\":\"kojo\"},{\"id\":\"002\",\"name\":\"yokoi\"},{\"id\":\"003\",\"name\":\"taiji\"},{\"id\":\"004\",\"name\":\"seigo\"}]",
                "tot": "json"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 310,
        "y": 100,
        "wires": [
            [
                "35a6b86280d8a98a"
            ]
        ]
    },
    {
        "id": "0ecd4f0590fdcc00",
        "type": "http in",
        "z": "334995aafa9a54b3",
        "name": "",
        "url": "/member",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 130,
        "y": 220,
        "wires": [
            [
                "0b0049b2eeae6f11"
            ]
        ]
    },
    {
        "id": "0b0049b2eeae6f11",
        "type": "change",
        "z": "334995aafa9a54b3",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "$filter($flowContext(\"members\"), function($v, $i, $a) {\t  $v.id = payload.id\t})\t",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 320,
        "y": 220,
        "wires": [
            [
                "6105b6b0d00ba411"
            ]
        ]
    },
    {
        "id": "df7e7f134ed6fa02",
        "type": "http response",
        "z": "334995aafa9a54b3",
        "name": "",
        "statusCode": "",
        "headers": {},
        "x": 650,
        "y": 220,
        "wires": []
    },
    {
        "id": "6105b6b0d00ba411",
        "type": "template",
        "z": "334995aafa9a54b3",
        "name": "",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "<ul>\n    {{#payload}}\n    <li>{{name}}</li>\n    {{/payload}}\n</ul>",
        "output": "str",
        "x": 500,
        "y": 220,
        "wires": [
            [
                "df7e7f134ed6fa02"
            ]
        ]
    },
    {
        "id": "fc3a2399a25af660",
        "type": "http in",
        "z": "334995aafa9a54b3",
        "name": "",
        "url": "/members",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 140,
        "y": 160,
        "wires": [
            [
                "7a77d5e11c31c411"
            ]
        ]
    },
    {
        "id": "03a80983146f5cb3",
        "type": "http response",
        "z": "334995aafa9a54b3",
        "name": "",
        "statusCode": "",
        "headers": {},
        "x": 510,
        "y": 160,
        "wires": []
    },
    {
        "id": "7a77d5e11c31c411",
        "type": "template",
        "z": "334995aafa9a54b3",
        "name": "",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "<ul>\n    {{#flow.members}}\n    <li>{{name}}</li>\n    {{/flow.members}}\n</ul>",
        "output": "str",
        "x": 340,
        "y": 160,
        "wires": [
            [
                "03a80983146f5cb3"
            ]
        ]
    }
]
```

より実践的なお試し記事は[こちら](https://qiita.com/joeartsea/items/7de978f2da26e1d25385)！

https://qiita.com/joeartsea/items/7de978f2da26e1d25385


## なにが Node-RED 向きなのか？

触ってみた感じ htmx はサーバサイドレンダリングへの回帰だと感じました。Next.js とかの流れで SSR が復活してきてますが、 htmx は**ページの一部をサーバサイドレンダリングしたコンテンツに置き換える**ことでフロントエンド開発の複雑な部分を解消しつつ、ユーザー体験もそんなに落とさないという絶妙なラインを責めてるように思いました。

これまでも Node-RED で http 系ノードと template ノードを使って Web ページをレンダリングできましたが、すべてサーバサイドレンダリングの画面遷移になるので1990年代のユーザー体験を強いてました。

そこで、この絶妙な合いの子である htmx と組み合わせることでユーザー体験もそんなに落とさずシンプルなフロントエンド開発ができるんじゃないか！ということで試してみたという記事でした〜（どこまでイケるか、もうちょっとキャッチアップ続けます）

なんていうか **温故知新** ですな（使い方合ってる？）