ocre/htmx.rs
1use std::convert::Infallible;
2
3use axum::{
4 extract::FromRequestParts,
5 http::{HeaderValue, request::Parts},
6 response::{IntoResponse, Response},
7};
8
9/// Extractor telling whether htmx sent the request: `Htmx(true)` when it has `HX-Request: true`.
10///
11/// Use it to answer with an HTML fragment instead of a full page or a
12/// redirect. It never rejects: any other value or a missing header is
13/// `Htmx(false)`. Requires the `html` feature.
14///
15/// # Examples
16///
17/// ```no_run
18/// use axum::response::{Html, IntoResponse, Redirect, Response};
19/// use ocre::Htmx;
20///
21/// async fn like(Htmx(is_htmx): Htmx) -> Response {
22/// if is_htmx {
23/// Html("<button disabled>Liked</button>").into_response()
24/// } else {
25/// Redirect::to("/").into_response()
26/// }
27/// }
28/// # let _ = like;
29/// ```
30#[derive(Debug, Clone, Copy)]
31pub struct Htmx(pub bool);
32
33impl<S: Send + Sync> FromRequestParts<S> for Htmx {
34 type Rejection = Infallible;
35
36 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
37 Ok(Self(parts.headers.get("hx-request").is_some_and(|v| v == "true")))
38 }
39}
40
41/// Response telling htmx to load another page: `200 OK` with the `HX-Redirect` header.
42///
43/// htmx follows a plain `303` inside the XHR and swaps the result into the
44/// target, which is wrong when an inline widget (a modal, an inline edit
45/// form) should end on a whole new page. `HX-Redirect` makes htmx do a full
46/// browser navigation instead (Loco's `redirect_with_header_key`). Only for
47/// requests where [`Htmx`] is `true`: other clients ignore the header, so
48/// answer them with [`Redirect::to`](axum::response::Redirect::to). A
49/// target that is not a valid header value falls back to `/`.
50///
51/// # Examples
52///
53/// ```
54/// use axum::response::IntoResponse;
55/// use ocre::HxRedirect;
56///
57/// let response = HxRedirect("/posts/7".to_owned()).into_response();
58/// assert_eq!(response.status(), 200);
59/// assert_eq!(response.headers()["hx-redirect"], "/posts/7");
60/// ```
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct HxRedirect(pub String);
63
64impl IntoResponse for HxRedirect {
65 fn into_response(self) -> Response {
66 let target = HeaderValue::from_str(&self.0).unwrap_or(HeaderValue::from_static("/"));
67 ([("hx-redirect", target)], ()).into_response()
68 }
69}
70
71#[cfg(test)]
72#[path = "../tests/htmx.rs"]
73mod tests;