mirror of
https://codeberg.org/JasterV/axum-htmx-example.git
synced 2026-04-26 18:10:09 +00:00
basic counter
This commit is contained in:
commit
b81e82512d
12 changed files with 1357 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
1135
Cargo.lock
generated
Normal file
1135
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "axum-htmx"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = "0.6.18"
|
||||
config = "0.13.3"
|
||||
serde = { version = "1.0.167", features = ["derive", "rc"] }
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
anyhow = "1.0.71"
|
||||
askama = "0.12.0"
|
||||
10
Makefile.toml
Normal file
10
Makefile.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[env]
|
||||
PORT = "3000"
|
||||
|
||||
[tasks.run]
|
||||
command = "cargo"
|
||||
args = ["run"]
|
||||
|
||||
[tasks.run-watch]
|
||||
command = "cargo"
|
||||
args = ["watch", "-x", "run"]
|
||||
3
README.md
Normal file
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# axum-htmx
|
||||
|
||||
An example on how to easily serve an HTMX application using Axum and Askama
|
||||
30
src/configuration.rs
Normal file
30
src/configuration.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use anyhow::Context;
|
||||
use config::Config;
|
||||
use serde::Deserialize;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Configuration {
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
pub fn load() -> anyhow::Result<Self> {
|
||||
let config = Config::builder()
|
||||
.add_source(
|
||||
config::Environment::default()
|
||||
.try_parsing(true)
|
||||
.separator("__"),
|
||||
)
|
||||
.build()
|
||||
.context("Failed to load app configuration")?
|
||||
.try_deserialize()
|
||||
.context("Cannot deserialize configuration")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn address(&self) -> SocketAddr {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), self.port)
|
||||
}
|
||||
}
|
||||
30
src/error.rs
Normal file
30
src/error.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
// Make our own error that wraps `anyhow::Error`.
|
||||
#[derive(Debug)]
|
||||
pub struct AppError(anyhow::Error);
|
||||
|
||||
// Tell axum how to convert `AppError` into a response.
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal error: {self:#?}"),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
|
||||
// `Result<_, AppError>`. That way you don't need to do that manually.
|
||||
impl<E> From<E> for AppError
|
||||
where
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
fn from(err: E) -> Self {
|
||||
Self(err.into())
|
||||
}
|
||||
}
|
||||
41
src/main.rs
Normal file
41
src/main.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
mod configuration;
|
||||
mod error;
|
||||
mod pages;
|
||||
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use configuration::Configuration;
|
||||
use pages::counter::CounterState;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct AppState {
|
||||
counter: Arc<CounterState>,
|
||||
}
|
||||
|
||||
fn router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/counter",
|
||||
get(pages::counter::view).with_state(state.counter.clone()),
|
||||
)
|
||||
.route(
|
||||
"/counter/increment",
|
||||
post(pages::counter::increment).with_state(state.counter),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config = Configuration::load().expect("Error loading configuration");
|
||||
let address = config.address();
|
||||
let state = AppState {
|
||||
counter: Arc::new(CounterState::new()),
|
||||
};
|
||||
|
||||
axum::Server::bind(&address)
|
||||
.serve(router(state).into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
36
src/pages/counter.rs
Normal file
36
src/pages/counter.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use crate::pages::HtmlTemplate;
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use std::sync::{atomic::AtomicUsize, Arc};
|
||||
|
||||
pub struct CounterState(AtomicUsize);
|
||||
|
||||
impl CounterState {
|
||||
pub fn new() -> Self {
|
||||
CounterState(AtomicUsize::new(0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "counter.html")]
|
||||
pub struct CounterTemplate {
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
pub async fn view(State(state): State<Arc<CounterState>>) -> impl IntoResponse {
|
||||
let template = CounterTemplate {
|
||||
count: state.0.load(std::sync::atomic::Ordering::Relaxed),
|
||||
};
|
||||
HtmlTemplate::new(template)
|
||||
}
|
||||
|
||||
pub async fn increment(State(state): State<Arc<CounterState>>) -> impl IntoResponse {
|
||||
let increment = 1;
|
||||
let previous = state
|
||||
.0
|
||||
.fetch_add(increment, std::sync::atomic::Ordering::Relaxed);
|
||||
Html(format!("{}", previous + increment))
|
||||
}
|
||||
30
src/pages/html_template.rs
Normal file
30
src/pages/html_template.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use askama::Template;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
|
||||
pub struct HtmlTemplate<T>(T);
|
||||
|
||||
impl<T: Template> HtmlTemplate<T> {
|
||||
pub fn new(template: T) -> Self {
|
||||
HtmlTemplate(template)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for HtmlTemplate<T>
|
||||
where
|
||||
T: Template,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
match self.0.render() {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to render template. Error: {}", err),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
src/pages/mod.rs
Normal file
4
src/pages/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod counter;
|
||||
mod html_template;
|
||||
|
||||
pub use html_template::*;
|
||||
23
templates/counter.html
Normal file
23
templates/counter.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Counter</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- <link href="css/style.css" rel="stylesheet"> -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.3"
|
||||
integrity="sha384-lVb3Rd/Ca0AxaoZg5sACe8FJKF0tnUgR2Kd7ehUOG5GCcROv5uBIZsOqovBAcWua"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<button hx-post="/counter/increment" hx-trigger="click" hx-target="#count">
|
||||
Click Me!
|
||||
</button>
|
||||
<div id="count">{{ count }}</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in a new issue