今更htmxに触れてみた

## htmxとは

htmxは近年話題のJavaScriptライブラリで、JavaScriptのライブラリながらJavaScriptを書くことなく、HTMLだけでAjax通信やそれに伴うDOMの更新を実現することが可能です。

指標として、1年間のGitHubスター数の増加ランキングである[JavaScriptライジングスター 2024](https://risingstars.js.org/2024/ja#section-framework)のフロントエンドフレームワーク部門ではReactを抑えて1位を獲得しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/5572eeba-da5d-4a59-84d9-f4bc18c912c5.png)


### htmxの思想
htmxの思想については[公式サイト](https://htmx.org/)に記載されているモチベーションを見るのが分かりやすいでしょう。

#### motivation

> * Why should only \<a> & \<form> be able to make HTTP requests?
> * Why should only click & submit events trigger them?
> * Why should only GET & POST methods be available?
> * Why should you only be able to replace the entire screen?
> 
> By removing these constraints, htmx completes HTML as a hypertext

↓ Google先生による翻訳版

> * なぜ \<a> と \<form> だけが HTTP リクエストを実行できるのですか?
> * なぜクリック&送信イベントだけがトリガーする必要があるのですか?
> * なぜ GET & POST メソッドだけを利用可能にすべきなのですか?
> * なぜ画面全体しか交換できないのですか?
> 
> これらの制約を取り除くことで、htmx は HTML をハイパーテキストとして完成させます

また、公式サイトには以下のような記述も存在します。
> haiku
javascript fatigue:
longing for a hypertext
already in hand

↓ 翻訳版
> 俳句
> JavaScript疲労： すでに手元にある
> ハイパーテキストへの憧れ

乱暴な言い方ですが、インタラクティブなWebアプリの実装において、何でもかんでもJavaScriptで実装せずにHTML標準の機能に目を向けて実装しよう！という思想のもとで開発されたライブラリなのかな？という印象を受けました。

また、以下の記事でhtmxの思想について詳しく解説されているので詳しく知りたい方は是非ご覧ください。

https://qiita.com/tsmd/items/0d07feb8e02cfa213cc4

https://www.zenryoku-kun.com/new-post/htmx

### 特徴

近年主流となっているReactのようなSPAとバックエンドAPIで構成されるWebアプリのようなJSONでデータをやり取りしてJavaScriptでレンダリングする方式とは異なり、htmxではバックエンドからHTMLを返し、フロントエンドではDOMを置換するだけという形式を取っています。

そのため、クライアント側で複雑なロジックを実装する必要がなく、シンプルな実装で完結するいう特徴があります。

:::note
htmxではサーバーからのレスポンスで指定されたDOMを書き換えるだけなので、JSON形式でレスポンスを受信した場合、JSONがテキストとしてDOMに反映されます。
:::

### 実装してみる

#### 実装イメージ
![HTMX Tutorial.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/670b354f-12f5-44d7-ba14-970e95d505ed.gif)

#### フロントエンド
フロントエンドの実装はindex.htmlのみで、JavaScriptの実装はしていません。

```public/index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>HTMX Tutorial</title>
    <script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>

<body>
    <h1>Welcome to HTMX Tutorial</h1>
    <h3>This is a simple HTMX template.</h3>
    <button hx-get="/list" hx-target="#content" hx-trigger="click" hx-swap="innerHTML">GET LIST</button>
    <div id="content">
        <div>ボタンをクリックしたらここが更新されます。</div>
    </div>
</body>

<style>
    body {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }
</style>
</html>
```

#### バックエンド
今回はバックエンドを[express](https://expressjs.com/ja/)と[ejs](https://ejs.co/)で実装しています。

:::note info
バックエンドに関してはJSONではなくHTMLでレスポンスを返していることだけ理解すればOKです。
:::

<details>

```index.js
import express from "express";

const app = express();
const PORT = 3000;

// Serve static files from the "public" directory
app.use(express.static("public"));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.set("view engine", "ejs");
app.engine("ejs", require("ejs").__express);

const list = [
    { id: 1, name: "John Doe", age: 30 },
    { id: 2, name: "Jane Smith", age: 25 },
    { id: 3, name: "Alice Johnson", age: 28 },
    { id: 4, name: "Bob Brown", age: 35 },
    { id: 5, name: "Charlie Davis", age: 22 },
    { id: 6, name: "Diana Prince", age: 27 },
    { id: 7, name: "Ethan Hunt", age: 32 },
    { id: 8, name: "Fiona Apple", age: 29 },
    { id: 9, name: "George Clooney", age: 40 },
    { id: 10, name: "Hannah Montana", age: 21 },
    { id: 11, name: "Ian Somerhalder", age: 33 },
    { id: 12, name: "Jessica Alba", age: 26 },
    { id: 13, name: "Kevin Spacey", age: 38 },
    { id: 14, name: "Liam Neeson", age: 45 },
    { id: 15, name: "Megan Fox", age: 24 },
    { id: 16, name: "Nicolas Cage", age: 50 },
    { id: 17, name: "Olivia Wilde", age: 31 },
    { id: 18, name: "Paul Rudd", age: 36 },
    { id: 19, name: "Quentin Tarantino", age: 55 },
    { id: 20, name: "Rachel McAdams", age: 23 }
];

// Start the server
app.listen(PORT, () => {
    console.log(`Static site is being hosted at http://localhost:${PORT}`);
});

app.get("/list", (req, res) => {
    res.render("list_template", { list, status: 200 });
});
```

```views/list_template.ejs
<div>
    <h3>ユーザ一覧</h3>
</div>
<table>
    <thead>
        <tr>
            <th>id</th>
            <th>name</th>
            <th>age</th>
        </tr>
    </thead>
    <tbody>
        <% list.forEach(function(item) { %>
            <tr>
                <td>
                    <%= item.id %>
                </td>
                <td>
                    <%= item.name %>
                </td>
                <td>
                    <%= item.age %>
                </td>
            </tr>
            <% }) %>
    </tbody>
</table>
```

</details>

#### 解説
htmxではHTML要素に独自の`hx-xxxxx`属性を指定して各種処理の設定を行います。

下記サンプルでは以下のような設定をしています。
1. ボタンをクリックしたら
1. `/list`へGETリクエストを送信し
1. レスポンスの内容で`#content`配下の要素を置き換える

```html
<button  
    hx-trigger="click" 
    hx-get="/list" 
    hx-target="#content" 
    hx-swap="innerHTML">
    GET LIST
</button>
```

##### hx-trigger
Ajaxリクエストをトリガーする操作を指定します。
`click`や`submit`、`mouseover`といった操作のほか、`load`（初回読み込み時に発火）や`revealed`（viewPortに読み込まれたタイミングで発火）といった特殊なトリガーが存在しています。

##### hx-get
指定したURLに対してGETリクエストを送信する命令を指定します。
その他にも`hx-post`、`hx-put`、`hx-delete`といった形式でHTTPリクエストを指定することが可能です。

##### hx-target
レスポンスの内容で置換するhtml要素をCSSセレクタで指定します。

##### hx-swap
`hx-target`で指定された要素をレスポンスの値で置換する際の設定です。
上記例では`innerHTML`を指定しており、`#content`の内部を置換しています。

```html
<!-- before -->
<div id="content">
    <div>ボタンをクリックしたらここが更新されます。</div>
</div>

<!-- after -->
<div id="content">
    <table>...</table>
</div>
```


## おわりに
今回htmxに入門してみた感想ですが、日本ではまだ流行っていないのか、日本語の記事が少なかったですね。
公式サイトも英語のみなのでしばらくは翻訳機能と仲良くする必要がありそうです。

実装面の話であれば、基本となるのが以下のステップなので学習コストはかなり低い印象を受けました。
1. 特定の動作に紐づけて(hx-trigger)
1. HTTPリクエストを実行して(hx-get, hx-post)
1. 指定した要素をレスポンスの内容で置換する(hx-target)

シンプルなWebサービスであればフロントエンドはhtmxだけで事足りると思うので今後の動向に注目しつつ、htmxで色々遊んでいきたいところです。

## 参考
https://htmx.org/

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

https://qiita.com/tsmd/items/0d07feb8e02cfa213cc4