✨ (htmx): Añade soporte HTMX a tablas ordenables
- Nuevo `SortDir` en `pagetop::html` para representar direcciones de orden (asc/desc) y calcular la siguiente al pulsar una cabecera. - `hx_table::sort_link()` construye el enlace de ordenación con los cuatro atributos `hx-*` fijos, reutilizable en cualquier tabla. - `HtmxResponse` usa `RoutePath` en `location`/`push_url`/`replace_url`/ `redirect` para preservar "lang"; `location_json()` se separa de `location()` para el caso de configuración JSON personalizada. - Añade el módulo `prelude` y una batería de tests para hx, hx_table, request, response y extension.
This commit is contained in:
parent
55159f6d8f
commit
e7f2563967
14 changed files with 1176 additions and 46 deletions
|
|
@ -24,11 +24,9 @@
|
|||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pagetop::prelude::*;
|
||||
//! use pagetop_htmx::hx;
|
||||
//! use pagetop_htmx::prelude::*;
|
||||
//!
|
||||
//! let endpoint = "/api/items"; // Calculado en tiempo de ejecución.
|
||||
//!
|
||||
//! let props = Props::new(hx::GET, endpoint)
|
||||
//! let props = Props::new(hx::GET, "/api/items")
|
||||
//! .with_prop(PropsOp::set(hx::TARGET, "#list"))
|
||||
//! .with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML));
|
||||
//!
|
||||
|
|
@ -45,7 +43,7 @@
|
|||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pagetop::prelude::*;
|
||||
//! use pagetop_htmx::hx;
|
||||
//! use pagetop_htmx::prelude::*;
|
||||
//!
|
||||
//! #[derive(AutoDefault, Getters)]
|
||||
//! pub struct MyButton {
|
||||
|
|
@ -75,7 +73,7 @@
|
|||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pagetop::prelude::*;
|
||||
//! use pagetop_htmx::hx;
|
||||
//! use pagetop_htmx::prelude::*;
|
||||
//!
|
||||
//! // Evento nativo del DOM: hx-on:click="..."
|
||||
//! // Evento propio de HTMX: hx-on::after-swap="..."
|
||||
|
|
@ -91,7 +89,7 @@
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// # use pagetop_htmx::prelude::*;
|
||||
/// let props = Props::new(hx::GET, "/api/search")
|
||||
/// .with_prop(PropsOp::set(hx::TARGET, "#results"));
|
||||
/// ```
|
||||
|
|
@ -113,7 +111,7 @@ pub const PATCH: &str = "hx-patch";
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// # use pagetop_htmx::prelude::*;
|
||||
/// // Al eliminar un elemento, reemplazarlo con respuesta vacía borra el nodo del DOM.
|
||||
/// let props = Props::new(hx::DELETE, "/api/item/42")
|
||||
/// .with_prop(PropsOp::set(hx::TARGET, "closest li"))
|
||||
|
|
@ -130,7 +128,7 @@ pub const DELETE: &str = "hx-delete";
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// # use pagetop_htmx::prelude::*;
|
||||
/// let props = Props::new(hx::GET, "/api/detalles")
|
||||
/// .with_prop(PropsOp::set(hx::TARGET, "closest article"));
|
||||
/// ```
|
||||
|
|
@ -144,7 +142,7 @@ pub const TARGET: &str = "hx-target";
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// # use pagetop_htmx::prelude::*;
|
||||
/// // Reemplaza el elemento completo con una transición de 300 ms.
|
||||
/// let props = Props::new(hx::SWAP, "outerHTML swap:300ms");
|
||||
/// // O usando la constante tipada más los modificadores:
|
||||
|
|
@ -176,7 +174,7 @@ pub const SELECT_OOB: &str = "hx-select-oob";
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// # use pagetop_htmx::prelude::*;
|
||||
/// // Buscar mientras se escribe, con 400 ms de espera y sólo si el valor cambia:
|
||||
/// let props = Props::new(hx::GET, "/api/search")
|
||||
/// .with_prop(PropsOp::set(hx::TRIGGER, "keyup changed delay:400ms"))
|
||||
|
|
@ -294,6 +292,11 @@ pub const PRESERVE: &str = "hx-preserve";
|
|||
/// - `"sse"` - soporte Server-Sent Events.
|
||||
/// - `"json-enc"` - codifica la petición como JSON en lugar de form-urlencoded.
|
||||
/// - `"loading-states"` - gestión avanzada de estados de carga.
|
||||
///
|
||||
/// `pagetop-htmx` sólo integra el *core* de HTMX: usar cualquiera de estas extensiones (ver el
|
||||
/// [catálogo oficial](https://htmx.org/extensions/)) requiere añadir su script correspondiente por
|
||||
/// separado, por ejemplo con [`JavaScript::defer()`](pagetop::html::JavaScript::defer) en
|
||||
/// [`dependencies()`](pagetop::core::extension::Extension::dependencies).
|
||||
pub const EXT: &str = "hx-ext";
|
||||
|
||||
/// Atributos HTMX que los elementos descendientes NO heredarán de este elemento.
|
||||
|
|
@ -348,7 +351,7 @@ pub const DISABLE: &str = "hx-disable";
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// # use pagetop_htmx::prelude::*;
|
||||
/// let props = Props::new(hx::on("click"), "this.classList.toggle('active')")
|
||||
/// .with_prop(PropsOp::set(hx::on("mouseenter"), "this.style.opacity='0.8'"));
|
||||
/// ```
|
||||
|
|
@ -364,7 +367,7 @@ pub fn on(event: &str) -> String {
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// # use pagetop_htmx::prelude::*;
|
||||
/// let props = Props::new(hx::on_htmx("before-request"), "console.log('enviando...')")
|
||||
/// .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "initTooltips()"));
|
||||
/// ```
|
||||
|
|
@ -382,7 +385,7 @@ pub fn on_htmx(event: &str) -> String {
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::hx;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// async fn handler(request: HttpRequest) {
|
||||
/// if let Some(target) = request.headers().get(hx::request::TARGET) {
|
||||
|
|
@ -417,18 +420,19 @@ pub mod request {
|
|||
/// manualmente, aunque lo habitual es usar el constructor [`HtmxResponse`](crate::HtmxResponse).
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop_htmx::hx;
|
||||
/// use pagetop::web::http::{HeaderMap, HeaderName, HeaderValue};
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// let mut headers = HeaderMap::new();
|
||||
/// let mut headers = web::http::HeaderMap::new();
|
||||
/// headers.insert(
|
||||
/// hx::response::TRIGGER.parse::<HeaderName>().unwrap(),
|
||||
/// HeaderValue::from_static("itemAdded"),
|
||||
/// hx::response::TRIGGER.parse::<web::http::HeaderName>().unwrap(),
|
||||
/// web::http::HeaderValue::from_static("itemAdded"),
|
||||
/// );
|
||||
/// ```
|
||||
pub mod response {
|
||||
/// Redirige mediante AJAX a la URL o configuración JSON indicada. Ver
|
||||
/// [`HtmxResponse::location()`](crate::HtmxResponse::location).
|
||||
/// [`HtmxResponse::location()`](crate::HtmxResponse::location) y
|
||||
/// [`HtmxResponse::location_json()`](crate::HtmxResponse::location_json).
|
||||
pub const LOCATION: &str = "HX-Location";
|
||||
/// Empuja la URL indicada al historial del navegador. Ver
|
||||
/// [`HtmxResponse::push_url()`](crate::HtmxResponse::push_url).
|
||||
|
|
@ -475,7 +479,7 @@ pub mod response {
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// # use pagetop_htmx::prelude::*;
|
||||
/// // Reemplaza el elemento con una transición de 200 ms y desplaza al inicio:
|
||||
/// let props = Props::new(hx::SWAP, format!("{} swap:200ms scroll:top", hx::swap::OUTER_HTML));
|
||||
/// ```
|
||||
|
|
@ -515,7 +519,7 @@ pub mod swap {
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// # use pagetop_htmx::prelude::*;
|
||||
/// // Búsqueda progresiva: petición 400 ms después de que el usuario deje de escribir.
|
||||
/// let search = Props::new(hx::TRIGGER, "keyup changed delay:400ms");
|
||||
///
|
||||
|
|
|
|||
74
extensions/pagetop-htmx/src/hx_table.rs
Normal file
74
extensions/pagetop-htmx/src/hx_table.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
//! Soporte HTMX al componente [`Table`].
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use crate::hx;
|
||||
|
||||
// **< sort_link() >********************************************************************************
|
||||
|
||||
/// Construye un [`SortLink`](pagetop::base::component::table::SortLink) para actualizar el orden de
|
||||
/// la tabla sin recargar la página.
|
||||
///
|
||||
/// [`Table`] y `SortLink` no requieren HTMX. Cada extensión que quiera aplicar una navegación sin
|
||||
/// recarga debe añadir sus propios atributos `hx-*` usando
|
||||
/// [`SortLink::with_prop()`](pagetop::base::component::table::SortLink::with_prop). Como esos
|
||||
/// cuatro atributos son siempre los mismos para cualquier cabecera ordenable (`hx-get` igual al
|
||||
/// `href`, `hx-swap="outerHTML"` y `hx-push-url="true"`, y sólo `hx-target` cambia según la tabla),
|
||||
/// [`sort_link()`] evita reescribirlos en cada columna de cada listado.
|
||||
///
|
||||
/// El enlace resultante funciona igual con o sin HTMX: `href` es siempre la URL real del nuevo
|
||||
/// estado de orden, así que navega correctamente aunque HTMX no esté disponible en el cliente.
|
||||
///
|
||||
/// # Argumentos
|
||||
///
|
||||
/// - `href`: URL completa hacia el nuevo estado de orden, reflejando ya el campo y la dirección
|
||||
/// que resultarán de pulsar esta cabecera. Acepta cualquier tipo convertible a [`RoutePath`],
|
||||
/// normalmente el resultado de [`Context::route()`](pagetop::core::component::Context::route),
|
||||
/// para que el enlace preserve el parámetro `lang` cuando corresponda.
|
||||
/// - `target`: selector CSS del elemento que HTMX debe reemplazar (`hx-target`), típicamente el
|
||||
/// contenedor que envuelve la tabla completa.
|
||||
/// - `dir`: dirección de orden vigente de esta columna, o `None` si la tabla está ordenada
|
||||
/// actualmente por otra columna. Se traslada tal cual a
|
||||
/// [`SortLink::with_dir()`](pagetop::base::component::table::SortLink::with_dir).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// # fn build_column(cx: &Context) -> table::Column {
|
||||
/// let current_field = "username"; // Estado vigente de la tabla.
|
||||
/// let current_dir = html::SortDir::Asc; // Ordenada por "username" en ascendente.
|
||||
///
|
||||
/// let field = "username"; // Cabecera de la propia columna "username".
|
||||
/// let is_active = field == current_field; // En el ejemplo, coincide con el campo vigente.
|
||||
/// let active = is_active.then_some(current_dir); // `Some` sólo si esta columna ordena ahora.
|
||||
/// let next_dir = html::SortDir::next_for(active); // El siguiente clic alterna la dirección.
|
||||
///
|
||||
/// // `cx` es el `Context` de la petición en curso.
|
||||
/// let href = cx
|
||||
/// .route("/admin/users")
|
||||
/// .with_param("sort", field)
|
||||
/// .with_param("dir", next_dir);
|
||||
///
|
||||
/// table::Column::new(L10n::n("User"))
|
||||
/// .with_sort(hx_table::sort_link(href, "#user-table-wrapper", active))
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn sort_link(
|
||||
href: impl Into<RoutePath>,
|
||||
target: impl AsRef<str>,
|
||||
dir: impl Into<Option<SortDir>>,
|
||||
) -> table::SortLink {
|
||||
// Se materializa como `String` propio porque el mismo valor sirve para dos llamadas: como
|
||||
// `RoutePath` en `SortLink::new()` (vía `href.as_str()`) y como `CowStr` en `PropsOp::set()`.
|
||||
let href = href.into().to_string();
|
||||
let target = target.as_ref().to_owned();
|
||||
table::SortLink::new(href.as_ref())
|
||||
.with_dir(dir)
|
||||
.with_prop(PropsOp::set(hx::GET, href))
|
||||
.with_prop(PropsOp::set(hx::TARGET, target))
|
||||
.with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML))
|
||||
.with_prop(PropsOp::set(hx::PUSH_URL, "true"))
|
||||
}
|
||||
|
|
@ -64,11 +64,35 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
.render().await
|
||||
}
|
||||
```
|
||||
|
||||
Cuando los valores se construyen en tiempo de ejecución o quieres que una extensión aplique estos
|
||||
atributos sin que el componente dependa de HTMX, usa `Props` junto con las constantes de `hx` en
|
||||
lugar de escribirlos como literales en `html!`:
|
||||
|
||||
```rust
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_htmx::prelude::*;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
let props = Props::new(hx::GET, "/api/hello")
|
||||
.with_prop(PropsOp::set(hx::TARGET, "#result"));
|
||||
|
||||
Page::new(request)
|
||||
.with_child(Html::with(move |_| html! {
|
||||
button (props) { "Say hello" }
|
||||
div #result {}
|
||||
}))
|
||||
.render().await
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
include_locales!(LOCALES_HTMX);
|
||||
|
||||
pub mod hx;
|
||||
pub mod hx_table;
|
||||
|
||||
mod request;
|
||||
pub use request::HtmxRequestExt;
|
||||
|
|
@ -76,7 +100,14 @@ pub use request::HtmxRequestExt;
|
|||
mod response;
|
||||
pub use response::HtmxResponse;
|
||||
|
||||
include_locales!(LOCALES_HTMX);
|
||||
/// Prelude de `pagetop-htmx`.
|
||||
pub mod prelude {
|
||||
pub use crate::hx;
|
||||
pub use crate::hx_table;
|
||||
|
||||
pub use crate::request::HtmxRequestExt;
|
||||
pub use crate::response::HtmxResponse;
|
||||
}
|
||||
|
||||
/// Integra HTMX 2 en cualquier aplicación PageTop.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::{HtmxRequestExt, HtmxResponse};
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// async fn list_items(request: HttpRequest) -> Response {
|
||||
/// if request.is_htmx() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::{HtmxResponse, hx};
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// async fn add_item(request: HttpRequest) -> impl IntoResponse {
|
||||
/// let new_item = html! { li #item-42 { "New item" } };
|
||||
|
|
@ -37,7 +37,7 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::HtmxResponse;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// async fn delete_item() -> impl IntoResponse {
|
||||
/// HtmxResponse::empty().redirect("/items")
|
||||
|
|
@ -59,7 +59,7 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::HtmxResponse;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// // Dos eventos sin datos:
|
||||
/// HtmxResponse::empty().trigger("itemAdded, listUpdated");
|
||||
|
|
@ -67,6 +67,7 @@ use pagetop::prelude::*;
|
|||
/// // Evento con datos en JSON:
|
||||
/// HtmxResponse::empty().trigger(r#"{"itemAdded": {"id": 42}}"#);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub struct HtmxResponse {
|
||||
markup: Markup,
|
||||
headers: web::http::HeaderMap,
|
||||
|
|
@ -91,45 +92,123 @@ impl HtmxResponse {
|
|||
/// Hace que HTMX realice una navegación AJAX a la URL indicada sin recargar la página.
|
||||
///
|
||||
/// A diferencia de [`redirect()`](Self::redirect), la navegación usa HTMX y actualiza sólo el
|
||||
/// objetivo definido por el destino. Acepta una URL o un objeto JSON con claves `path`,
|
||||
/// `target`, `swap`, `select` y `values` para personalizar la navegación:
|
||||
/// objetivo definido por el destino. Para personalizar `target`, `swap`, `select` o `values`,
|
||||
/// usa [`location_json()`](Self::location_json).
|
||||
///
|
||||
/// Usa [`Context::route()`](pagetop::core::component::Context::route) en lugar de un literal
|
||||
/// para que la URL preserve el parámetro `lang` cuando corresponda:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::HtmxResponse;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// // Navegación simple:
|
||||
/// HtmxResponse::empty().location("/items");
|
||||
///
|
||||
/// // Navegación con destino personalizado:
|
||||
/// HtmxResponse::empty()
|
||||
/// .location(r##"{"path": "/items", "target": "#content"}"##);
|
||||
/// # fn build_response(cx: &Context) -> HtmxResponse {
|
||||
/// HtmxResponse::empty().location(cx.route("/items"))
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn location(self, url: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-location", url)
|
||||
pub fn location(self, url: impl Into<RoutePath>) -> Self {
|
||||
self.set_header(b"hx-location", url.into().to_string())
|
||||
}
|
||||
|
||||
/// Hace que HTMX realice una navegación AJAX personalizada, con un objeto JSON de configuración
|
||||
/// en lugar de una URL simple.
|
||||
///
|
||||
/// Acepta un objeto JSON con las claves `path`, `target`, `swap`, `select` y `values` (ver la
|
||||
/// [documentación de HTMX](https://htmx.org/reference/#response_headers) para el detalle de
|
||||
/// cada una). Al no ser una URL, no admite `Context::route()`: si `path` necesita el parámetro
|
||||
/// `lang`, hay que componerlo a mano antes de construir el JSON. Para una navegación simple sin
|
||||
/// estas opciones, usa [`location()`](Self::location).
|
||||
///
|
||||
/// Si `json` no es sintácticamente válido, la cabecera se descarta y se registra un aviso; el
|
||||
/// resto de la respuesta no se ve afectado. Esta comprobación sólo valida la sintaxis JSON, no
|
||||
/// que las claves sean las que espera HTMX.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// HtmxResponse::empty()
|
||||
/// .location_json(r##"{"path": "/items", "target": "#content"}"##);
|
||||
/// ```
|
||||
///
|
||||
/// Si algún valor se calcula en tiempo de ejecución, constrúyelo con [`serde_json::json!`] en
|
||||
/// lugar de interpolarlo a mano con `format!()`: evita comillas u otros caracteres sin escapar
|
||||
/// que romperían la estructura del JSON.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// let item_name = "Alice's item"; // Contiene una comilla: no es seguro interpolarlo a mano.
|
||||
///
|
||||
/// let json = serde_json::json!({
|
||||
/// "path": "/items",
|
||||
/// "values": { "name": item_name },
|
||||
/// })
|
||||
/// .to_string();
|
||||
///
|
||||
/// HtmxResponse::empty().location_json(json);
|
||||
/// ```
|
||||
pub fn location_json(self, json: impl Into<String>) -> Self {
|
||||
let json = json.into();
|
||||
if let Err(error) = serde_json::from_str::<serde_json::Value>(&json) {
|
||||
trace::warn!(
|
||||
json = %json,
|
||||
%error,
|
||||
"HtmxResponse: invalid JSON in location_json(), header discarded",
|
||||
);
|
||||
return self;
|
||||
}
|
||||
self.set_header(b"hx-location", json)
|
||||
}
|
||||
|
||||
/// Empuja la URL indicada al historial del navegador.
|
||||
///
|
||||
/// El usuario podrá navegar hacia atrás hasta esa URL. Usar `"false"` para desactivar el empuje
|
||||
/// aunque esté habilitado por el atributo `hx-push-url` del elemento.
|
||||
pub fn push_url(self, url: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-push-url", url)
|
||||
///
|
||||
/// Usa [`Context::route()`](pagetop::core::component::Context::route) en lugar de un literal
|
||||
/// para que la URL preserve el parámetro `lang` cuando corresponda:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// # fn build_response(cx: &Context) -> HtmxResponse {
|
||||
/// HtmxResponse::empty().push_url(cx.route("/items"))
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn push_url(self, url: impl Into<RoutePath>) -> Self {
|
||||
self.set_header(b"hx-push-url", url.into().to_string())
|
||||
}
|
||||
|
||||
/// Reemplaza la URL actual en el historial sin añadir una nueva entrada.
|
||||
///
|
||||
/// Usar `"false"` para desactivar el reemplazo.
|
||||
pub fn replace_url(self, url: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-replace-url", url)
|
||||
/// Usar `"false"` para desactivar el reemplazo. Usa
|
||||
/// [`Context::route()`](pagetop::core::component::Context::route) en lugar de un literal para
|
||||
/// que la URL preserve el parámetro `lang` cuando corresponda.
|
||||
pub fn replace_url(self, url: impl Into<RoutePath>) -> Self {
|
||||
self.set_header(b"hx-replace-url", url.into().to_string())
|
||||
}
|
||||
|
||||
/// Provoca una redirección completa del navegador a la URL indicada.
|
||||
///
|
||||
/// A diferencia de [`location()`](Self::location), esta redirección recarga la página por
|
||||
/// completo, como un `window.location.href = url` en JavaScript.
|
||||
pub fn redirect(self, url: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-redirect", url)
|
||||
///
|
||||
/// Usa [`Context::route()`](pagetop::core::component::Context::route) en lugar de un literal
|
||||
/// para que la URL preserve el parámetro `lang` cuando corresponda:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// # fn build_response(cx: &Context) -> HtmxResponse {
|
||||
/// HtmxResponse::empty().redirect(cx.route("/items"))
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn redirect(self, url: impl Into<RoutePath>) -> Self {
|
||||
self.set_header(b"hx-redirect", url.into().to_string())
|
||||
}
|
||||
|
||||
/// Provoca una recarga completa de la página actual.
|
||||
|
|
@ -170,7 +249,7 @@ impl HtmxResponse {
|
|||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::HtmxResponse;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// // Evento simple:
|
||||
/// HtmxResponse::empty().trigger("itemAdded");
|
||||
|
|
@ -181,6 +260,21 @@ impl HtmxResponse {
|
|||
/// // Evento con datos en JSON:
|
||||
/// HtmxResponse::empty().trigger(r#"{"itemAdded": {"id": 42, "name": "Example"}}"#);
|
||||
/// ```
|
||||
///
|
||||
/// Si el dato del evento se calcula en tiempo de ejecución, constrúyelo con
|
||||
/// [`serde_json::json!`] en lugar de interpolarlo a mano con `format!()`, para evitar comillas
|
||||
/// u otros caracteres sin escapar que romperían la estructura del JSON:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// let item_name = "Alice's item"; // Contiene una comilla: no es seguro interpolarlo a mano.
|
||||
///
|
||||
/// let json = serde_json::json!({ "itemAdded": { "name": item_name } }).to_string();
|
||||
///
|
||||
/// HtmxResponse::empty().trigger(json);
|
||||
/// ```
|
||||
pub fn trigger(self, event: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-trigger", event)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue