✖️

HTMXとRustを組み合わせて遊んでみた

2024/03/01に公開

なにやら少し話題になっていたHTMXが気になっていたので、触ってみました。
https://risingstars.js.org/2023/en#section-framework

基本的なことは公式リファレンスにまとまっているので、
今回はRustとHTMXを利用してカウンターのWebアプリを作成してみました。

HTMXとは?

htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext

https://htmx.org/

意訳すると、「JavaScript使わんでも色々できまっせー」って感じらしい。
多分、実際に使ってみると言ってることがわかってくる。

サンプルコード

maudを利用して、HTMXを用いたテンプレートを作成しています。

https://docs.rs/maud/latest/maud/

templates.rs
use maud::{html, Markup, PreEscaped, DOCTYPE};

pub fn create_page() -> Markup {
    html! {
        (DOCTYPE)
        html {
            head {
                title { "Sample App" }
                script src="https://unpkg.com/htmx.org@1.9.10" {}
            }
            body {
                h1 { "HTMX Sample" }
                div id="content" {
                    button hx-get="/test1" hx-trigger="click" hx-target="#display-area" { "Load test1 content" }
                    button hx-get="/test2" hx-trigger="click" hx-target="#display-area" { "Load test2 content" }
                    button hx-get="/test3" hx-trigger="click" hx-target="#display-area" { "Load test3 content" }

                    div id="display-area" {}
                }
            }
        }
    }
}

pub fn create_test1_content() -> Markup {
    html! {
        div {
            h1 { "Test" }
            p { "This is a test1." }
        }
    }
}

pub fn create_test2_content() -> Markup {
    html! {
        div {
            h1 { "Test" }
            p { "This is a test2." }
        }
    }
}

pub fn create_test3_content() -> Markup {
    html! {
        div {
            h1 { "Test" }
            p { "This is a test3." }
        }
    }
}
main.rs
mod templates;
use std::collections::HashMap;

use actix_web::{web, App, HttpResponse, HttpServer, Responder};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(index))
            .route("/test1", web::get().to(test1))
            .route("/test2", web::get().to(test2))
            .route("/test3", web::get().to(test3))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

async fn index() -> HttpResponse {
    let markup = templates::create_page();
    HttpResponse::Ok().content_type("text/html").body(markup.into_string())
}

async fn test1() -> impl Responder {
    let markup = templates::create_test1_content();
    HttpResponse::Ok().content_type("text/html").body(markup.into_string())
}

async fn test2() -> impl Responder {
    let markup = templates::create_test2_content();
    HttpResponse::Ok().content_type("text/html").body(markup.into_string())
}

async fn test3() -> impl Responder {
    let markup = templates::create_test3_content();
    HttpResponse::Ok().content_type("text/html").body(markup.into_string())
}

コードについて、注目して欲しいところは以下の箇所です。

button hx-get="/test1" hx-trigger="click" hx-target="#display-area" { "Load test1 content" }

ボタンなのはパッとみてわかりますが、いくつか見慣れない属性があります。

hx-get

指定のURLに対して、GETリクエストを発行する属性です。
サンプルコードでは、/test1にHTTP GETリクエストを発行しています。

https://htmx.org/attributes/hx-get/

hx-trigger

リクエスト発火のトリガーを指定する属性です。
サンプルコードでは、ボタンをクリックした時に発火するよう指定しています。

https://htmx.org/attributes/hx-trigger/

hx-target

レスポンスの内容を出力するターゲットを指定する属性です。
サンプルコードでは、#display-areaの要素に対して、GETリクエストのレスポンスを出力するように指定してます。

https://htmx.org/attributes/hx-target/

ボタンクリック時の動作

ボタンをクリックすると、以下の処理が実行されることになります。

  1. /test1にHTTP GETリクエストを発行する。
  2. バックエンド側で何やかんや処理されて、レスポンスが返ってくる。(今回はHTML要素を返している)
  3. 取得した結果を#display-areaの要素に対して出力する。

実際に動作した内容を記録した動画です。

JavaScriptを書かずとも、インタラクティブなアプリを作ることができました。

カウンターアプリを作ってみる

もちろん、JavaScriptを利用することもできます。
その例を紹介するために、カウンターアプリを作成してみました。
(無駄な処理をしていますが、お気になさらず🙄)

コード

templates.rs
use maud::{html, Markup, PreEscaped, DOCTYPE};

pub fn create_page() -> Markup {
    html! {
        (DOCTYPE)
        html {
            head {
                title { "Counter App" }
                script src="https://unpkg.com/htmx.org@1.9.10" {}
                script { (PreEscaped(r#"
                    function getResultValue() {
                        return document.getElementById('result').innerText;
                    }
                "#)) }
            }
            body {
                h1 { "HTMX Counter" }
                button hx-get="/increment" hx-trigger="click" hx-target="#result"  hx-vals="js:{count: getResultValue()}" {
                    "+"
                }
                button hx-get="/decrement" hx-trigger="click" hx-target="#result" hx-vals="js:{count: getResultValue()}" {
                    "-"
                }
                div id="result" { "0" }
            }
        }
    }
}
main.rs
mod templates;
use std::collections::HashMap;

use actix_web::{web, App, HttpResponse, HttpServer, Responder};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(index))
            .route("/increment", web::get().to(increment))
            .route("/decrement", web::get().to(decrement))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

async fn increment(query_param: web::Query<HashMap<String, String>>) -> impl Responder {
    let count_str = query_param.get("count").unwrap_or(&"0".to_string()).to_string();
    let count: i32 = count_str.parse().unwrap_or(0);
    HttpResponse::Ok().json(count + 1)
}

async fn decrement(query_param: web::Query<HashMap<String, String>>) -> impl Responder {
    let count_str = query_param.get("count").unwrap_or(&"0".to_string()).to_string();
    let count: i32 = count_str.parse().unwrap_or(0);
    HttpResponse::Ok().json(count - 1)
}
async fn index() -> HttpResponse {
    let markup = templates::create_page();
    HttpResponse::Ok().content_type("text/html").body(markup.into_string())
}

hx-vals

hx-valsという属性を利用して、GETリクエストのパラメータを渡しています。
なので「+」ボタンをクリックすると/increment?count=0というGETリクエストが発行されています。

https://htmx.org/attributes/hx-vals/

バックエンドでは、ただ足し算・引き算をしているだけなので、説明は省きます。

実際の挙動

ちゃんとできていますね👌

まとめ

公式リファレンスを読んでいると、まだまだ試せていないことが多くあります。
Svelteより人気とかふざけんなよ!とか勝手に距離を置いていましたが、触ってみると面白かったです🥺

公式サイトにあるエッセイを読んでいても、HTMXに対する熱い想いを感じられて良いですね!
https://htmx.org/essays/

コラボスタイル Developers

Discussion