🎨 Convierte el ciclo de renderizado en async

`prepare()` de `Component`, `render()` de `Region` y `Template`, y la
implementación de `ComponentRender` pasan a ser funciones async. Se
actualizan todos los componentes base, helpers, tests y ejemplos.
This commit is contained in:
Manuel Cillero 2026-07-06 00:48:41 +02:00
parent 9acd8cc51a
commit 9354894b3a
48 changed files with 436 additions and 365 deletions

View file

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

View file

@ -81,6 +81,7 @@ pub struct Button {
disabled: bool,
}
#[async_trait]
impl Component for Button {
fn new() -> Self {
Self::default()
@ -94,7 +95,7 @@ impl Component for Button {
self.alter_prop(PropsOp::prepend_classes("button"));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! {
button
type=(self.kind())

View file

@ -121,6 +121,7 @@ pub struct Field {
inline: bool,
}
#[async_trait]
impl Component for Field {
fn new() -> Self {
Self::default()
@ -145,7 +146,7 @@ impl Component for Field {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes"));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
let name = self.name().get().unwrap();
let container_id = self.id().unwrap();

View file

@ -61,6 +61,7 @@ pub struct Checkbox {
reverse: bool,
}
#[async_trait]
impl Component for Checkbox {
fn new() -> Self {
Self::default()
@ -95,7 +96,7 @@ impl Component for Checkbox {
self.alter_prop(PropsOp::prepend_classes(classes));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
let name = self.name().get().unwrap();
let container_id = self.id().unwrap();

View file

@ -57,6 +57,7 @@ pub struct Form {
children: Children,
}
#[async_trait]
impl Component for Form {
fn new() -> Self {
Self::default()
@ -70,7 +71,7 @@ impl Component for Form {
self.alter_prop(PropsOp::prepend_classes("form"));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let method = match self.method() {
form::Method::Post => Some("post"),
form::Method::Get => None,
@ -82,7 +83,7 @@ impl Component for Form {
method=[method]
accept-charset=[self.charset().get()]
{
(self.children().render(cx))
(self.children().render(cx).await)
}
})
}

View file

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

View file

@ -35,12 +35,13 @@ pub struct Hidden {
value: AttrValue,
}
#[async_trait]
impl Component for Hidden {
fn new() -> Self {
Self::default()
}
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! {
input
type="hidden"

View file

@ -157,6 +157,7 @@ pub struct Field {
inputmode: Attr<Mode>,
}
#[async_trait]
impl Component for Field {
fn new() -> Self {
Self::default()
@ -181,7 +182,7 @@ impl Component for Field {
)));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id();
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
let input_class = if *self.plaintext() {

View file

@ -112,6 +112,7 @@ pub struct Field {
inline: bool,
}
#[async_trait]
impl Component for Field {
fn new() -> Self {
Self::default()
@ -136,7 +137,7 @@ impl Component for Field {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios"));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
let name = self.name().get().unwrap();
let container_id = self.id().unwrap();

View file

@ -53,6 +53,7 @@ pub struct Range {
disabled: bool,
}
#[async_trait]
impl Component for Range {
fn new() -> Self {
Self::default()
@ -74,7 +75,7 @@ impl Component for Range {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-range"));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id();
let range_id = container_id.as_deref().map(|id| util::join!(id, "-range"));
Ok(html! {

View file

@ -212,6 +212,7 @@ pub struct Field {
disabled: bool,
}
#[async_trait]
impl Component for Field {
fn new() -> Self {
Self::default()
@ -233,7 +234,7 @@ impl Component for Field {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-select"));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id();
let select_id = container_id.as_deref().map(|id| util::join!(id, "-select"));

View file

@ -63,6 +63,7 @@ pub struct Textarea {
disabled: bool,
}
#[async_trait]
impl Component for Textarea {
fn new() -> Self {
Self::default()
@ -84,7 +85,7 @@ impl Component for Textarea {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-textarea"));
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id();
let textarea_id = container_id
.as_deref()

View file

@ -49,12 +49,13 @@ impl Default for Html {
}
}
#[async_trait]
impl Component for Html {
fn new() -> Self {
Self::default()
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(self.html(cx))
}
}

View file

@ -107,12 +107,13 @@ impl Default for Intro {
}
}
#[async_trait]
impl Component for Intro {
fn new() -> Self {
Self::default()
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
cx.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION),
));
@ -146,7 +147,7 @@ impl Component for Intro {
}
aside class="intro-header-img" aria-hidden="true" {
div class="intro-header-mascot" {
(PageTopSvg::Color.render(cx))
(PageTopSvg::Color.markup(cx))
}
}
}
@ -191,7 +192,7 @@ impl Component for Intro {
(L10n::l("intro_text2").using(cx))
}
}
(self.children().render(cx))
(self.children().render(cx).await)
}
}
}
@ -199,7 +200,7 @@ impl Component for Intro {
div class="intro-footer" {
section class="intro-footer-body" {
div class="intro-footer-logo" {
(PageTopSvg::LineLight.render(cx))
(PageTopSvg::LineLight.markup(cx))
}
div class="intro-footer-links" {
a href="https://crates.io/crates/pagetop" target="_blank" rel="noopener noreferrer" { ("Crates.io") }

View file

@ -14,6 +14,7 @@ pub struct PoweredBy {
copyright: Option<String>,
}
#[async_trait]
impl Component for PoweredBy {
/// Crea una nueva instancia de `PoweredBy`.
///
@ -25,7 +26,7 @@ impl Component for PoweredBy {
PoweredBy { copyright: Some(c) }
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! {
div id=[self.id()] class="poweredby" {
@if let Some(c) = self.copyright() {

View file

@ -11,6 +11,7 @@ use crate::prelude::*;
/// Resulta útil en demos o para comprobar rápidamente que el servidor ha arrancado correctamente.
pub struct Welcome;
#[async_trait]
impl Extension for Welcome {
fn name(&self) -> L10n {
L10n::l("welcome_extension_name")
@ -62,4 +63,5 @@ async fn home(request: HttpRequest) -> Result<Markup, ErrorPage> {
),
)
.render()
.await
}

View file

@ -4,12 +4,14 @@ use crate::prelude::*;
/// Tema básico por defecto que extiende el funcionamiento predeterminado de [`Theme`].
pub struct Basic;
#[async_trait]
impl Extension for Basic {
fn theme(&self) -> Option<ThemeRef> {
Some(&Self)
}
}
#[async_trait]
impl Theme for Basic {
fn before_render_page_body(&self, page: &mut Page) {
page.alter_assets(AssetsOp::AddStyleSheet(