✨ (htmx): Añade integración con HTMX 2
Constantes `hx-*`, `HtmxRequestExt` y `HtmxResponse` cubren el ciclo completo: escribir atributos, leer la petición y construir la respuesta. La extensión Htmx inyecta el script automáticamente. Añade `IntoResponse` y `Response` al prelude de PageTop.
This commit is contained in:
parent
511149caa7
commit
38fd24453e
15 changed files with 1389 additions and 1 deletions
132
extensions/pagetop-htmx/src/request.rs
Normal file
132
extensions/pagetop-htmx/src/request.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
//! Implementación de [`HtmxRequestExt`] para [`pagetop::web::HttpRequest`].
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
// **< HtmxRequestExt >*****************************************************************************
|
||||
|
||||
/// Extiende [`HttpRequest`](pagetop::web::HttpRequest) con métodos para detectar y leer peticiones
|
||||
/// HTMX.
|
||||
///
|
||||
/// HTMX añade cabeceras especiales a cada petición AJAX. Este trait permite acceder a ellas de
|
||||
/// forma expresiva, sin manipular [`pagetop::web::http::HeaderMap`] directamente.
|
||||
///
|
||||
/// El patrón más común es devolver una respuesta distinta según si la petición viene de HTMX
|
||||
/// (fragmento parcial) o de una navegación directa (página completa). Cuando las dos ramas retornan
|
||||
/// tipos distintos, se usa [`pagetop::web::Response`] como tipo común:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::{HtmxRequestExt, HtmxResponse};
|
||||
///
|
||||
/// async fn list_items(request: HttpRequest) -> Response {
|
||||
/// if request.is_htmx() {
|
||||
/// // Fragmento parcial con cabeceras HTMX opcionales.
|
||||
/// HtmxResponse::new(html! { ul { li { "Item 1" } li { "Item 2" } } })
|
||||
/// .into_response()
|
||||
/// } else {
|
||||
/// // Página completa para navegación directa.
|
||||
/// Page::new(request)
|
||||
/// .with_child(Html::with(|_| html! {
|
||||
/// ul { li { "Item 1" } li { "Item 2" } }
|
||||
/// }))
|
||||
/// .render()
|
||||
/// .into_response()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Los nombres de cabecera como constantes están en [`crate::hx::request`].
|
||||
pub trait HtmxRequestExt {
|
||||
/// Devuelve `true` si la petición proviene de HTMX (`HX-Request: true`).
|
||||
///
|
||||
/// Es la comprobación principal para distinguir una petición HTMX de una navegación directa del
|
||||
/// navegador.
|
||||
fn is_htmx(&self) -> bool;
|
||||
|
||||
/// Devuelve `true` si la petición proviene de un enlace o formulario con `hx-boost`.
|
||||
///
|
||||
/// Las peticiones boosted son HTMX pero conservan la semántica de navegación completa: el
|
||||
/// objetivo por defecto es `<body>`. Puede ser útil para no devolver un fragmento parcial en
|
||||
/// este caso.
|
||||
fn is_boosted(&self) -> bool;
|
||||
|
||||
/// Devuelve `true` si la petición es una restauración del historial del navegador.
|
||||
///
|
||||
/// Ocurre cuando el usuario navega hacia atrás o adelante y HTMX necesita restaurar el estado
|
||||
/// de la página. En este caso conviene devolver la página completa.
|
||||
fn is_history_restore(&self) -> bool;
|
||||
|
||||
/// URL de la página activa en el navegador en el momento de la petición (`HX-Current-URL`).
|
||||
///
|
||||
/// Útil para redirigir o actualizar la URL del historial en función de dónde estaba el usuario.
|
||||
fn hx_current_url(&self) -> Option<&str>;
|
||||
|
||||
/// Valor del atributo `id` del elemento objetivo de la petición (`HX-Target`).
|
||||
///
|
||||
/// Si el elemento objetivo no tiene `id`, esta cabecera no se envía.
|
||||
fn hx_target(&self) -> Option<&str>;
|
||||
|
||||
/// Valor del atributo `id` del elemento que disparó la petición (`HX-Trigger`).
|
||||
///
|
||||
/// Si el elemento disparador no tiene `id`, esta cabecera no se envía. Ver también
|
||||
/// [`hx_trigger_name()`](Self::hx_trigger_name).
|
||||
fn hx_trigger_id(&self) -> Option<&str>;
|
||||
|
||||
/// Valor del atributo `name` del elemento que disparó la petición (`HX-Trigger-Name`).
|
||||
///
|
||||
/// Especialmente útil en formularios, donde el elemento disparador puede tener `name` pero no
|
||||
/// `id`.
|
||||
fn hx_trigger_name(&self) -> Option<&str>;
|
||||
|
||||
/// Texto introducido por el usuario en un diálogo `hx-prompt` (`HX-Prompt`).
|
||||
///
|
||||
/// Sólo presente si el elemento tiene el atributo `hx-prompt` y el usuario no canceló el
|
||||
/// diálogo.
|
||||
fn hx_prompt(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
impl HtmxRequestExt for HttpRequest {
|
||||
fn is_htmx(&self) -> bool {
|
||||
header_equals(self.headers(), "hx-request", "true")
|
||||
}
|
||||
|
||||
fn is_boosted(&self) -> bool {
|
||||
header_equals(self.headers(), "hx-boosted", "true")
|
||||
}
|
||||
|
||||
fn is_history_restore(&self) -> bool {
|
||||
header_equals(self.headers(), "hx-history-restore-request", "true")
|
||||
}
|
||||
|
||||
fn hx_current_url(&self) -> Option<&str> {
|
||||
header_str(self.headers(), "hx-current-url")
|
||||
}
|
||||
|
||||
fn hx_target(&self) -> Option<&str> {
|
||||
header_str(self.headers(), "hx-target")
|
||||
}
|
||||
|
||||
fn hx_trigger_id(&self) -> Option<&str> {
|
||||
header_str(self.headers(), "hx-trigger")
|
||||
}
|
||||
|
||||
fn hx_trigger_name(&self) -> Option<&str> {
|
||||
header_str(self.headers(), "hx-trigger-name")
|
||||
}
|
||||
|
||||
fn hx_prompt(&self) -> Option<&str> {
|
||||
header_str(self.headers(), "hx-prompt")
|
||||
}
|
||||
}
|
||||
|
||||
fn header_equals(headers: &web::http::HeaderMap, name: &str, expected: &str) -> bool {
|
||||
headers
|
||||
.get(name)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v == expected)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn header_str<'a>(headers: &'a web::http::HeaderMap, name: &str) -> Option<&'a str> {
|
||||
headers.get(name).and_then(|v| v.to_str().ok())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue