✨ (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
|
|
@ -9,6 +9,7 @@ members = [
|
|||
# Extensions
|
||||
"extensions/pagetop-aliner",
|
||||
"extensions/pagetop-bootsier",
|
||||
"extensions/pagetop-htmx",
|
||||
"extensions/pagetop-seaorm",
|
||||
]
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
|||
# Extensions
|
||||
pagetop-aliner = { version = "0.1", path = "extensions/pagetop-aliner" }
|
||||
pagetop-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" }
|
||||
pagetop-htmx = { version = "0.1", path = "extensions/pagetop-htmx" }
|
||||
pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
|
||||
# PageTop
|
||||
pagetop = { version = "0.5", path = "." }
|
||||
|
|
|
|||
21
extensions/pagetop-htmx/Cargo.toml
Normal file
21
extensions/pagetop-htmx/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "pagetop-htmx"
|
||||
version = "0.1.0"
|
||||
|
||||
description = """
|
||||
Extensión de PageTop que integra HTMX para enriquecer las páginas con interacciones dinámicas.
|
||||
"""
|
||||
categories = ["web-programming"]
|
||||
keywords = ["pagetop", "htmx", "ajax", "ssr"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pagetop.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
pagetop-build.workspace = true
|
||||
201
extensions/pagetop-htmx/LICENSE-APACHE
Normal file
201
extensions/pagetop-htmx/LICENSE-APACHE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 Manuel Cillero
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
21
extensions/pagetop-htmx/LICENSE-MIT
Normal file
21
extensions/pagetop-htmx/LICENSE-MIT
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Manuel Cillero
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
88
extensions/pagetop-htmx/README.md
Normal file
88
extensions/pagetop-htmx/README.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop HTMX</h1>
|
||||
|
||||
<p>Extensión para <strong>PageTop</strong> que integra <a href="https://htmx.org">HTMX</a> para enriquecer las páginas con interacciones dinámicas.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-htmx)
|
||||
[](https://crates.io/crates/pagetop-htmx)
|
||||
[](https://crates.io/crates/pagetop-htmx)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-htmx#licencia)
|
||||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
## Guía rápida
|
||||
|
||||
**Añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-htmx = { ... }
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
|
||||
|
||||
```rust
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
// ...
|
||||
&pagetop_htmx::Htmx
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A partir de ese momento, todas las páginas de la aplicación incluirán automáticamente el script de
|
||||
HTMX 2. Puedes usar los atributos `hx-*` directamente en tus componentes o el código HTML generado:
|
||||
|
||||
```rust
|
||||
use pagetop::prelude::*;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_child(Html::with(|_| html! {
|
||||
button hx-get="/api/hello" hx-target="#result" {
|
||||
"Say hello"
|
||||
}
|
||||
div #result {}
|
||||
}))
|
||||
.render()
|
||||
}
|
||||
```
|
||||
|
||||
## Créditos
|
||||
|
||||
Este *crate* integra la biblioteca [HTMX 2.0.10](https://htmx.org), distribuida bajo licencia
|
||||
[BSD 2-Clause](https://github.com/bigskysoftware/htmx/blob/master/LICENSE).
|
||||
|
||||
## Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
## Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
* **Licencia MIT**
|
||||
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
|
||||
|
||||
* **Licencia Apache, Versión 2.0**
|
||||
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||
el ecosistema Rust.
|
||||
7
extensions/pagetop-htmx/build.rs
Normal file
7
extensions/pagetop-htmx/build.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_dir("./static", None)
|
||||
.with_name("htmx")
|
||||
.build()
|
||||
}
|
||||
549
extensions/pagetop-htmx/src/hx.rs
Normal file
549
extensions/pagetop-htmx/src/hx.rs
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
//! Constantes para los atributos y valores de HTMX 2.
|
||||
//!
|
||||
//! Usar estas constantes en lugar de literales evita errores tipográficos en tiempo de compilación.
|
||||
//! Una mala declaración como `"hx-gte"` no genera ningún error en tiempo de ejecución, falla
|
||||
//! silenciosamente. Con constantes, el compilador lo detecta de inmediato.
|
||||
//!
|
||||
//! # Atributos estáticos en `html!`
|
||||
//!
|
||||
//! Para valores conocidos en tiempo de compilación, los atributos `hx-*` pueden escribirse
|
||||
//! directamente en la macro `html!` sin necesidad de [`Props`](pagetop::html::Props):
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pagetop::prelude::*;
|
||||
//!
|
||||
//! let markup = html! {
|
||||
//! button hx-get="/api/items" hx-target="#list" hx-swap="outerHTML" { "Load" }
|
||||
//! };
|
||||
//! ```
|
||||
//!
|
||||
//! # Atributos dinámicos con [`Props`](pagetop::html::Props)
|
||||
//!
|
||||
//! Cuando los valores se construyen en tiempo de ejecución o se inyectan desde una extensión,
|
||||
//! puedes usar [`Props`](pagetop::html::Props) combinado con las constantes de este módulo:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pagetop::prelude::*;
|
||||
//! use pagetop_htmx::hx;
|
||||
//!
|
||||
//! let endpoint = "/api/items"; // Calculado en tiempo de ejecución.
|
||||
//!
|
||||
//! let props = Props::new(hx::GET, endpoint)
|
||||
//! .with_prop(PropsOp::set(hx::TARGET, "#list"))
|
||||
//! .with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML));
|
||||
//!
|
||||
//! let markup = html! {
|
||||
//! button (props) { "Load" }
|
||||
//! };
|
||||
//! ```
|
||||
//!
|
||||
//! # Integración en componentes
|
||||
//!
|
||||
//! El patrón recomendado es añadir un campo `props: Props` al componente y exponerlo con
|
||||
//! `with_prop()`. Cualquier extensión puede entonces inyectar atributos HTMX sin que el componente
|
||||
//! ni el tema necesiten conocer HTMX:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pagetop::prelude::*;
|
||||
//! use pagetop_htmx::hx;
|
||||
//!
|
||||
//! #[derive(AutoDefault, Getters)]
|
||||
//! pub struct MyButton {
|
||||
//! props: Props,
|
||||
//! }
|
||||
//!
|
||||
//! impl MyButton {
|
||||
//! pub fn new() -> Self { Self::default() }
|
||||
//!
|
||||
//! #[builder_fn]
|
||||
//! pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
//! self.props.alter_prop(op);
|
||||
//! self
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! MyButton::new()
|
||||
//! .with_prop(PropsOp::set(hx::POST, "/api/save"))
|
||||
//! .with_prop(PropsOp::set(hx::TARGET, "#message"))
|
||||
//! .with_prop(PropsOp::set(hx::SWAP, hx::swap::INNER_HTML));
|
||||
//! ```
|
||||
//!
|
||||
//! # Eventos en línea
|
||||
//!
|
||||
//! Para el atributo `hx-on:*` (cuyo nombre incluye el evento y no puede ser una constante) usa las
|
||||
//! funciones [`on()`] y [`on_htmx()`]:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pagetop::prelude::*;
|
||||
//! use pagetop_htmx::hx;
|
||||
//!
|
||||
//! // Evento nativo del DOM: hx-on:click="..."
|
||||
//! // Evento propio de HTMX: hx-on::after-swap="..."
|
||||
//! let props = Props::new(hx::on("click"), "this.classList.toggle('active')")
|
||||
//! .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "console.log('done')"));
|
||||
//! ```
|
||||
|
||||
// **< HTTP Methods >*******************************************************************************
|
||||
|
||||
/// Realiza una petición GET al servidor y aplica la respuesta al objetivo.
|
||||
///
|
||||
/// Es el atributo HTMX más común: carga contenido desde el servidor sin recargar la página.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// let props = Props::new(hx::GET, "/api/search")
|
||||
/// .with_prop(PropsOp::set(hx::TARGET, "#results"));
|
||||
/// ```
|
||||
pub const GET: &str = "hx-get";
|
||||
|
||||
/// Realiza una petición POST al servidor.
|
||||
///
|
||||
/// Se usa habitualmente para enviar datos de formulario o acciones que modifican el estado del
|
||||
/// servidor. Para subida de ficheros, combinar con [`ENCODING`] = `"multipart/form-data"`.
|
||||
pub const POST: &str = "hx-post";
|
||||
|
||||
/// Realiza una petición PUT al servidor.
|
||||
pub const PUT: &str = "hx-put";
|
||||
|
||||
/// Realiza una petición PATCH al servidor.
|
||||
pub const PATCH: &str = "hx-patch";
|
||||
|
||||
/// Realiza una petición DELETE al servidor.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// // 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"))
|
||||
/// .with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML));
|
||||
/// ```
|
||||
pub const DELETE: &str = "hx-delete";
|
||||
|
||||
// **< Target and Swap >****************************************************************************
|
||||
|
||||
/// Selector CSS del elemento que recibirá la respuesta. Por defecto, el elemento mismo.
|
||||
///
|
||||
/// Además de selectores CSS estándar, HTMX acepta `"this"` (el elemento), `"closest X"` (ancestro
|
||||
/// más próximo), `"find X"` (descendiente) y `"next X"` / `"previous X"`.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// let props = Props::new(hx::GET, "/api/detalles")
|
||||
/// .with_prop(PropsOp::set(hx::TARGET, "closest article"));
|
||||
/// ```
|
||||
pub const TARGET: &str = "hx-target";
|
||||
|
||||
/// Cómo se inserta la respuesta en el DOM. Por defecto, `innerHTML`.
|
||||
///
|
||||
/// Los valores estándar están disponibles en el sub-módulo [`swap`]. Además del modo base, se
|
||||
/// pueden añadir modificadores separados por espacio: retrasos (`swap:200ms`), tiempo de
|
||||
/// asentamiento (`settle:300ms`), scroll (`scroll:top`) y foco (`focus-scroll:true`).
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// // 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:
|
||||
/// let props = Props::new(hx::SWAP, format!("{} swap:300ms", hx::swap::OUTER_HTML));
|
||||
/// ```
|
||||
pub const SWAP: &str = "hx-swap";
|
||||
|
||||
/// Sustituye fuera de banda el elemento del DOM cuyo `id` coincida con el indicado.
|
||||
///
|
||||
/// La respuesta del servidor puede incluir elementos marcados con `hx-swap-oob="true"`: HTMX los
|
||||
/// extrae y actualiza el DOM en la posición correcta, independientemente del objetivo principal.
|
||||
pub const SWAP_OOB: &str = "hx-swap-oob";
|
||||
|
||||
/// Selector CSS del fragmento de la respuesta que se insertará en el objetivo.
|
||||
///
|
||||
/// Permite devolver una página completa desde el servidor y que HTMX extraiga sólo la parte
|
||||
/// relevante, facilitando la reutilización de rutas existentes.
|
||||
pub const SELECT: &str = "hx-select";
|
||||
|
||||
/// Selector CSS de fragmentos de la respuesta que se sustituyen fuera de banda.
|
||||
pub const SELECT_OOB: &str = "hx-select-oob";
|
||||
|
||||
// **< Trigger >************************************************************************************
|
||||
|
||||
/// Evento que activa la petición. Por defecto, `click` en botones y links, `change` en inputs.
|
||||
///
|
||||
/// Los valores simples están disponibles en el sub-módulo [`trigger`]. Las expresiones de disparo
|
||||
/// compuestas se escriben como literales de cadena:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// // 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"))
|
||||
/// .with_prop(PropsOp::set(hx::TARGET, "#results"));
|
||||
///
|
||||
/// // Disparar una vez al cargar la página:
|
||||
/// let lazy = Props::new(hx::GET, "/api/stats")
|
||||
/// .with_prop(PropsOp::set(hx::TRIGGER, "load once"));
|
||||
///
|
||||
/// // Polling cada 5 segundos:
|
||||
/// let poll = Props::new(hx::GET, "/api/estado")
|
||||
/// .with_prop(PropsOp::set(hx::TRIGGER, "every 5s"));
|
||||
/// ```
|
||||
pub const TRIGGER: &str = "hx-trigger";
|
||||
|
||||
/// Convierte en AJAX todos los enlaces y formularios del elemento y sus descendientes.
|
||||
///
|
||||
/// El valor debe ser `"true"` o `"false"`. Cuando vale `"true"`, las respuestas se aplican al
|
||||
/// `<body>` por defecto; se puede combinar con [`TARGET`] para redirigirlas.
|
||||
pub const BOOST: &str = "hx-boost";
|
||||
|
||||
/// Empuja la URL de la respuesta al historial del navegador.
|
||||
///
|
||||
/// Acepta `"true"` (usa la URL de la petición), `"false"` (desactiva) o una URL concreta. Permite
|
||||
/// navegación con el botón atrás manteniendo el comportamiento SPA.
|
||||
pub const PUSH_URL: &str = "hx-push-url";
|
||||
|
||||
/// Reemplaza la URL actual en el historial sin añadir una nueva entrada.
|
||||
///
|
||||
/// Acepta `"true"`, `"false"` o una URL concreta. Útil cuando la petición refina la vista sin que
|
||||
/// deba ser un paso independiente en el historial.
|
||||
pub const REPLACE_URL: &str = "hx-replace-url";
|
||||
|
||||
/// Sincroniza las peticiones del elemento con las de otros elementos.
|
||||
///
|
||||
/// Formato: `"selector:estrategia"`. Estrategias disponibles: `drop` (descarta la nueva si hay una
|
||||
/// en curso), `abort` (cancela la nueva), `replace` (cancela la anterior), `queue first|last|all`
|
||||
/// (encola). Ejemplo: `"#form:abort"`.
|
||||
pub const SYNC: &str = "hx-sync";
|
||||
|
||||
// **< Request Data >*******************************************************************************
|
||||
|
||||
/// Selector CSS de elementos adicionales cuyos valores se incluyen en la petición.
|
||||
///
|
||||
/// Acepta selectores CSS estándar más las extensiones de HTMX: `"this"`, `"closest X"`, `"find X"`.
|
||||
/// Útil para incluir campos de un formulario padre en una petición de detalle.
|
||||
pub const INCLUDE: &str = "hx-include";
|
||||
|
||||
/// Controla qué parámetros del formulario se envían en la petición.
|
||||
///
|
||||
/// - `"*"` - todos (valor por defecto).
|
||||
/// - `"none"` - ninguno.
|
||||
/// - Lista de nombres: `"name surname"` - sólo esos.
|
||||
/// - Exclusión: `"not surname"` - todos excepto los indicados.
|
||||
pub const PARAMS: &str = "hx-params";
|
||||
|
||||
/// Valores extra en JSON que se añaden a los parámetros de la petición.
|
||||
///
|
||||
/// Formato: objeto JSON. Los valores sobreescriben parámetros del formulario con el mismo nombre.
|
||||
/// Admite JavaScript con el prefijo `js:`: `"js:{date: new Date().toISOString()}"`.
|
||||
pub const VALS: &str = "hx-vals";
|
||||
|
||||
/// Cabeceras extra en JSON que se añaden a la petición.
|
||||
///
|
||||
/// Formato: objeto JSON. Permiten enviar contexto (token de sesión, versión de API, etc.) sin
|
||||
/// exponerlo en los parámetros visibles del formulario.
|
||||
pub const HEADERS: &str = "hx-headers";
|
||||
|
||||
/// Codificación de la petición. Por defecto, `"application/x-www-form-urlencoded"`.
|
||||
///
|
||||
/// Usar `"multipart/form-data"` para peticiones que incluyan campos de tipo `file`.
|
||||
pub const ENCODING: &str = "hx-encoding";
|
||||
|
||||
// **< Element Behavior >***************************************************************************
|
||||
|
||||
/// Selector CSS del indicador de carga que se muestra mientras dura la petición.
|
||||
///
|
||||
/// El elemento indicado recibe la clase `htmx-request` durante la petición. Por defecto, si no se
|
||||
/// especifica, la recibe el propio elemento que realiza la petición.
|
||||
pub const INDICATOR: &str = "hx-indicator";
|
||||
|
||||
/// Selector CSS de elementos que se deshabilitan mientras dura la petición.
|
||||
///
|
||||
/// Añade el atributo `disabled` durante la petición y lo elimina al terminar. Evita envíos
|
||||
/// duplicados al hacer clic varias veces.
|
||||
pub const DISABLED_ELT: &str = "hx-disabled-elt";
|
||||
|
||||
/// Muestra un diálogo de confirmación (`window.confirm`) antes de enviar la petición.
|
||||
///
|
||||
/// Si el usuario cancela, la petición no se realiza. El valor es el texto del mensaje.
|
||||
pub const CONFIRM: &str = "hx-confirm";
|
||||
|
||||
/// Muestra un `prompt` de texto y envía el resultado como cabecera `HX-Prompt`.
|
||||
///
|
||||
/// Si el usuario cancela el prompt, la petición no se realiza.
|
||||
pub const PROMPT: &str = "hx-prompt";
|
||||
|
||||
/// Activa la validación HTML5 del formulario antes de enviar la petición.
|
||||
///
|
||||
/// Si algún campo no supera la validación nativa del navegador, la petición se cancela.
|
||||
pub const VALIDATE: &str = "hx-validate";
|
||||
|
||||
/// Preserva el elemento entre respuestas HTMX.
|
||||
///
|
||||
/// El elemento debe tener un `id` único. HTMX no lo destruye ni lo recrea al aplicar la respuesta,
|
||||
/// manteniendo su estado interno (p. ej. posición de reproducción de un vídeo).
|
||||
pub const PRESERVE: &str = "hx-preserve";
|
||||
|
||||
// **< Config and Extensions >**********************************************************************
|
||||
|
||||
/// Activa una o varias extensiones HTMX en el elemento y sus descendientes.
|
||||
///
|
||||
/// Las extensiones se identifican por nombre y se separan con comas. Extensiones comunes:
|
||||
/// - `"ws"` - soporte WebSocket.
|
||||
/// - `"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.
|
||||
pub const EXT: &str = "hx-ext";
|
||||
|
||||
/// Atributos HTMX que los elementos descendientes NO heredarán de este elemento.
|
||||
///
|
||||
/// Acepta una lista de atributos separados por comas o `"*"` para bloquear toda herencia.
|
||||
pub const DISINHERIT: &str = "hx-disinherit";
|
||||
|
||||
/// Atributos HTMX que los elementos descendientes SÍ heredarán (anula [`DISINHERIT`]).
|
||||
pub const INHERIT: &str = "hx-inherit";
|
||||
|
||||
/// Opciones de configuración de la petición en JSON.
|
||||
///
|
||||
/// Claves disponibles: `timeout` (ms), `credentials` (`"include"`, `"omit"`...), `noHeaders`
|
||||
/// (bool), `getWithBody` (bool).
|
||||
pub const REQUEST: &str = "hx-request";
|
||||
|
||||
/// Controla si este elemento participa en el historial del navegador.
|
||||
///
|
||||
/// Con el valor `"false"`, las peticiones de este elemento no se guardan en el historial aunque
|
||||
/// [`PUSH_URL`] esté activo en un elemento padre.
|
||||
pub const HISTORY: &str = "hx-history";
|
||||
|
||||
/// Designa el elemento como contenedor del historial del navegador.
|
||||
///
|
||||
/// HTMX guarda y restaura el contenido de este elemento al navegar hacia atrás/adelante. Sólo debe
|
||||
/// haber un elemento con este atributo en la página.
|
||||
pub const HISTORY_ELT: &str = "hx-history-elt";
|
||||
|
||||
/// Desactiva el procesamiento HTMX en el elemento y todos sus descendientes.
|
||||
///
|
||||
/// Útil para aislar zonas del DOM gestionadas por otra librería o para desactivar HTMX en secciones
|
||||
/// de contenido generado dinámicamente donde no debe intervenir.
|
||||
pub const DISABLE: &str = "hx-disable";
|
||||
|
||||
// **< Inline Events (hx-on) >**********************************************************************
|
||||
|
||||
/// Genera `hx-on:{event}` para escuchar eventos nativos del DOM en línea.
|
||||
///
|
||||
/// Es la alternativa de HTMX a los manejadores `on*` de HTML (`onclick`, `onmouseenter`, ...). La
|
||||
/// diferencia clave está en cómo los trata el navegador bajo una política CSP (*Content Security
|
||||
/// Policy*): los atributos `on*` son JavaScript en línea y quedan bloqueados si la CSP no incluye
|
||||
/// `'unsafe-inline'`; en cambio, `hx-on:*` es un atributo de datos que HTMX lee e interpreta desde
|
||||
/// su propio código ya autorizado, por lo que la CSP no lo bloquea.
|
||||
///
|
||||
/// La CSP puede definirla el servidor en la cabecera HTTP `Content-Security-Policy`, o la
|
||||
/// aplicación en una etiqueta `<meta http-equiv="Content-Security-Policy">` en el `<head>` del
|
||||
/// documento. En la práctica, pocas aplicaciones configuran una CSP estricta, pero es una buena
|
||||
/// práctica de seguridad que conviene tener en cuenta.
|
||||
///
|
||||
/// El valor es código JavaScript que se ejecuta cuando el evento se dispara; `event` contiene el
|
||||
/// objeto del evento.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// let props = Props::new(hx::on("click"), "this.classList.toggle('active')")
|
||||
/// .with_prop(PropsOp::set(hx::on("mouseenter"), "this.style.opacity='0.8'"));
|
||||
/// ```
|
||||
pub fn on(event: &str) -> String {
|
||||
format!("hx-on:{event}")
|
||||
}
|
||||
|
||||
/// Genera `hx-on::{event}` para escuchar eventos propios de HTMX en línea.
|
||||
///
|
||||
/// Los eventos de HTMX usan un doble carácter dos-puntos (`hx-on::evento`). El ciclo de vida
|
||||
/// completo incluye `before-request`, `after-request`, `before-swap`, `after-swap`,
|
||||
/// `before-settle`, `after-settle`, `after-on-load`, `history-restore`, entre otros.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// let props = Props::new(hx::on_htmx("before-request"), "console.log('enviando...')")
|
||||
/// .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "initTooltips()"));
|
||||
/// ```
|
||||
pub fn on_htmx(event: &str) -> String {
|
||||
format!("hx-on::{event}")
|
||||
}
|
||||
|
||||
// **< HTMX Request Headers >***********************************************************************
|
||||
|
||||
/// Nombres de las cabeceras que HTMX envía con cada petición AJAX.
|
||||
///
|
||||
/// Están en minúsculas porque así las normaliza el módulo `http`. Se pueden usar con
|
||||
/// [`HttpRequest::headers()`](pagetop::web::HttpRequest::headers) para leer sus valores
|
||||
/// directamente, aunque lo habitual es usar el trait [`HtmxRequestExt`](crate::HtmxRequestExt).
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::hx;
|
||||
///
|
||||
/// async fn handler(request: HttpRequest) {
|
||||
/// if let Some(target) = request.headers().get(hx::request::TARGET) {
|
||||
/// // El elemento objetivo tenía este id.
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub mod request {
|
||||
/// Siempre `"true"` en peticiones HTMX. Permite distinguirlas de navegaciones directas.
|
||||
pub const REQUEST: &str = "hx-request";
|
||||
/// `"true"` si la petición viene de un enlace o formulario con `hx-boost`.
|
||||
pub const BOOSTED: &str = "hx-boosted";
|
||||
/// URL de la página activa en el navegador cuando se realizó la petición.
|
||||
pub const CURRENT_URL: &str = "hx-current-url";
|
||||
/// `"true"` si la petición es una restauración del historial del navegador.
|
||||
pub const HISTORY_RESTORE_REQUEST: &str = "hx-history-restore-request";
|
||||
/// Texto introducido por el usuario en un diálogo `hx-prompt`.
|
||||
pub const PROMPT: &str = "hx-prompt";
|
||||
/// Valor del atributo `id` del elemento objetivo de la petición.
|
||||
pub const TARGET: &str = "hx-target";
|
||||
/// Valor del atributo `id` del elemento que disparó la petición.
|
||||
pub const TRIGGER: &str = "hx-trigger";
|
||||
/// Valor del atributo `name` del elemento que disparó la petición.
|
||||
pub const TRIGGER_NAME: &str = "hx-trigger-name";
|
||||
}
|
||||
|
||||
// **< HTMX Response Headers >**********************************************************************
|
||||
|
||||
/// Cabeceras de respuesta HTTP para HTMX.
|
||||
///
|
||||
/// Se pueden usar con [`HeaderMap`](pagetop::web::http::HeaderMap) para construir respuestas
|
||||
/// 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};
|
||||
///
|
||||
/// let mut headers = HeaderMap::new();
|
||||
/// headers.insert(
|
||||
/// hx::response::TRIGGER.parse::<HeaderName>().unwrap(),
|
||||
/// HeaderValue::from_static("itemAdded"),
|
||||
/// );
|
||||
/// ```
|
||||
pub mod response {
|
||||
/// Redirige mediante AJAX a la URL o configuración JSON indicada. Ver
|
||||
/// [`HtmxResponse::location()`](crate::HtmxResponse::location).
|
||||
pub const LOCATION: &str = "HX-Location";
|
||||
/// Empuja la URL indicada al historial del navegador. Ver
|
||||
/// [`HtmxResponse::push_url()`](crate::HtmxResponse::push_url).
|
||||
pub const PUSH_URL: &str = "HX-Push-Url";
|
||||
/// Provoca una redirección completa del navegador. Ver
|
||||
/// [`HtmxResponse::redirect()`](crate::HtmxResponse::redirect).
|
||||
pub const REDIRECT: &str = "HX-Redirect";
|
||||
/// Provoca una recarga completa de la página. Ver
|
||||
/// [`HtmxResponse::refresh()`](crate::HtmxResponse::refresh).
|
||||
pub const REFRESH: &str = "HX-Refresh";
|
||||
/// Reemplaza la URL actual en el historial. Ver
|
||||
/// [`HtmxResponse::replace_url()`](crate::HtmxResponse::replace_url).
|
||||
pub const REPLACE_URL: &str = "HX-Replace-Url";
|
||||
/// Anula el `hx-swap` del elemento. Ver
|
||||
/// [`HtmxResponse::reswap()`](crate::HtmxResponse::reswap).
|
||||
pub const RESWAP: &str = "HX-Reswap";
|
||||
/// Anula el `hx-target` del elemento. Ver
|
||||
/// [`HtmxResponse::retarget()`](crate::HtmxResponse::retarget).
|
||||
pub const RETARGET: &str = "HX-Retarget";
|
||||
/// Anula el `hx-select` del elemento. Ver
|
||||
/// [`HtmxResponse::reselect()`](crate::HtmxResponse::reselect).
|
||||
pub const RESELECT: &str = "HX-Reselect";
|
||||
/// Dispara eventos JavaScript al completar la respuesta. Ver
|
||||
/// [`HtmxResponse::trigger()`](crate::HtmxResponse::trigger).
|
||||
pub const TRIGGER: &str = "HX-Trigger";
|
||||
/// Dispara eventos tras la fase *settle*. Ver
|
||||
/// [`HtmxResponse::trigger_after_settle()`](crate::HtmxResponse::trigger_after_settle).
|
||||
pub const TRIGGER_AFTER_SETTLE: &str = "HX-Trigger-After-Settle";
|
||||
/// Dispara eventos tras el *swap*. Ver
|
||||
/// [`HtmxResponse::trigger_after_swap()`](crate::HtmxResponse::trigger_after_swap).
|
||||
pub const TRIGGER_AFTER_SWAP: &str = "HX-Trigger-After-Swap";
|
||||
}
|
||||
|
||||
// **< hx-swap Values >*****************************************************************************
|
||||
|
||||
/// Valores estándar del atributo [`SWAP`] (`hx-swap`).
|
||||
///
|
||||
/// Se pueden combinar con modificadores separados por espacio:
|
||||
/// - `swap:Xms` - tiempo de espera antes de realizar el intercambio.
|
||||
/// - `settle:Xms` - tiempo de espera antes de quitar las clases de transición.
|
||||
/// - `scroll:top` / `scroll:bottom` - desplaza el objetivo tras el intercambio.
|
||||
/// - `show:top` / `show:bottom` - hace visible el objetivo tras el intercambio.
|
||||
/// - `focus-scroll:true` - sigue al elemento enfocado.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// // 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));
|
||||
/// ```
|
||||
pub mod swap {
|
||||
/// Reemplaza el contenido interior del objetivo (valor por defecto de HTMX).
|
||||
pub const INNER_HTML: &str = "innerHTML";
|
||||
/// Reemplaza el elemento objetivo completo.
|
||||
pub const OUTER_HTML: &str = "outerHTML";
|
||||
/// Inserta la respuesta antes de la etiqueta de apertura del objetivo.
|
||||
pub const BEFORE_BEGIN: &str = "beforebegin";
|
||||
/// Inserta la respuesta al inicio del contenido del objetivo.
|
||||
pub const AFTER_BEGIN: &str = "afterbegin";
|
||||
/// Inserta la respuesta al final del contenido del objetivo.
|
||||
pub const BEFORE_END: &str = "beforeend";
|
||||
/// Inserta la respuesta después de la etiqueta de cierre del objetivo.
|
||||
pub const AFTER_END: &str = "afterend";
|
||||
/// Elimina el elemento objetivo independientemente de la respuesta.
|
||||
pub const DELETE: &str = "delete";
|
||||
/// No realiza ningún intercambio; útil cuando sólo importan las cabeceras de respuesta.
|
||||
pub const NONE: &str = "none";
|
||||
}
|
||||
|
||||
// **< hx-trigger Values >**************************************************************************
|
||||
|
||||
/// Eventos comunes del atributo [`TRIGGER`] (`hx-trigger`).
|
||||
///
|
||||
/// Estos valores cubren los disparadores más simples. Las expresiones de disparo compuestas deben
|
||||
/// escribirse como literales de cadena. Modificadores disponibles:
|
||||
/// - `once` - se dispara sólo la primera vez.
|
||||
/// - `changed` - sólo si el valor del elemento ha cambiado.
|
||||
/// - `delay:Xms` - espera antes de disparar (se cancela si el evento vuelve a ocurrir).
|
||||
/// - `throttle:Xms` - limita la frecuencia máxima de disparo.
|
||||
/// - `from:selector` - escucha el evento en otro elemento.
|
||||
/// - `target:selector` - sólo si el evento viene del selector indicado.
|
||||
/// - `consume` - evita que el evento se propague a otros elementos HTMX.
|
||||
/// - `queue:first|last|all|none` - política de cola cuando llegan eventos consecutivos.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_htmx::hx;
|
||||
/// // 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");
|
||||
///
|
||||
/// // Carga diferida al entrar en el viewport, una sola vez.
|
||||
/// let lazy = Props::new(hx::TRIGGER, "intersect once");
|
||||
///
|
||||
/// // Polling: actualiza cada 10 segundos mientras el elemento esté en el DOM.
|
||||
/// let poll = Props::new(hx::TRIGGER, "every 10s");
|
||||
///
|
||||
/// // Múltiples eventos: clic o pulsación de Enter en el campo.
|
||||
/// let multi = Props::new(hx::TRIGGER, "click, keyup[key=='Enter']");
|
||||
///
|
||||
/// // Escucha un evento personalizado emitido desde otro elemento.
|
||||
/// let custom = Props::new(hx::TRIGGER, "itemAdded from:body");
|
||||
/// ```
|
||||
pub mod trigger {
|
||||
/// Se dispara al hacer clic (valor por defecto en la mayoría de elementos interactivos).
|
||||
pub const CLICK: &str = "click";
|
||||
/// Se dispara cuando el valor del elemento cambia (valor por defecto en `input`/`select`).
|
||||
pub const CHANGE: &str = "change";
|
||||
/// Se dispara al enviar un formulario.
|
||||
pub const SUBMIT: &str = "submit";
|
||||
/// Se dispara al soltar una tecla.
|
||||
pub const KEYUP: &str = "keyup";
|
||||
/// Se dispara cuando la página termina de cargarse.
|
||||
pub const LOAD: &str = "load";
|
||||
/// Se dispara cuando el elemento entra en el área visible del *viewport* al hacer scroll.
|
||||
pub const REVEALED: &str = "revealed";
|
||||
/// Se dispara cuando el elemento intersecta con el *viewport* (Intersection Observer API).
|
||||
pub const INTERSECT: &str = "intersect";
|
||||
}
|
||||
129
extensions/pagetop-htmx/src/lib.rs
Normal file
129
extensions/pagetop-htmx/src/lib.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop HTMX</h1>
|
||||
|
||||
<p>Extensión para <strong>PageTop</strong> que integra <a href="https://htmx.org">HTMX</a> para enriquecer las páginas con interacciones dinámicas.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-htmx)
|
||||
[](https://crates.io/crates/pagetop-htmx)
|
||||
[](https://crates.io/crates/pagetop-htmx)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-htmx#licencia)
|
||||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
## Guía rápida
|
||||
|
||||
**Añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-htmx = { ... }
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
|
||||
|
||||
```rust
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
// ...
|
||||
&pagetop_htmx::Htmx
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A partir de ese momento, todas las páginas de la aplicación incluirán automáticamente el script de
|
||||
HTMX 2. Puedes usar los atributos `hx-*` directamente en tus componentes o el código HTML generado:
|
||||
|
||||
```rust
|
||||
use pagetop::prelude::*;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_child(Html::with(|_| html! {
|
||||
button hx-get="/api/hello" hx-target="#result" {
|
||||
"Say hello"
|
||||
}
|
||||
div #result {}
|
||||
}))
|
||||
.render()
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
pub mod hx;
|
||||
|
||||
mod request;
|
||||
pub use request::HtmxRequestExt;
|
||||
|
||||
mod response;
|
||||
pub use response::HtmxResponse;
|
||||
|
||||
include_locales!(LOCALES_HTMX);
|
||||
|
||||
/// Integra HTMX 2 en cualquier aplicación PageTop.
|
||||
///
|
||||
/// Poner esta extensión en [`dependencies()`](pagetop::core::extension::Extension::dependencies)
|
||||
/// hace que todas las páginas de la aplicación incluyan automáticamente el script de HTMX mediante
|
||||
/// un atributo [`defer`](pagetop::html::JavaScript::defer). No es necesaria ninguna configuración
|
||||
/// adicional.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// struct MyApp;
|
||||
///
|
||||
/// impl Extension for MyApp {
|
||||
/// fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
/// vec![
|
||||
/// // ...
|
||||
/// &pagetop_htmx::Htmx
|
||||
/// // ...
|
||||
/// ]
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Htmx;
|
||||
|
||||
impl Extension for Htmx {
|
||||
fn name(&self) -> L10n {
|
||||
L10n::t("extension_name", &LOCALES_HTMX)
|
||||
}
|
||||
|
||||
fn description(&self) -> L10n {
|
||||
L10n::t("extension_description", &LOCALES_HTMX)
|
||||
}
|
||||
|
||||
fn actions(&self) -> Vec<ActionBox> {
|
||||
actions![action::page::BeforeRenderBody::new(add_htmx_script)]
|
||||
}
|
||||
|
||||
fn configure_router(&self, router: Router) -> Router {
|
||||
serve_static_files!(router, [htmx] => "/htmx");
|
||||
router
|
||||
}
|
||||
}
|
||||
|
||||
fn add_htmx_script(page: &mut Page) {
|
||||
page.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/htmx/js/htmx.min.js").with_version("2.0.10"),
|
||||
));
|
||||
}
|
||||
2
extensions/pagetop-htmx/src/locale/en-US/extension.ftl
Normal file
2
extensions/pagetop-htmx/src/locale/en-US/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
extension_name = HTMX
|
||||
extension_description = Integrates HTMX to enrich pages with dynamic interactions.
|
||||
2
extensions/pagetop-htmx/src/locale/es-ES/extension.ftl
Normal file
2
extensions/pagetop-htmx/src/locale/es-ES/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
extension_name = HTMX
|
||||
extension_description = Integra HTMX para enriquecer las páginas con interacciones dinámicas.
|
||||
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())
|
||||
}
|
||||
228
extensions/pagetop-htmx/src/response.rs
Normal file
228
extensions/pagetop-htmx/src/response.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
//! Implementación de [`HtmxResponse`] e [`IntoResponse`](pagetop::web::IntoResponse) para HTMX.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
// **< HtmxResponse >*******************************************************************************
|
||||
|
||||
/// Generador de respuestas HTML parciales con cabeceras HTMX.
|
||||
///
|
||||
/// En una aplicación HTMX, los *handlers* del servidor devuelven con frecuencia fragmentos HTML
|
||||
/// parciales acompañados de cabeceras especiales que instruyen al cliente sobre qué hacer con la
|
||||
/// respuesta: actualizar la URL del historial, disparar eventos JavaScript, redirigir, etc.
|
||||
///
|
||||
/// Implementa [`IntoResponse`](pagetop::web::IntoResponse), por lo que puede devolverse
|
||||
/// directamente desde cualquier *handler*.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::{HtmxResponse, hx};
|
||||
///
|
||||
/// async fn add_item(request: HttpRequest) -> impl IntoResponse {
|
||||
/// let new_item = html! { li #item-42 { "New item" } };
|
||||
///
|
||||
/// HtmxResponse::new(new_item)
|
||||
/// .retarget("#list")
|
||||
/// .reswap(hx::swap::BEFORE_END)
|
||||
/// .push_url("/items")
|
||||
/// .trigger("itemAdded")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Respuestas de sólo cabeceras
|
||||
///
|
||||
/// Cuando la respuesta no lleva cuerpo HTML (por ejemplo, una redirección o un refresco), usa
|
||||
/// [`HtmxResponse::empty()`](Self::empty):
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::HtmxResponse;
|
||||
///
|
||||
/// async fn delete_item() -> impl IntoResponse {
|
||||
/// HtmxResponse::empty().redirect("/items")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Construcción
|
||||
///
|
||||
/// - [`HtmxResponse::new(markup)`](Self::new), con el fragmento HTML.
|
||||
/// - [`HtmxResponse::empty()`](Self::empty), sin cuerpo, sólo cabeceras.
|
||||
///
|
||||
/// # Cabeceras disponibles
|
||||
///
|
||||
/// Los nombres de cabecera como constantes están en [`crate::hx::response`].
|
||||
///
|
||||
/// # Múltiples eventos en `trigger`
|
||||
///
|
||||
/// Para disparar varios eventos en una sola llamada, pasa una cadena con comas o un objeto JSON:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::HtmxResponse;
|
||||
///
|
||||
/// // Dos eventos sin datos:
|
||||
/// HtmxResponse::empty().trigger("itemAdded, listUpdated");
|
||||
///
|
||||
/// // Evento con datos en JSON:
|
||||
/// HtmxResponse::empty().trigger(r#"{"itemAdded": {"id": 42}}"#);
|
||||
/// ```
|
||||
pub struct HtmxResponse {
|
||||
markup: Markup,
|
||||
headers: web::http::HeaderMap,
|
||||
}
|
||||
|
||||
impl HtmxResponse {
|
||||
/// Crea una respuesta con el fragmento HTML indicado.
|
||||
pub fn new(markup: Markup) -> Self {
|
||||
Self {
|
||||
markup,
|
||||
headers: web::http::HeaderMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea una respuesta sin cuerpo HTML, útil para respuestas de sólo cabeceras.
|
||||
pub fn empty() -> Self {
|
||||
Self::new(html! {})
|
||||
}
|
||||
|
||||
// **< HtmxResponse BUILDER >*******************************************************************
|
||||
|
||||
/// 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:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::HtmxResponse;
|
||||
///
|
||||
/// // Navegación simple:
|
||||
/// HtmxResponse::empty().location("/items");
|
||||
///
|
||||
/// // Navegación con destino personalizado:
|
||||
/// HtmxResponse::empty()
|
||||
/// .location(r##"{"path": "/items", "target": "#content"}"##);
|
||||
/// ```
|
||||
pub fn location(self, url: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-location", url)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Provoca una recarga completa de la página actual.
|
||||
///
|
||||
/// Equivale a `window.location.reload()` en JavaScript.
|
||||
pub fn refresh(self) -> Self {
|
||||
self.set_header(b"hx-refresh", "true")
|
||||
}
|
||||
|
||||
/// Anula el `hx-target` del elemento y redirige la respuesta al selector CSS indicado.
|
||||
///
|
||||
/// Útil cuando el servidor necesita actualizar un elemento distinto al que realizó la petición,
|
||||
/// sin modificar el HTML del cliente.
|
||||
pub fn retarget(self, selector: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-retarget", selector)
|
||||
}
|
||||
|
||||
/// Anula el `hx-swap` del elemento e impone la estrategia de sustitución indicada.
|
||||
///
|
||||
/// Acepta los mismos valores que el atributo `hx-swap`, incluidos modificadores (`swap:200ms`,
|
||||
/// `scroll:top`, ...). Los valores tipados están en [`crate::hx::swap`].
|
||||
pub fn reswap(self, strategy: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-reswap", strategy)
|
||||
}
|
||||
|
||||
/// Anula el `hx-select` del elemento y selecciona el fragmento CSS indicado de la respuesta
|
||||
/// para insertarlo en el objetivo.
|
||||
pub fn reselect(self, selector: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-reselect", selector)
|
||||
}
|
||||
|
||||
/// Dispara uno o varios eventos JavaScript en el cliente al completar la respuesta.
|
||||
///
|
||||
/// Los eventos se disparan inmediatamente tras procesar la respuesta. Para disparar eventos con
|
||||
/// datos o después de otras fases del ciclo HTMX, ver
|
||||
/// [`trigger_after_settle()`](Self::trigger_after_settle) y
|
||||
/// [`trigger_after_swap()`](Self::trigger_after_swap).
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_htmx::HtmxResponse;
|
||||
///
|
||||
/// // Evento simple:
|
||||
/// HtmxResponse::empty().trigger("itemAdded");
|
||||
///
|
||||
/// // Múltiples eventos sin datos:
|
||||
/// HtmxResponse::empty().trigger("itemAdded, listUpdated");
|
||||
///
|
||||
/// // Evento con datos en JSON:
|
||||
/// HtmxResponse::empty().trigger(r#"{"itemAdded": {"id": 42, "name": "Example"}}"#);
|
||||
/// ```
|
||||
pub fn trigger(self, event: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-trigger", event)
|
||||
}
|
||||
|
||||
/// Dispara eventos JavaScript después de que HTMX haya aplicado la respuesta al DOM y haya
|
||||
/// completado la fase de *settle* (animaciones CSS).
|
||||
///
|
||||
/// Acepta los mismos formatos que [`trigger()`](Self::trigger).
|
||||
pub fn trigger_after_settle(self, event: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-trigger-after-settle", event)
|
||||
}
|
||||
|
||||
/// Dispara eventos JavaScript después de que HTMX haya aplicado la respuesta al DOM, pero antes
|
||||
/// de la fase de *settle*.
|
||||
///
|
||||
/// Acepta los mismos formatos que [`trigger()`](Self::trigger).
|
||||
pub fn trigger_after_swap(self, event: impl Into<String>) -> Self {
|
||||
self.set_header(b"hx-trigger-after-swap", event)
|
||||
}
|
||||
|
||||
// Inserta o reemplaza una cabecera. Los nombres deben ser bytes ASCII en minúsculas.
|
||||
fn set_header(mut self, name: &[u8], value: impl Into<String>) -> Self {
|
||||
let value = value.into();
|
||||
if let (Ok(n), Ok(v)) = (
|
||||
web::http::HeaderName::from_bytes(name),
|
||||
web::http::HeaderValue::from_str(&value),
|
||||
) {
|
||||
self.headers.insert(n, v);
|
||||
} else {
|
||||
trace::warn!(value = %value, "HtmxResponse: invalid header value, header discarded");
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl web::IntoResponse for HtmxResponse {
|
||||
fn into_response(self) -> Response {
|
||||
let mut headers = self.headers;
|
||||
headers.insert(
|
||||
web::http::header::CONTENT_TYPE,
|
||||
web::http::HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
);
|
||||
(headers, self.markup.into_string()).into_response()
|
||||
}
|
||||
}
|
||||
1
extensions/pagetop-htmx/static/js/htmx.min.js
vendored
Normal file
1
extensions/pagetop-htmx/static/js/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -36,7 +36,7 @@ pub use crate::locale::*;
|
|||
pub use crate::datetime::*;
|
||||
|
||||
pub use crate::web;
|
||||
pub use crate::web::{HttpRequest, Router};
|
||||
pub use crate::web::{HttpRequest, IntoResponse, Response, Router};
|
||||
|
||||
pub use crate::core::{AnyCast, AnyInfo, TypeInfo};
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ case "$CRATE" in
|
|||
# Extensions
|
||||
--exclude-path "extensions/pagetop-aliner/**/*"
|
||||
--exclude-path "extensions/pagetop-bootsier/**/*"
|
||||
--exclude-path "extensions/pagetop-htmx/**/*"
|
||||
--exclude-path "extensions/pagetop-seaorm/**/*"
|
||||
)
|
||||
;;
|
||||
|
|
@ -76,6 +77,10 @@ case "$CRATE" in
|
|||
CHANGELOG_FILE="extensions/pagetop-bootsier/CHANGELOG.md"
|
||||
PATH_FLAGS=(--include-path "extensions/pagetop-bootsier/**/*")
|
||||
;;
|
||||
pagetop-htmx)
|
||||
CHANGELOG_FILE="extensions/pagetop-htmx/CHANGELOG.md"
|
||||
PATH_FLAGS=(--include-path "extensions/pagetop-htmx/**/*")
|
||||
;;
|
||||
pagetop-seaorm)
|
||||
CHANGELOG_FILE="extensions/pagetop-seaorm/CHANGELOG.md"
|
||||
PATH_FLAGS=(--include-path "extensions/pagetop-seaorm/**/*")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue