♻️ (bootsier, htmx): Adapta ciclo de render async

Añade `#[async_trait]` a todos los `impl Extension`, `Theme` y
`Component`. Convierte `prepare()` en `async fn` y añade `.await` a las
llamadas `render()` y `render_offcanvas()`.
This commit is contained in:
Manuel Cillero 2026-07-06 07:55:30 +02:00
parent 9354894b3a
commit 1139df6210
17 changed files with 68 additions and 42 deletions

View file

@ -35,6 +35,7 @@ use pagetop::prelude::*;
struct MyApp; struct MyApp;
#[async_trait]
impl Extension for MyApp { impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![ vec![
@ -69,13 +70,13 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
p { (L10n::l("sample_content").using(cx)) } p { (L10n::l("sample_content").using(cx)) }
})), })),
) )
.render() .render().await
} }
``` ```
*/ */
#![doc( #![doc(
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico"
)] )]
use pagetop::prelude::*; use pagetop::prelude::*;
@ -99,6 +100,7 @@ mod handlers {
/// Implementa el tema. /// Implementa el tema.
pub struct Bootsier; pub struct Bootsier;
#[async_trait]
impl Extension for Bootsier { impl Extension for Bootsier {
fn name(&self) -> L10n { fn name(&self) -> L10n {
L10n::t("extension_name", &LOCALES_BOOTSIER) L10n::t("extension_name", &LOCALES_BOOTSIER)
@ -128,13 +130,14 @@ impl Extension for Bootsier {
} }
} }
#[async_trait]
impl Theme for Bootsier { impl Theme for Bootsier {
#[inline] #[inline]
fn default_template(&self) -> TemplateRef { fn default_template(&self) -> TemplateRef {
&BootsierTemplate::Standard &BootsierTemplate::Standard
} }
fn handle_component( async fn handle_component(
&self, &self,
component: &mut dyn Component, component: &mut dyn Component,
cx: &mut Context, cx: &mut Context,

View file

@ -31,6 +31,7 @@ pub struct Container {
children: Children, children: Children,
} }
#[async_trait]
impl Component for Container { impl Component for Container {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -44,8 +45,8 @@ impl Component for Container {
self.alter_prop(PropsOp::prepend_classes(self.container_width().to_class())); self.alter_prop(PropsOp::prepend_classes(self.container_width().to_class()));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let output = self.children().render(cx); let output = self.children().render(cx).await;
if output.is_empty() { if output.is_empty() {
return Ok(html! {}); return Ok(html! {});
} }

View file

@ -63,6 +63,7 @@ pub struct Dropdown {
items: Children, items: Children,
} }
#[async_trait]
impl Component for Dropdown { impl Component for Dropdown {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -78,9 +79,9 @@ impl Component for Dropdown {
)); ));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// Si no hay elementos en el menú, no se prepara. // Si no hay elementos en el menú, no se prepara.
let items = self.items().render(cx); let items = self.items().render(cx).await;
if items.is_empty() { if items.is_empty() {
return Ok(html! {}); return Ok(html! {});
} }

View file

@ -51,6 +51,7 @@ pub struct Item {
item_kind: ItemKind, item_kind: ItemKind,
} }
#[async_trait]
impl Component for Item { impl Component for Item {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -60,7 +61,7 @@ impl Component for Item {
self.props.get_id() self.props.get_id()
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(match self.item_kind() { Ok(match self.item_kind() {
ItemKind::Void => html! {}, ItemKind::Void => html! {},

View file

@ -112,7 +112,7 @@ impl Direction {
/// Alineación horizontal del menú desplegable [`Dropdown`](crate::theme::bs::Dropdown). /// Alineación horizontal del menú desplegable [`Dropdown`](crate::theme::bs::Dropdown).
/// ///
/// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una /// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una
/// alineación diferente a partir de un punto de ruptura ([`BreakPoint`]). /// alineación diferente a partir de un punto de ruptura ([`BreakPoint`](token::BreakPoint)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum MenuAlign { pub enum MenuAlign {
/// Alineación al inicio (comportamiento por defecto). /// Alineación al inicio (comportamiento por defecto).

View file

@ -21,6 +21,7 @@ pub struct Icon {
aria_label: AttrL10n, aria_label: AttrL10n,
} }
#[async_trait]
impl Component for Icon { impl Component for Icon {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -39,7 +40,7 @@ impl Component for Icon {
} }
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(match self.icon_kind() { Ok(match self.icon_kind() {
IconKind::None => html! {}, IconKind::None => html! {},
IconKind::Font(_) => { IconKind::Font(_) => {

View file

@ -24,6 +24,7 @@ pub struct Image {
alternative: Attr<L10n>, alternative: Attr<L10n>,
} }
#[async_trait]
impl Component for Image { impl Component for Image {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -38,7 +39,7 @@ impl Component for Image {
self.alter_prop(PropsOp::prepend_classes(self.source().to_class())); self.alter_prop(PropsOp::prepend_classes(self.source().to_class()));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let dimensions = self.size().to_style(); let dimensions = self.size().to_style();
let alt_text = self.alternative().lookup(cx).unwrap_or_default(); let alt_text = self.alternative().lookup(cx).unwrap_or_default();
let is_decorative = alt_text.is_empty(); let is_decorative = alt_text.is_empty();
@ -52,7 +53,7 @@ impl Component for Image {
aria-label=[(!is_decorative).then_some(alt_text)] aria-label=[(!is_decorative).then_some(alt_text)]
aria-hidden=[is_decorative.then_some("true")] aria-hidden=[is_decorative.then_some("true")]
{ {
(logo.render(cx)) (logo.markup(cx))
} }
}); });
} }

View file

@ -44,6 +44,7 @@ pub struct Nav {
items: Children, items: Children,
} }
#[async_trait]
impl Component for Nav { impl Component for Nav {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -63,8 +64,8 @@ impl Component for Nav {
})); }));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let items = self.items().render(cx); let items = self.items().render(cx).await;
if items.is_empty() { if items.is_empty() {
return Ok(html! {}); return Ok(html! {});
} }

View file

@ -65,6 +65,7 @@ pub struct Item {
item_kind: ItemKind, item_kind: ItemKind,
} }
#[async_trait]
impl Component for Item { impl Component for Item {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -78,7 +79,7 @@ impl Component for Item {
self.alter_prop(PropsOp::prepend_classes(self.item_kind().as_str())); self.alter_prop(PropsOp::prepend_classes(self.item_kind().as_str()));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(match self.item_kind() { Ok(match self.item_kind() {
ItemKind::Void => html! {}, ItemKind::Void => html! {},
@ -133,13 +134,13 @@ impl Component for Item {
ItemKind::Html(html) => html! { ItemKind::Html(html) => html! {
li (self.props()) { li (self.props()) {
(html.render(cx)) (html.render(cx).await)
} }
}, },
ItemKind::Dropdown(menu) => { ItemKind::Dropdown(menu) => {
if let Some(dd) = menu.get() { if let Some(dd) = menu.get() {
let items = dd.items().render(cx); let items = dd.items().render(cx).await;
if items.is_empty() { if items.is_empty() {
return Ok(html! {}); return Ok(html! {});
} }

View file

@ -25,13 +25,14 @@ pub struct Brand {
route: Option<FnPathByContext>, route: Option<FnPathByContext>,
} }
#[async_trait]
impl Component for Brand { impl Component for Brand {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let image = self.image().render(cx); let image = self.image().render(cx).await;
let title = self.title().using(cx); let title = self.title().using(cx);
if title.is_empty() && image.is_empty() { if title.is_empty() && image.is_empty() {
return Ok(html! {}); return Ok(html! {});

View file

@ -149,6 +149,7 @@ pub struct Navbar {
items: Children, items: Children,
} }
#[async_trait]
impl Component for Navbar { impl Component for Navbar {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -171,7 +172,7 @@ impl Component for Navbar {
})); }));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// Botón de despliegue (colapso u offcanvas) para la barra. // Botón de despliegue (colapso u offcanvas) para la barra.
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup { fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
let id_content_target = util::join!("#", id_content); let id_content_target = util::join!("#", id_content);
@ -196,7 +197,7 @@ impl Component for Navbar {
} }
// Si no hay contenidos, no tiene sentido mostrar una barra vacía. // Si no hay contenidos, no tiene sentido mostrar una barra vacía.
let items = self.items().render(cx); let items = self.items().render(cx).await;
if items.is_empty() { if items.is_empty() {
return Ok(html! {}); return Ok(html! {});
} }
@ -225,7 +226,7 @@ impl Component for Navbar {
// Barra con marca a la izquierda, siempre visible. // Barra con marca a la izquierda, siempre visible.
bs::navbar::Layout::SimpleBrandLeft(brand) => { bs::navbar::Layout::SimpleBrandLeft(brand) => {
(brand.render(cx)) (brand.render(cx).await)
(items) (items)
}, },
@ -233,7 +234,7 @@ impl Component for Navbar {
bs::navbar::Layout::BrandLeft(brand) => { bs::navbar::Layout::BrandLeft(brand) => {
@let id_content = util::join!(id, "-content"); @let id_content = util::join!(id, "-content");
(brand.render(cx)) (brand.render(cx).await)
(button(cx, TOGGLE_COLLAPSE, &id_content)) (button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(&id_content) class="collapse navbar-collapse" { div id=(&id_content) class="collapse navbar-collapse" {
(items) (items)
@ -245,7 +246,7 @@ impl Component for Navbar {
@let id_content = util::join!(id, "-content"); @let id_content = util::join!(id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content)) (button(cx, TOGGLE_COLLAPSE, &id_content))
(brand.render(cx)) (brand.render(cx).await)
div id=(&id_content) class="collapse navbar-collapse" { div id=(&id_content) class="collapse navbar-collapse" {
(items) (items)
} }
@ -257,7 +258,7 @@ impl Component for Navbar {
(button(cx, TOGGLE_OFFCANVAS, &id_content)) (button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.get() { @if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(self.items()))) (oc.render_offcanvas(cx, Some(self.items())).await)
} }
}, },
@ -265,10 +266,10 @@ impl Component for Navbar {
bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => { bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default(); @let id_content = offcanvas.id().unwrap_or_default();
(brand.render(cx)) (brand.render(cx).await)
(button(cx, TOGGLE_OFFCANVAS, &id_content)) (button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.get() { @if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(self.items()))) (oc.render_offcanvas(cx, Some(self.items())).await)
} }
}, },
@ -277,9 +278,9 @@ impl Component for Navbar {
@let id_content = offcanvas.id().unwrap_or_default(); @let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content)) (button(cx, TOGGLE_OFFCANVAS, &id_content))
(brand.render(cx)) (brand.render(cx).await)
@if let Some(oc) = offcanvas.get() { @if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(self.items()))) (oc.render_offcanvas(cx, Some(self.items())).await)
} }
}, },
} }

View file

@ -26,6 +26,7 @@ pub enum Item {
Text(L10n), Text(L10n),
} }
#[async_trait]
impl Component for Item { impl Component for Item {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -42,19 +43,19 @@ impl Component for Item {
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
if let Self::Nav(nav) = self { if let Self::Nav(nav) = self {
if let Some(mut nav) = nav.get() { if let Some(nav) = nav.get_mut() {
nav.alter_prop(PropsOp::prepend_classes("navbar-nav")); nav.alter_prop(PropsOp::prepend_classes("navbar-nav"));
} }
} }
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(match self { Ok(match self {
Self::Void => html! {}, Self::Void => html! {},
Self::Brand(brand) => html! { (brand.render(cx)) }, Self::Brand(brand) => html! { (brand.render(cx).await) },
Self::Nav(nav) => { Self::Nav(nav) => {
if let Some(nav) = nav.get() { if let Some(nav) = nav.get() {
let items = nav.items().render(cx); let items = nav.items().render(cx).await;
if items.is_empty() { if items.is_empty() {
return Ok(html! {}); return Ok(html! {});
} }

View file

@ -61,6 +61,7 @@ pub struct Offcanvas {
children: Children, children: Children,
} }
#[async_trait]
impl Component for Offcanvas { impl Component for Offcanvas {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -84,8 +85,8 @@ impl Component for Offcanvas {
})); }));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(self.render_offcanvas(cx, None)) Ok(self.render_offcanvas(cx, None).await)
} }
} }
@ -167,9 +168,17 @@ impl Offcanvas {
// **< Offcanvas HELPERS >********************************************************************** // **< Offcanvas HELPERS >**********************************************************************
pub(crate) fn render_offcanvas(&self, cx: &mut Context, extra: Option<&Children>) -> Markup { pub(crate) async fn render_offcanvas(
let body = self.children().render(cx); &self,
let body_extra = extra.map(|c| c.render(cx)).unwrap_or_else(|| html! {}); cx: &mut Context,
extra: Option<&Children>,
) -> Markup {
let body = self.children().render(cx).await;
let body_extra = if let Some(c) = extra {
c.render(cx).await
} else {
html! {}
};
if body.is_empty() && body_extra.is_empty() { if body.is_empty() && body_extra.is_empty() {
return html! {}; return html! {};
} }

View file

@ -34,6 +34,7 @@ use pagetop::prelude::*;
struct MyApp; struct MyApp;
#[async_trait]
impl Extension for MyApp { impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![ vec![
@ -59,7 +60,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
} }
div #result {} div #result {}
})) }))
.render() .render().await
} }
``` ```

View file

@ -332,7 +332,7 @@ pub const DISABLE: &str = "hx-disable";
/// Genera `hx-on:{event}` para escuchar eventos nativos del DOM en línea. /// 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 /// Es la alternativa de HTMX a los *handlers* `on*` de HTML (`onclick`, `onmouseenter`, ...). La
/// diferencia clave está en cómo los trata el navegador bajo una política CSP (*Content Security /// 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 /// 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 /// `'unsafe-inline'`; en cambio, `hx-on:*` es un atributo de datos que HTMX lee e interpreta desde

View file

@ -35,6 +35,7 @@ use pagetop::prelude::*;
struct MyApp; struct MyApp;
#[async_trait]
impl Extension for MyApp { impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![ vec![
@ -60,7 +61,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
} }
div #result {} div #result {}
})) }))
.render() .render().await
} }
``` ```
*/ */
@ -91,6 +92,7 @@ include_locales!(LOCALES_HTMX);
/// ///
/// struct MyApp; /// struct MyApp;
/// ///
/// #[async_trait]
/// impl Extension for MyApp { /// impl Extension for MyApp {
/// fn dependencies(&self) -> Vec<ExtensionRef> { /// fn dependencies(&self) -> Vec<ExtensionRef> {
/// vec![ /// vec![
@ -103,6 +105,7 @@ include_locales!(LOCALES_HTMX);
/// ``` /// ```
pub struct Htmx; pub struct Htmx;
#[async_trait]
impl Extension for Htmx { impl Extension for Htmx {
fn name(&self) -> L10n { fn name(&self) -> L10n {
L10n::t("extension_name", &LOCALES_HTMX) L10n::t("extension_name", &LOCALES_HTMX)

View file

@ -29,7 +29,7 @@ use pagetop::prelude::*;
/// .with_child(Html::with(|_| html! { /// .with_child(Html::with(|_| html! {
/// ul { li { "Item 1" } li { "Item 2" } } /// ul { li { "Item 1" } li { "Item 2" } }
/// })) /// }))
/// .render() /// .render().await
/// .into_response() /// .into_response()
/// } /// }
/// } /// }