use acton_htmx::htmx::{HxRequest, HxSwapOob, SwapStrategy};
use acton_htmx::template::HxTemplate;
use askama::Template;
use axum::{
response::{Html, IntoResponse, Response},
routing::get,
Form, Router,
};
use serde::Deserialize;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct AppState {
items: Arc<Mutex<Vec<Item>>>,
counter: Arc<Mutex<i32>>,
}
#[allow(dead_code)]
#[derive(Clone)]
struct Item {
id: usize,
name: String,
}
impl Default for AppState {
fn default() -> Self {
Self {
items: Arc::new(Mutex::new(vec![
Item { id: 1, name: "First item".to_string() },
Item { id: 2, name: "Second item".to_string() },
])),
counter: Arc::new(Mutex::new(2)),
}
}
}
#[derive(Template)]
#[template(source = "<h1>{{ title }}</h1><p>Count: {{ count }}</p>", ext = "html")]
struct SimpleTemplate {
title: String,
count: i32,
}
#[derive(Template)]
#[template(source = "{% for item in items %}<li>{{ item.name }}</li>{% endfor %}", ext = "html")]
struct ItemListTemplate {
items: Vec<Item>,
}
#[derive(Template)]
#[template(source = "<li id=\"item-{{ id }}\">{{ name }}</li>", ext = "html")]
struct ItemTemplate {
id: usize,
name: String,
}
#[derive(Template)]
#[template(source = "<div class=\"flash\">{{ message }}</div>", ext = "html")]
struct FlashTemplate {
message: String,
}
async fn index(
axum::extract::State(state): axum::extract::State<AppState>,
HxRequest(is_htmx): HxRequest,
) -> Response {
let counter = *state.counter.lock().unwrap();
let template = SimpleTemplate {
title: "HTMX Templates Demo".to_string(),
count: counter,
};
template.render_htmx(is_htmx)
}
async fn items_list(
axum::extract::State(state): axum::extract::State<AppState>,
) -> impl IntoResponse {
let items = state.items.lock().unwrap().clone();
let template = ItemListTemplate { items };
template.render_html()
}
#[derive(Deserialize)]
struct AddItemForm {
name: String,
}
async fn add_item(
axum::extract::State(state): axum::extract::State<AppState>,
Form(form): Form<AddItemForm>,
) -> impl IntoResponse {
let new_id = {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
usize::try_from(*counter).unwrap_or(0)
};
let item = Item {
id: new_id,
name: form.name.clone(),
};
state.items.lock().unwrap().push(item);
let item_template = ItemTemplate {
id: new_id,
name: form.name,
};
let flash_template = FlashTemplate {
message: format!("Item {new_id} added!"),
};
let flash_html = flash_template.render().unwrap();
let counter_str = new_id.to_string();
HxSwapOob::with_primary(item_template.render().unwrap())
.with("flash", flash_html, SwapStrategy::InnerHTML)
.with("counter", counter_str, SwapStrategy::InnerHTML)
}
async fn oob_demo() -> impl IntoResponse {
let template = FlashTemplate {
message: "This was an OOB swap!".to_string(),
};
template.render_oob("flash", None)
}
async fn oob_string_demo() -> impl IntoResponse {
let flash1 = FlashTemplate {
message: "First message".to_string(),
};
let flash2 = FlashTemplate {
message: "Second message".to_string(),
};
let oob1 = flash1.render_oob_str("flash1", None).unwrap();
let oob2 = flash2.render_oob_str("flash2", Some("outerHTML")).unwrap();
Html(format!("<p>Main content</p>{oob1}{oob2}"))
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
let state = AppState::default();
let app = Router::new()
.route("/", get(index))
.route("/items", get(items_list).post(add_item))
.route("/oob", get(oob_demo))
.route("/oob-string", get(oob_string_demo))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("Server running at http://127.0.0.1:3000");
println!();
println!("Endpoints:");
println!(" GET / - Index (try with/without HX-Request header)");
println!(" GET /items - List items");
println!(" POST /items - Add item (multi-target OOB response)");
println!(" GET /oob - OOB swap demo");
println!(" GET /oob-string - Manual OOB composition demo");
println!();
println!("Test with curl:");
println!(" curl http://localhost:3000/");
println!(" curl -H 'HX-Request: true' http://localhost:3000/");
println!(" curl -X POST -d 'name=NewItem' http://localhost:3000/items");
axum::serve(listener, app).await.unwrap();
}