Compare commits

..

2 commits

Author SHA1 Message Date
28f1eee391 ♻️ (pagetop): Separa handle_component() en 2 métodos
`Theme::handle_component()` se divide en `setup_component()` (síncrono,
muta por tipo con la macro homónima) y `render_component()` (async,
decide el renderizado con su propia macro). Aplica mismo criterio que ya
separa `Component::setup()` de `prepare()`.
2026-09-06 13:07:01 +02:00
1e2d171805 ♻️ (pagetop): setup() recibe &mut Context
Antes sólo mutaba el propio componente; ahora también puede aplicar
ajustes síncronos sobre el Context, visibles para las extensiones que
intercepten BeforeRender. La frontera con prepare() pasa a ser de
sincronía, no de qué puede mutar cada uno.
2026-09-06 09:04:44 +02:00
45 changed files with 253 additions and 226 deletions

View file

@ -7,7 +7,7 @@ use crate::registry;
/// Construye el [`Nav`] en cada petición a partir de [`registry::admin_menu()`], ya filtrado por /// Construye el [`Nav`] en cada petición a partir de [`registry::admin_menu()`], ya filtrado por
/// el usuario de la petición actual. Pensado para que un tema lo registre en su propia región de /// el usuario de la petición actual. Pensado para que un tema lo registre en su propia región de
/// navegación (p. ej. `pagetop-bootsier` lo añade a su sidebar) e intercepte `Nav`/`nav::Item` en /// navegación (p. ej. `pagetop-bootsier` lo añade a su sidebar) e intercepte `Nav`/`nav::Item` en
/// [`Theme::handle_component()`](pagetop::core::theme::Theme::handle_component) si quiere darle un /// [`Theme::render_component()`](pagetop::core::theme::Theme::render_component) si quiere darle un
/// aspecto propio; sin intercepción, se renderiza con el marcado por defecto de [`Nav`]. /// aspecto propio; sin intercepción, se renderiza con el marcado por defecto de [`Nav`].
/// ///
/// Sólo se renderiza en páginas creadas con /// Sólo se renderiza en páginas creadas con

View file

@ -276,7 +276,7 @@ pub fn global() -> &'static AdminRegistry {
/// Pensado para que un tema lo use como navegación de `CoreTemplates::Admin` (p. ej. un sidebar) -- /// Pensado para que un tema lo use como navegación de `CoreTemplates::Admin` (p. ej. un sidebar) --
/// ver [`crate::component::AdminMenu`]. `pagetop-admin` no impone ningún marcado propio: el /// ver [`crate::component::AdminMenu`]. `pagetop-admin` no impone ningún marcado propio: el
/// [`Nav`] resultante se renderiza con su aspecto por defecto salvo que el tema lo intercepte en /// [`Nav`] resultante se renderiza con su aspecto por defecto salvo que el tema lo intercepte en
/// [`Theme::handle_component()`](pagetop::core::theme::Theme::handle_component). /// [`Theme::render_component()`](pagetop::core::theme::Theme::render_component).
pub fn admin_menu(cx: &Context) -> Nav { pub fn admin_menu(cx: &Context) -> Nav {
let reg = global(); let reg = global();
let current_path = cx.request().map(|r| r.path()).unwrap_or(""); let current_path = cx.request().map(|r| r.path()).unwrap_or("");

View file

@ -129,11 +129,7 @@ impl Theme for Bootsier {
theme::BootsierColors::from(intent).as_str() theme::BootsierColors::from(intent).as_str()
} }
async fn handle_component( fn setup_component(&self, component: &mut dyn Component, _cx: &mut Context) {
&self,
component: &mut dyn Component,
cx: &mut Context,
) -> Option<Result<Markup, ComponentError>> {
setup_component!(component, { setup_component!(component, {
Badge => |c| theme::bs::badge::setup(c), Badge => |c| theme::bs::badge::setup(c),
Brand => |c| theme::bs::brand::setup(c), Brand => |c| theme::bs::brand::setup(c),
@ -148,7 +144,13 @@ impl Theme for Bootsier {
form::select::Field => |c| theme::bs::form::select::setup(c), form::select::Field => |c| theme::bs::form::select::setup(c),
form::Textarea => |c| theme::bs::form::textarea::setup(c), form::Textarea => |c| theme::bs::form::textarea::setup(c),
}); });
}
async fn render_component(
&self,
component: &dyn Component,
cx: &mut Context,
) -> Option<Result<Markup, ComponentError>> {
render_component!(component, { render_component!(component, {
layout::Region => |c| theme::bs::layout::region::render(c, cx).await?, layout::Region => |c| theme::bs::layout::region::render(c, cx).await?,
layout::Template => |c| theme::bs::layout::template::render(c, cx).await?, layout::Template => |c| theme::bs::layout::template::render(c, cx).await?,

View file

@ -17,7 +17,7 @@
//! la shell completa de AdminLTE 4 (barra superior + barra lateral + área de contenido), que se //! la shell completa de AdminLTE 4 (barra superior + barra lateral + área de contenido), que se
//! activa creando la página con [`Page::admin()`](pagetop::response::Page::admin) en lugar de //! activa creando la página con [`Page::admin()`](pagetop::response::Page::admin) en lugar de
//! [`Page::new()`](pagetop::response::Page::new). No define sus propias variantes de plantilla: //! [`Page::new()`](pagetop::response::Page::new). No define sus propias variantes de plantilla:
//! intercepta el componente `Template` en `handle_component()` (ver `bs::layout`). //! intercepta el componente `Template` en `render_component()` (ver `bs::layout`).
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use pagetop::prelude::*; //! use pagetop::prelude::*;

View file

@ -31,7 +31,7 @@ impl Component for Icon {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
if !matches!(self.icon_kind(), IconKind::None) { if !matches!(self.icon_kind(), IconKind::None) {
self.alter_prop(PropsOp::prepend_classes("icon")); self.alter_prop(PropsOp::prepend_classes("icon"));
} }

View file

@ -9,7 +9,7 @@ pub enum BootsierRegions {
/// Los componentes registrados aquí se renderizan directamente dentro del /// Los componentes registrados aquí se renderizan directamente dentro del
/// `<ul class="sidebar-menu">`, sin el `<div>` envolvente que añade /// `<ul class="sidebar-menu">`, sin el `<div>` envolvente que añade
/// [`Region`](pagetop::base::component::layout::Region) por defecto -- /// [`Region`](pagetop::base::component::layout::Region) por defecto --
/// [`Bootsier`](crate::Bootsier) intercepta este componente en `handle_component()` para /// [`Bootsier`](crate::Bootsier) intercepta este componente en `render_component()` para
/// renderizarlo así. Los elementos esperados son /// renderizarlo así. Los elementos esperados son
/// [`bs::sidebar::Item`](crate::theme::bs::sidebar::Item) y /// [`bs::sidebar::Item`](crate::theme::bs::sidebar::Item) y
/// [`bs::sidebar::Section`](crate::theme::bs::sidebar::Section). /// [`bs::sidebar::Section`](crate::theme::bs::sidebar::Section).

View file

@ -71,7 +71,7 @@ impl Component for Offcanvas {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
// Asegura que el panel tiene un identificador único. // Asegura que el panel tiene un identificador único.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1))); self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));

View file

@ -17,7 +17,7 @@ use crate::tree::{MenuKey, MenuNode, TreeOptions, build_tree, try_resolve_menu_u
/// </nav> /// </nav>
/// ``` /// ```
/// ///
/// Los temas pueden sobreescribir el render con `handle_component()`, tanto de `MenuBlock` como, /// Los temas pueden sobreescribir el render con `render_component()`, tanto de `MenuBlock` como,
/// más generalmente, de [`Nav`]/[`nav::Item`]/[`Dropdown`]/[`dropdown::Item`]. `pagetop-bootsier` /// más generalmente, de [`Nav`]/[`nav::Item`]/[`Dropdown`]/[`dropdown::Item`]. `pagetop-bootsier`
/// ya intercepta `Dropdown` así (ver `theme::bs::dropdown`), y por tanto también los que cuelguen /// ya intercepta `Dropdown` así (ver `theme::bs::dropdown`), y por tanto también los que cuelguen
/// de un `nav::Item::dropdown()`; `MenuBlock`, `Nav` y `Navbar` siguen sin interceptarse: Bootsier /// de un `nav::Item::dropdown()`; `MenuBlock`, `Nav` y `Navbar` siguen sin interceptarse: Bootsier

View file

@ -33,7 +33,7 @@ impl Component for RoleTable {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::set_id("role-table-wrapper")); self.alter_prop(PropsOp::set_id("role-table-wrapper"));
self.alter_prop(PropsOp::prepend_classes("user-admin-table-wrapper")); self.alter_prop(PropsOp::prepend_classes("user-admin-table-wrapper"));
} }

View file

@ -34,7 +34,7 @@ impl Component for UserTable {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::set_id("user-table-wrapper")); self.alter_prop(PropsOp::set_id("user-table-wrapper"));
self.alter_prop(PropsOp::prepend_classes("user-admin-table-wrapper")); self.alter_prop(PropsOp::prepend_classes("user-admin-table-wrapper"));
} }

View file

@ -33,7 +33,7 @@ impl Component for Badge {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes(util::join!( self.alter_prop(PropsOp::prepend_classes(util::join!(
"badge badge-", "badge badge-",
self.intent().color(cx) self.intent().color(cx)

View file

@ -24,7 +24,7 @@ impl Component for Block {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
// Asegura que el bloque tiene un identificador único. // Asegura que el bloque tiene un identificador único.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1))); self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));

View file

@ -48,7 +48,7 @@ impl Component for Brand {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes("brand")); self.alter_prop(PropsOp::prepend_classes("brand"));
} }

View file

@ -44,7 +44,7 @@ impl Component for Breadcrumb {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
for crumb in self.crumbs.iter_mut() { for crumb in self.crumbs.iter_mut() {
crumb.setup(cx); crumb.setup(cx);
} }

View file

@ -79,7 +79,7 @@ impl Crumb {
} }
// Normaliza la clase base según el papel del elemento. Sólo lo usa `Breadcrumb`. // Normaliza la clase base según el papel del elemento. Sólo lo usa `Breadcrumb`.
pub(super) fn setup(&mut self, _cx: &Context) { pub(super) fn setup(&mut self, _cx: &mut Context) {
if *self.is_current() { if *self.is_current() {
self.alter_prop(PropsOp::prepend_classes("active")) self.alter_prop(PropsOp::prepend_classes("active"))
.alter_prop(PropsOp::set("aria-current", "page")); .alter_prop(PropsOp::set("aria-current", "page"));

View file

@ -78,7 +78,7 @@ impl Component for Button {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
use button::{Size, Style}; use button::{Size, Style};
self.alter_prop(PropsOp::prepend_classes(match self.size() { self.alter_prop(PropsOp::prepend_classes(match self.size() {

View file

@ -64,7 +64,7 @@ impl Component for Container {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
if let Some(flex) = self.flex() { if let Some(flex) = self.flex() {
flex.apply_to(&mut self.props); flex.apply_to(&mut self.props);
} }

View file

@ -62,7 +62,7 @@ impl Component for Dialog {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
// Asegura que el diálogo tiene un identificador único con el que abrirlo. // Asegura que el diálogo tiene un identificador único con el que abrirlo.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1))); self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));
self.alter_prop(PropsOp::prepend_classes("dialog")); self.alter_prop(PropsOp::prepend_classes("dialog"));

View file

@ -51,7 +51,7 @@ impl Component for Dropdown {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes("dropdown")); self.alter_prop(PropsOp::prepend_classes("dropdown"));
} }

View file

@ -123,7 +123,7 @@ impl Component for Field {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
// Asegura `name` e `id`. // Asegura `name` e `id`.
// Si falta uno se deriva del otro; si faltan ambos se genera un valor único. // Si falta uno se deriva del otro; si faltan ambos se genera un valor único.
let name = self let name = self

View file

@ -71,7 +71,7 @@ impl Component for Checkbox {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
// Asegura `name` e `id`. // Asegura `name` e `id`.
// Si falta uno se deriva del otro; si faltan ambos se genera un valor único. // Si falta uno se deriva del otro; si faltan ambos se genera un valor único.
let name = self let name = self

View file

@ -67,7 +67,7 @@ impl Component for Form {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes("form")); self.alter_prop(PropsOp::prepend_classes("form"));
} }

View file

@ -199,7 +199,7 @@ impl Component for Field {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))

View file

@ -70,7 +70,7 @@ impl Component for Number {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))

View file

@ -123,7 +123,7 @@ impl Component for Field {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
// Asegura `name` e `id`. // Asegura `name` e `id`.
// Si falta uno se deriva del otro; si faltan ambos se genera un valor único. // Si falta uno se deriva del otro; si faltan ambos se genera un valor único.
let name = self let name = self

View file

@ -67,7 +67,7 @@ impl Component for Range {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))

View file

@ -225,7 +225,7 @@ impl Component for Field {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))

View file

@ -76,7 +76,7 @@ impl Component for Textarea {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))

View file

@ -42,7 +42,7 @@ impl Component for Image {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes(match self.source() { self.alter_prop(PropsOp::prepend_classes(match self.source() {
image::Source::Logo(_) => "image image-fluid", image::Source::Logo(_) => "image image-fluid",
image::Source::Responsive(_) => "image image-fluid", image::Source::Responsive(_) => "image image-fluid",

View file

@ -9,8 +9,8 @@ use std::fmt;
/// se renderiza nada. /// se renderiza nada.
/// ///
/// Si un tema necesita maquetar una región determinada de forma distinta, puede capturar este /// Si un tema necesita maquetar una región determinada de forma distinta, puede capturar este
/// componente en [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) y hacer /// componente en [`Theme::render_component()`](crate::core::theme::Theme::render_component) y
/// [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`RegionRef`] que devuelve /// hacer [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`RegionRef`] que devuelve
/// [`Self::region()`], para compararlo con la variante deseada. /// [`Self::region()`], para compararlo con la variante deseada.
/// ///
/// Como cualquier otro componente, participa también en el despacho de las /// Como cualquier otro componente, participa también en el despacho de las

View file

@ -16,7 +16,7 @@ use std::fmt;
/// independientemente de la plantilla que se use. /// independientemente de la plantilla que se use.
/// ///
/// Si un tema necesita maquetar una plantilla determinada de forma distinta, puede capturar este /// Si un tema necesita maquetar una plantilla determinada de forma distinta, puede capturar este
/// componente en [`Theme::handle_component()`] y hacer [`downcast_ref()`] sobre el [`TemplateRef`] /// componente en [`Theme::render_component()`] y hacer [`downcast_ref()`] sobre el [`TemplateRef`]
/// que devuelve [`Self::template()`], para compararlo con la variante deseada. /// que devuelve [`Self::template()`], para compararlo con la variante deseada.
/// ///
/// Como cualquier otro componente, participa también en el despacho de las /// Como cualquier otro componente, participa también en el despacho de las
@ -28,7 +28,7 @@ use std::fmt;
/// [`ReservedRegions::PageBottom`]: crate::response::ReservedRegions::PageBottom /// [`ReservedRegions::PageBottom`]: crate::response::ReservedRegions::PageBottom
/// [`Page::render()`]: crate::response::Page::render /// [`Page::render()`]: crate::response::Page::render
/// [`Theme::render_page_body()`]: crate::core::theme::Theme::render_page_body /// [`Theme::render_page_body()`]: crate::core::theme::Theme::render_page_body
/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component /// [`Theme::render_component()`]: crate::core::theme::Theme::render_component
/// [`downcast_ref()`]: crate::core::AnyCast::downcast_ref /// [`downcast_ref()`]: crate::core::AnyCast::downcast_ref
#[derive(Clone, Getters)] #[derive(Clone, Getters)]
pub struct Template { pub struct Template {

View file

@ -45,7 +45,7 @@ impl Component for Messages {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes("messages")); self.alter_prop(PropsOp::prepend_classes("messages"));
} }

View file

@ -43,7 +43,7 @@ impl Component for Nav {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes(match self.nav_layout() { self.alter_prop(PropsOp::prepend_classes(match self.nav_layout() {
nav::Layout::Default => "nav", nav::Layout::Default => "nav",
nav::Layout::Start => "nav nav-start", nav::Layout::Start => "nav nav-start",

View file

@ -77,7 +77,7 @@ impl Component for Item {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes(self.item_kind().as_str())); self.alter_prop(PropsOp::prepend_classes(self.item_kind().as_str()));
} }

View file

@ -98,7 +98,7 @@ impl Component for Navbar {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
// Asegura que la barra de navegación tiene un identificador único: lo necesita el botón de // Asegura que la barra de navegación tiene un identificador único: lo necesita el botón de
// despliegue para referenciar el contenido colapsable con `aria-controls`. // despliegue para referenciar el contenido colapsable con `aria-controls`.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1))); self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));

View file

@ -37,7 +37,7 @@ impl Component for Item {
} }
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
if let Self::Nav(nav) = self if let Self::Nav(nav) = self
&& let Some(nav) = nav.get_mut() && let Some(nav) = nav.get_mut()
{ {

View file

@ -184,7 +184,7 @@ impl Component for Pager {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, cx: &mut Context) {
// Asegura un `id` propio si no está definido. El formulario de salto a página deriva sus // Asegura un `id` propio si no está definido. El formulario de salto a página deriva sus
// identificadores de éste para no colisionar si hay varios paginadores en la misma página. // identificadores de éste para no colisionar si hay varios paginadores en la misma página.
let id = cx.required_id::<Self>(self.id(), 1); let id = cx.required_id::<Self>(self.id(), 1);

View file

@ -72,7 +72,7 @@ impl Component for Table {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes("table")); self.alter_prop(PropsOp::prepend_classes("table"));
} }

View file

@ -89,38 +89,45 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// Configura el estado interno del componente antes de generar el marcado. /// Configura el estado interno del componente antes de generar el marcado.
/// ///
/// Segundo paso del [ciclo de renderizado](ComponentRender): se ejecuta tras comprobar /// Segundo paso del [ciclo de renderizado](ComponentRender). Se ejecuta tras comprobar
/// [`is_renderable()`](Self::is_renderable) y antes de la acción /// [`is_renderable()`] y antes de la acción [`BeforeRender`] y de [`prepare()`]. Por defecto no
/// [`BeforeRender`](crate::base::action::component::BeforeRender) y de /// hace nada.
/// [`prepare()`](Self::prepare). Recibe sólo una referencia compartida al contexto porque su
/// propósito es mutar el propio componente, no el contexto. Por defecto no hace nada.
/// ///
/// Está pensado para **normalizar el estado interno** del componente antes de renderizarlo. Por /// Está pensado para **normalizar el estado interno** del componente antes de renderizarlo. Por
/// ejemplo, calcular clases CSS, ajustar valores de campos, derivar atributos a partir del /// ejemplo, calcular clases CSS, ajustar valores de campos, derivar atributos a partir del
/// contexto, etc. Se desaconseja utilizar para operaciones de E/S o consultas a base de datos; /// contexto, etc. Recibe `&mut Context` donde también puede aplicar ajustes que dependan del
/// es intencionadamente síncrono. /// propio componente (p. ej. estilos o *assets*); cualquier extensión que intercepte
/// [`BeforeRender`] verá ya aplicados esos cambios, porque `setup()` se ejecuta antes. Se
/// desaconseja utilizar para operaciones de E/S o consultas a base de datos; es
/// intencionadamente síncrono.
/// ///
/// La carga y el acceso a datos en general corresponden a [`prepare()`](Self::prepare), que es /// La frontera con [`prepare()`] es de **sincronía**, no de qué puede modificar cada uno. Los
/// `async` precisamente para ello. /// dos reciben `&mut Context` (ambos pueden realizar ajustes en él), pero sólo `setup()` recibe
/// además `&mut self` (`prepare()` sólo recibe `&self`, así que no puede normalizar el propio
/// componente). La carga y el acceso a datos siguen correspondiendo a `prepare()`, que es
/// `async` precisamente para permitir E/S; `setup()` se mantiene síncrono a propósito. Esta
/// separación es deliberada y no debe fusionarse.
/// ///
/// La separación entre `setup()` (mutación de estado) y [`prepare()`](Self::prepare) /// [`is_renderable()`]: Self::is_renderable
/// (generación de HTML) es deliberada y no debe fusionarse. /// [`prepare()`]: Self::prepare
/// [`BeforeRender`]: crate::base::action::component::BeforeRender
#[allow(unused_variables)] #[allow(unused_variables)]
fn setup(&mut self, cx: &Context) {} fn setup(&mut self, cx: &mut Context) {}
/// Genera el marcado HTML del componente cuando ningún tema lo sobrescribe. /// Genera el marcado HTML del componente cuando ningún tema lo sobrescribe.
/// ///
/// Este es el cuarto paso del [ciclo de renderizado](ComponentRender) tras llamar al /// Este es el cuarto paso del [ciclo de renderizado](ComponentRender) tras llamar al
/// [`setup()`] del componente y despachar la acción [`BeforeRender`] que atiende los cambios de /// [`setup()`] del componente y despachar la acción [`BeforeRender`] que atiende los cambios de
/// otras extensiones antes de renderizar. Se invoca sólo si ningún tema en la cadena devuelve /// otras extensiones antes de renderizar. Se invoca sólo si ningún tema en la cadena devuelve
/// `Some` en [`Theme::handle_component()`] para este componente. /// `Some` en [`Theme::render_component()`] para este componente.
/// ///
/// Es `async`, a diferencia de [`setup()`], para permitir a los componentes realizar aquí sus /// Es `async`, a diferencia de [`setup()`], para permitir a los componentes realizar aquí sus
/// llamadas asíncronas, como consultas a base de datos, peticiones a servicios externos, o /// llamadas asíncronas, como consultas a base de datos, peticiones a servicios externos, o
/// cualquier operación de E/S, que necesiten para preparar su contenido. /// cualquier operación de E/S, que necesiten para preparar su contenido.
/// ///
/// Se recomienda obtener los datos del componente a través de sus propios métodos para que los /// Se recomienda obtener los datos del componente a través de sus propios métodos para que los
/// temas puedan implementar `handle_component()` sin depender de los detalles internos. /// temas puedan implementar [`Theme::setup_component()`]/[`Theme::render_component()`] sin
/// depender de los detalles internos.
/// ///
/// Los campos que representen contenido no deben almacenar [`Markup`] ya generado, sino que /// Los campos que representen contenido no deben almacenar [`Markup`] ya generado, sino que
/// guardarán el dato en bruto, por ejemplo [`Lc`] para textos traducibles, o un componente /// guardarán el dato en bruto, por ejemplo [`Lc`] para textos traducibles, o un componente
@ -132,12 +139,13 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*). /// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*).
/// ///
/// [`setup()`]: Self::setup /// [`setup()`]: Self::setup
/// [`BeforeRender`]: crate::base::action::component::BeforeRender
/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component
/// [`Lc`]: crate::locale::Lc /// [`Lc`]: crate::locale::Lc
/// [`Html`]: crate::base::component::Html /// [`Theme::setup_component()`]: crate::core::theme::Theme::setup_component
/// [`Theme::render_component()`]: crate::core::theme::Theme::render_component
/// [`Embed`]: crate::core::component::Embed /// [`Embed`]: crate::core::component::Embed
/// [`Child`]: crate::core::component::Child /// [`Child`]: crate::core::component::Child
/// [`BeforeRender`]: crate::base::action::component::BeforeRender
/// [`Html`]: crate::base::component::Html
#[allow(unused_variables)] #[allow(unused_variables)]
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! {}) Ok(html! {})
@ -154,27 +162,35 @@ impl<T: Component + Clone + 'static> ComponentClone for T {
// ************************************************************************************************* // *************************************************************************************************
/// Implementa [`render()`](ComponentRender::render) para todos los componentes. /// Implementa [`render()`] para todos los componentes.
/// ///
/// El proceso de renderizado de cada componente sigue esta secuencia: /// El proceso de renderizado de cada componente sigue esta secuencia:
/// ///
/// 1. Ejecuta [`is_renderable()`](Component::is_renderable) para ver si puede renderizarse en el /// 1. Ejecuta [`is_renderable()`] para ver si puede renderizarse en el contexto actual. Si no es
/// contexto actual. Si no es así, devuelve un [`Markup`] vacío. /// así, devuelve un [`Markup`] vacío.
/// 2. Ejecuta [`setup()`](Component::setup) para que el componente /// 2. Ejecuta [`setup()`] para que el componente pueda ajustar su estado interno y, de forma
/// pueda ajustar su estructura interna. /// síncrona, el propio [`Context`].
/// 3. Despacha [`action::component::BeforeRender<C>`](crate::base::action::component::BeforeRender) /// 3. Despacha [`action::component::BeforeRender<C>`] para que las extensiones puedan hacer ajustes
/// para que las extensiones puedan hacer ajustes previos. /// previos.
/// 4. Prepara el renderizado del componente, recorre la cadena de temas (hijo > padre > abuelo...) /// 4. Prepara el renderizado del componente, recorre la cadena de temas (hijo > padre > abuelo...)
/// llamando a [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) en cada /// llamando en cada nivel primero a [`Theme::setup_component()`] y después a
/// nivel hasta que uno devuelva `Some`. Si ninguno lo sobrescribe, llama al /// [`Theme::render_component()`], hasta que uno devuelva `Some`. Si ninguno lo sobrescribe,
/// [`Component::prepare()`](Component::prepare) del propio componente. /// llama al [`Component::prepare()`] del propio componente.
/// 5. Despacha [`action::component::AfterRender<C>`](crate::base::action::component::AfterRender) /// 5. Despacha [`action::component::AfterRender<C>`] para que las extensiones puedan reaccionar con
/// para que las extensiones puedan reaccionar con sus últimos ajustes. /// sus últimos ajustes.
/// 6. Finalmente despacha /// 6. Finalmente despacha [`action::component::TransformMarkup<C>`] para que las extensiones puedan
/// [`action::component::TransformMarkup<C>`](crate::base::action::component::TransformMarkup) /// trabajar sobre el HTML final para modificarlo antes de devolverlo.
/// para que las extensiones puedan trabajar sobre el HTML final para modificarlo antes de
/// devolverlo.
/// 7. Devuelve el [`Markup`] resultante. /// 7. Devuelve el [`Markup`] resultante.
///
/// [`render()`]: ComponentRender::render
/// [`is_renderable()`]: Component::is_renderable
/// [`setup()`]: Component::setup
/// [`Component::prepare()`]: Component::prepare
/// [`action::component::BeforeRender<C>`]: crate::base::action::component::BeforeRender
/// [`action::component::AfterRender<C>`]: crate::base::action::component::AfterRender
/// [`action::component::TransformMarkup<C>`]: crate::base::action::component::TransformMarkup
/// [`Theme::setup_component()`]: crate::core::theme::Theme::setup_component
/// [`Theme::render_component()`]: crate::core::theme::Theme::render_component
#[async_trait] #[async_trait]
impl<C: Component> ComponentRender for C { impl<C: Component> ComponentRender for C {
async fn render(&mut self, cx: &mut Context) -> Markup { async fn render(&mut self, cx: &mut Context) -> Markup {
@ -183,7 +199,7 @@ impl<C: Component> ComponentRender for C {
return html! {}; return html! {};
} }
// Configura el componente antes de preparar. // Configura el componente (y, de forma síncrona, el contexto) antes de preparar.
self.setup(cx); self.setup(cx);
// Acciones de las extensiones antes de renderizar el componente. // Acciones de las extensiones antes de renderizar el componente.
@ -193,7 +209,8 @@ impl<C: Component> ComponentRender for C {
let result = 'resolve: { let result = 'resolve: {
let mut t: Option<ThemeRef> = Some(cx.theme()); let mut t: Option<ThemeRef> = Some(cx.theme());
while let Some(theme) = t { while let Some(theme) = t {
if let Some(r) = theme.handle_component(self, cx).await { theme.setup_component(self, cx);
if let Some(r) = theme.render_component(self, cx).await {
break 'resolve r; break 'resolve r;
} }
t = theme.parent(); t = theme.parent();

View file

@ -65,9 +65,9 @@ fn add_to_enabled(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
// Recorre la cadena de `Theme::parent()` para detectar referencias circulares. `parent()` se // Recorre la cadena de `Theme::parent()` para detectar referencias circulares. `parent()` se
// resuelve en tiempo de ejecución, así que un ciclo no puede descartarse al compilar. Se rechaza el // resuelve en tiempo de ejecución, así que un ciclo no puede descartarse al compilar. Se rechaza el
// arranque al detectar uno, antes de provocar un bucle infinito (en `Theme::handle_component()`) o // arranque si detecta uno, antes de provocar un bucle infinito en el `ComponentRender::render()` o
// un desbordamiento de pila (en los métodos predefinidos de `Theme` que delegan recursivamente en // un desbordamiento de pila en los métodos predefinidos de `Theme` que delegan recursivamente en el
// el tema padre). // tema padre.
fn check_theme_parent_chain(theme: ThemeRef) { fn check_theme_parent_chain(theme: ThemeRef) {
let mut chain: Vec<ThemeRef> = vec![theme]; let mut chain: Vec<ThemeRef> = vec![theme];
let mut current = theme; let mut current = theme;

View file

@ -1,15 +1,13 @@
//! API para añadir y gestionar nuevos temas. //! API para añadir y gestionar nuevos temas.
//! //!
//! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos //! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos
//! interactivos. Usa plantillas ([`Template`](crate::base::component::layout::Template)) para //! interactivos. Usa plantillas ([`Template`]) para maquetar los contenidos en base a regiones
//! maquetar los contenidos en base a regiones ([`Region`](crate::base::component::layout::Region)). //! ([`Region`]). Cada región es un contenedor lógico identificado por un nombre para agrupar y
//! Cada región es un contenedor lógico identificado por un nombre para agrupar y renderizar //! renderizar componentes.
//! componentes.
//! //!
//! Una página ([`Page`](crate::response::Page)) es un documento HTML completo. Implementa //! Una página ([`Page`]) es un documento HTML completo. Implementa [`Contextual`] para gestionar su
//! [`Contextual`](crate::core::component::Contextual) para gestionar su propio //! propio [`Context`], donde mantiene el tema activo, la plantilla seleccionada y los componentes
//! [`Context`](crate::core::component::Context), donde mantiene el tema activo, la plantilla //! asociados a cada región a renderizar.
//! seleccionada y los componentes asociados a cada región a renderizar.
//! //!
//! # Temas hijo, herencia y componentes //! # Temas hijo, herencia y componentes
//! //!
@ -17,26 +15,29 @@
//! identificado por [`Theme::parent()`]. Un tema hijo hereda automáticamente todos los métodos del //! identificado por [`Theme::parent()`]. Un tema hijo hereda automáticamente todos los métodos del
//! padre y puede sobrescribirlos selectivamente. Esta herencia determina qué implementación de sus //! padre y puede sobrescribirlos selectivamente. Esta herencia determina qué implementación de sus
//! métodos se usa cuando el tema hijo no los sobrescribe (ya sea el renderizado del `<body>` o del //! métodos se usa cuando el tema hijo no los sobrescribe (ya sea el renderizado del `<body>` o del
//! `<head>`, la definición de los recursos necesarios, la asignación de colores por intención vía //! `<head>`, la definición de los recursos necesarios, la traducción de puntos de corte y colores
//! [`Theme::intent_color()`], la captura de componentes con [`Theme::handle_component()`], las //! por intención vía [`Theme::breakpoint_min_width()`] y [`Theme::intent_color()`], la captura de
//! páginas de error, etc.). Un tema hijo puede ser a su vez padre de otro, basta declararlo cada //! componentes para alterar su comportamiento usando [`Theme::setup_component()`] y
//! vez con [`Theme::parent()`]. //! [`Theme::render_component()`], las páginas de error, etc.).
//! //!
//! Un tema hijo puede ser a su vez padre de otro, basta declararlo cada vez en [`Theme::parent()`].
//! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación //! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación
//! referencias circulares (un tema acaba siendo padre de sí mismo, directa o transitivamente). Ese //! referencias circulares (un tema acaba siendo padre de sí mismo, directa o transitivamente). Ese
//! ciclo provocaría un bucle infinito en [`Theme::handle_component()`] o un desbordamiento de pila //! ciclo causaría un bucle infinito recorriendo la cadena de temas en [`Theme::setup_component()`]
//! en los métodos predefinidos de `Theme` que delegan recursivamente en el padre. Para evitarlo, //! y [`Theme::render_component()`], o un desbordamiento de pila en los métodos predefinidos de
//! PageTop recorre la cadena de cada tema al registrarlo y **aborta el arranque de la aplicación** //! `Theme` que delegan recursivamente en el padre. Para evitarlo, PageTop recorre la cadena de cada
//! si detecta una referencia circular. //! tema al registrarlo y **aborta el arranque de la aplicación** si detecta una referencia
//! circular.
//! //!
//! Sin embargo, no dice nada sobre los componentes. Aunque un tema puede exportar su propio //! Sin embargo, no dice nada sobre los componentes. Aunque un tema puede exportar su propio
//! catálogo de componentes, realmente no pertenecen como tal a ningún tema ni dependen de esa //! catálogo de componentes, realmente no pertenecen como tal a ningún tema ni dependen de esa
//! cadena de herencia. Una extensión puede existir únicamente para aportar un componente genérico //! cadena de herencia. Una extensión puede existir únicamente para aportar un componente genérico
//! (por ejemplo, un editor de texto enriquecido) pensado para usarse en cualquier aplicación, con //! (por ejemplo, un editor de texto enriquecido) pensado para usarse en cualquier aplicación, con
//! independencia del tema activo. Que un tema decida capturar ese componente en //! independencia del tema activo. Que un tema decida capturar ese componente en
//! [`Theme::handle_component()`] para adaptarlo es una decisión propia del tema, no una relación de //! [`Theme::setup_component()`] o [`Theme::render_component()`] para adaptarlo es una decisión
//! parentesco: cualquier tema de la cadena de herencia puede interceptar cualquier componente, //! propia del tema, no una relación de parentesco. Cualquier tema de la cadena de herencia puede
//! venga de la extensión que venga, sin que exista ningún vínculo de diseño previo entre ambos. //! interceptar cualquier componente, venga de la extensión que venga, sin que exista ningún vínculo
//! de diseño previo entre ambos.
//! //!
//! Lo que sí es responsabilidad del tema activo es garantizar que el componente disponga de los //! Lo que sí es responsabilidad del tema activo es garantizar que el componente disponga de los
//! recursos que necesita para verse y comportarse correctamente: sus propios estilos y JavaScript, //! recursos que necesita para verse y comportarse correctamente: sus propios estilos y JavaScript,
@ -45,10 +46,9 @@
//! //!
//! # Cómo crear un tema nuevo //! # Cómo crear un tema nuevo
//! //!
//! Un tema mínimo es una extensión que implementa [`Extension`](crate::core::extension::Extension) //! Un tema mínimo es una extensión que implementa [`Extension`] y también [`Theme`] para que
//! y también [`Theme`] para que [`Extension::theme()`](crate::core::extension::Extension::theme) //! [`Extension::theme()`] devuelva `Some(&Self)`. Basta con un `impl Theme for MyTheme {}` vacío,
//! devuelva `Some(&Self)`. Basta con un `impl Theme for MyTheme {}` vacío, ya que todos los //! ya que todos los métodos de [`Theme`] tienen implementación por defecto.
//! métodos de [`Theme`] tienen implementación por defecto.
//! //!
//! Un tema puede personalizarse en seis pasos, cada uno necesario sólo si lo que ofrece PageTop //! Un tema puede personalizarse en seis pasos, cada uno necesario sólo si lo que ofrece PageTop
//! por defecto no basta o no aplica: //! por defecto no basta o no aplica:
@ -65,18 +65,18 @@
//! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada //! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada
//! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para //! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para
//! **añadir** plantillas que PageTop no ofrece, y no para redefinir `Standard`/`Admin`. //! **añadir** plantillas que PageTop no ofrece, y no para redefinir `Standard`/`Admin`.
//! 3. **Cambiar cómo se renderiza** una región, una plantilla o un componente ya existente, se hace //! 3. **Cambiar cómo se renderiza o se ajusta** una región, una plantilla o un componente ya
//! capturando el componente ([`Region`](crate::base::component::layout::Region) o //! existente. Para sobrescribir su renderizado se captura el componente (por ejemplo, [`Region`]
//! [`Template`](crate::base::component::layout::Template), o el componente que sea) en //! o [`Template`], o el componente que sea) usando [`Theme::render_component()`]. En el caso de
//! [`Theme::handle_component()`]. En el caso de regiones y plantillas, para distinguir *qué* //! regiones y plantillas, para distinguir *qué* región o plantilla concreta envuelve el
//! región o plantilla concreta envuelve el componente, sin comparar cadenas, basta con encadenar //! componente, sin comparar cadenas, basta con encadenar el *getter* correspondiente
//! el *getter* correspondiente
//! ([`Region::region()`](crate::base::component::layout::Region::region) o //! ([`Region::region()`](crate::base::component::layout::Region::region) o
//! [`Template::template()`](crate::base::component::layout::Template::template)) con //! [`Template::template()`](crate::base::component::layout::Template::template)) con
//! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por //! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por
//! ejemplo, [`CoreTemplates`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente //! ejemplo, [`CoreTemplates`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente
//! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias //! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias
//! variantes de plantilla. //! variantes de plantilla. Para ajustarlo sin rehacer su marcado (añadir una clase, un
//! atributo, etc.), se usa [`Theme::setup_component()`] en su lugar.
//! 4. **Definir los anchos mínimos *mobile-first* para los puntos de corte** sobrescribiendo //! 4. **Definir los anchos mínimos *mobile-first* para los puntos de corte** sobrescribiendo
//! [`Theme::breakpoint_min_width()`]. Por defecto, [`Breakpoint`] resuelve el ancho mínimo de //! [`Theme::breakpoint_min_width()`]. Por defecto, [`Breakpoint`] resuelve el ancho mínimo de
//! cada variante (`Sm`, `Md`, etc.) como una cadena CSS ya formateada (p. ej. `"768px"`) que //! cada variante (`Sm`, `Md`, etc.) como una cadena CSS ya formateada (p. ej. `"768px"`) que
@ -95,14 +95,14 @@
//! 6. **Reexportar, extender o añadir componentes**. Un tema puede reexportar tal cual los //! 6. **Reexportar, extender o añadir componentes**. Un tema puede reexportar tal cual los
//! componentes propios de PageTop que no requieran adaptación, extenderlos con un trait propio //! componentes propios de PageTop que no requieran adaptación, extenderlos con un trait propio
//! para añadir métodos exclusivos (guardando su estado en valores extra con //! para añadir métodos exclusivos (guardando su estado en valores extra con
//! [`PropsOp::set_extra()`](crate::html::PropsOp::set_extra) para consumirlos en el //! [`PropsOp::set_extra()`] para consumirlos en el `setup()` vía [`Theme::setup_component()`] o
//! `setup()`/`render()` vía [`Theme::handle_component()`]), o aportar componentes propios. //! en el `render()` vía [`Theme::render_component()`]), o aportar componentes propios.
//! `pagetop-bootsier` combina las tres estrategias: reexporta `Form`/`Fieldset` sin cambios, //! `pagetop-bootsier` combina las tres estrategias: reexporta `Form`/`Fieldset` sin cambios,
//! extiende `Button`/`Badge`/`Dropdown`/`Nav`/`Navbar` con sus propios traits (`ButtonBootsier`, //! extiende `Button`/`Badge`/`Dropdown`/`Nav`/`Navbar` con sus propios traits (`ButtonBootsier`,
//! `BadgeBootsier`, etc.), y añade componentes propios como `Offcanvas`. //! `BadgeBootsier`, etc.), y añade componentes propios como `Offcanvas`.
//! //!
//! Para forzar una plantilla completamente distinta en una página concreta, se puede llamar //! Para forzar una plantilla completamente distinta en una página concreta, se puede llamar
//! manualmente a [`with_template()`](crate::core::component::Contextual::with_template). //! manualmente a [`with_template()`].
//! //!
//! Las páginas de error (403, 404, y otros errores fatales) no tienen una plantilla propia: se //! Las páginas de error (403, 404, y otros errores fatales) no tienen una plantilla propia: se
//! renderizan con la plantilla ya activa en la página, para que el usuario no pierda el contexto de //! renderizan con la plantilla ya activa en la página, para que el usuario no pierda el contexto de
@ -115,11 +115,10 @@
//! //!
//! # Componentes que se procesan en todas las páginas //! # Componentes que se procesan en todas las páginas
//! //!
//! Los componentes añadidos a una página con //! Los componentes añadidos a una página con [`with_child_in()`] sólo existen para esa petición
//! [`with_child_in()`](crate::core::component::Contextual::with_child_in) sólo existen para esa //! concreta: hay que volver a añadirlos cada vez que se construya la página. [`InRegion`] resuelve
//! petición concreta: hay que volver a añadirlos cada vez que se construya la página. [`InRegion`] //! el caso contrario: un componente que se debe procesar en todas las páginas, o en todas las de un
//! resuelve el caso contrario: un componente que se debe procesar en todas las páginas, o en todas //! tema concreto, sin tener que registrarlo en el código de cada página.
//! las de un tema concreto, sin tener que registrarlo en el código de cada página.
//! //!
//! `InRegion` registra el componente una sola vez, normalmente al arrancar la aplicación o al //! `InRegion` registra el componente una sola vez, normalmente al arrancar la aplicación o al
//! inicializar una extensión, y a partir de ahí se procesa automáticamente en todas las páginas que //! inicializar una extensión, y a partir de ahí se procesa automáticamente en todas las páginas que
@ -134,13 +133,23 @@
//! renderizado, de modo que su `setup()` siempre parte de un estado inicial limpio y no acumula //! renderizado, de modo que su `setup()` siempre parte de un estado inicial limpio y no acumula
//! mutaciones entre peticiones. //! mutaciones entre peticiones.
//! //!
//! Como cualquier otro componente, antes de renderizarse pasa por //! Como cualquier otro componente, antes de renderizarse pasa por [`is_renderable()`], el primer
//! [`is_renderable()`](crate::core::component::Component::is_renderable), el primer paso del //! paso del [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite
//! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una //! registrarlo una sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta
//! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o //! de la petición o si el usuario actual está autenticado.
//! si el usuario actual está autenticado.
//! //!
//! [`PropsOp::set_extra()`]: crate::html::PropsOp::set_extra
//! [`ReservedRegions`]: crate::response::ReservedRegions //! [`ReservedRegions`]: crate::response::ReservedRegions
//! [`Page`]: crate::response::Page
//! [`Extension`]: crate::core::extension::Extension
//! [`Extension::theme()`]: crate::core::extension::Extension::theme
//! [`Context`]: crate::core::component::Context
//! [`Contextual`]: crate::core::component::Contextual
//! [`with_template()`]: crate::core::component::Contextual::with_template
//! [`with_child_in()`]: crate::core::component::Contextual::with_child_in
//! [`is_renderable()`]: crate::core::component::Component::is_renderable
//! [`Template`]: crate::base::component::layout::Template
//! [`Region`]: crate::base::component::layout::Region
mod intent; mod intent;
pub use intent::Intent; pub use intent::Intent;

View file

@ -236,38 +236,53 @@ pub trait Theme: Extension + Send + Sync {
} }
} }
/// Permite al tema intervenir en el ciclo de renderizado de un componente. /// Permite al tema modificar un componente antes de decidir cómo renderizarlo.
///
/// Este método tiene especial utilidad en los **temas hijo** porque permite ajustar el estado
/// de un componente concreto sin modificar el resto del comportamiento heredado.
///
/// Recibe una referencia mutable al componente (como objeto dinámico [`Component`]) y el
/// contexto de renderizado. Se ejecuta en cada tema de la cadena (hijo > padre > abuelo...), en
/// ese orden, justo antes de que ese mismo nivel decida si renderiza el componente con
/// [`render_component()`](Self::render_component). La implementación por defecto no hace nada.
///
/// Usa la macro [`setup_component!`](crate::setup_component) para mutar por tipo:
///
/// ```rust,ignore
/// fn setup_component(&self, component: &mut dyn Component, cx: &mut Context) {
/// setup_component!(component, {
/// Button => |btn| { btn.add_class("btn-primary"); },
/// });
/// }
/// ```
#[allow(unused_variables)]
fn setup_component(&self, component: &mut dyn Component, cx: &mut Context) {}
/// Permite al tema sobrescribir el renderizado de un componente.
/// ///
/// Este método tiene especial utilidad en los **temas hijo** porque permite sobrescribir el /// Este método tiene especial utilidad en los **temas hijo** porque permite sobrescribir el
/// renderizado que el propio componente o el tema padre ofrece para un componente concreto, sin /// renderizado que el propio componente o el tema padre ofrece para un componente concreto, sin
/// modificar el resto del comportamiento heredado. /// modificar el resto del comportamiento heredado.
/// ///
/// Recibe una referencia mutable al componente (como objeto dinámico [`Component`]) y el /// Recibe una referencia compartida al componente (como objeto dinámico [`Component`]), ya con
/// contexto de renderizado. Devuelve: /// las modificaciones de [`setup_component()`](Self::setup_component) del mismo nivel
/// aplicadas, y el contexto de renderizado. Devuelve:
/// ///
/// - `None` si este tema no sobrescribe el renderizado. Es la implementación por defecto. El /// - `None` si este tema no sobrescribe el renderizado. Es la implementación por defecto. El
/// sistema continúa con el siguiente tema de la cadena y, si ninguno lo sobrescribe, usa /// sistema continúa con el siguiente tema de la cadena y, si ninguno lo sobrescribe, usa
/// [`Component::prepare()`](crate::core::component::Component::prepare). /// [`Component::prepare()`](crate::core::component::Component::prepare).
/// El tema puede mutar el componente antes de devolver `None`, dejando que otro nivel de la
/// cadena se encargue del renderizado.
/// - `Some(Ok(markup))` con el HTML generado por el tema para el componente. /// - `Some(Ok(markup))` con el HTML generado por el tema para el componente.
/// - `Some(Err(e))` si el tema intentó renderizarlo pero falló. /// - `Some(Err(e))` si el tema intentó renderizarlo pero falló.
/// ///
/// Para renderizar usa [`render_component!`](crate::render_component), que devuelve `None` si /// Usa la macro [`render_component!`](crate::render_component), que devuelve `None` si ningún
/// ningún tipo coincide. Para mutar sin renderizar usa /// tipo coincide:
/// [`setup_component!`](crate::setup_component) y devuelve `None` explícitamente:
/// ///
/// ```rust,ignore /// ```rust,ignore
/// fn handle_component( /// fn render_component(
/// &self, /// &self,
/// component: &mut dyn Component, /// component: &dyn Component,
/// cx: &mut Context, /// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> { /// ) -> Option<Result<Markup, ComponentError>> {
/// // Sólo mutación: ajusta el componente y deja que otro nivel lo renderice.
/// setup_component!(component, {
/// Button => |btn| { btn.add_class("btn-primary"); },
/// });
/// // O renderizado completo:
/// render_component!(component, { /// render_component!(component, {
/// Button => |btn| Ok(html! { button.btn.btn-primary { (btn.label()) } }), /// Button => |btn| Ok(html! { button.btn.btn-primary { (btn.label()) } }),
/// Heading => |h| Ok(html! { h2.display-4 { (h.text()) } }), /// Heading => |h| Ok(html! { h2.display-4 { (h.text()) } }),
@ -275,9 +290,9 @@ pub trait Theme: Extension + Send + Sync {
/// } /// }
/// ``` /// ```
#[allow(unused_variables)] #[allow(unused_variables)]
async fn handle_component( async fn render_component(
&self, &self,
component: &mut dyn Component, component: &dyn Component,
cx: &mut Context, cx: &mut Context,
) -> Option<Result<Markup, ComponentError>> { ) -> Option<Result<Markup, ComponentError>> {
None None
@ -390,9 +405,47 @@ pub trait Theme: Extension + Send + Sync {
/// Referencia estática a un tema. /// Referencia estática a un tema.
pub type ThemeRef = &'static dyn Theme; pub type ThemeRef = &'static dyn Theme;
// **< setup_component! >***************************************************************************
/// Modifica un componente dentro de [`Theme::setup_component()`].
///
/// Evalúa `$component` contra cada tipo de componente listado en orden. En cuanto encuentra
/// coincidencia, ejecuta el bloque asociado y detiene la evaluación. Si ningún tipo coincide, no
/// hace nada.
///
/// Usa acceso mutable al componente mediante [`downcast_mut`], lo que permite modificar su estado.
///
/// # Ejemplo
///
/// ```rust,ignore
/// fn setup_component(&self, component: &mut dyn Component, cx: &mut Context) {
/// setup_component!(component, { Button => |btn| { btn.add_class("btn-primary"); } });
/// }
/// ```
///
/// El tipo mutado aquí puede además renderizarse en [`Theme::render_component()`] del mismo tema,
/// que se ejecuta a continuación sobre el componente ya mutado.
///
/// [`Theme::setup_component()`]: crate::core::theme::Theme::setup_component
/// [`downcast_mut`]: crate::core::AnyCast::downcast_mut
/// [`Theme::render_component()`]: crate::core::theme::Theme::render_component
#[macro_export]
macro_rules! setup_component {
($component:expr, { $($type:ty => |$var:ident| $body:expr),* $(,)? }) => {
'setup_component: {
$(
if let Some($var) = ($component).downcast_mut::<$type>() {
$body;
break 'setup_component;
}
)*
}
};
}
// **< render_component! >************************************************************************** // **< render_component! >**************************************************************************
/// Sobrescribe el renderizado de componentes en [`Theme::handle_component()`]. /// Sobrescribe el renderizado de componentes en [`Theme::render_component()`].
/// ///
/// Evalúa `$component` contra cada tipo de componente listado en orden. En cuanto encuentra /// Evalúa `$component` contra cada tipo de componente listado en orden. En cuanto encuentra
/// coincidencia, devuelve `Some(Ok(markup))` o `Some(Err(e))` según el resultado de la expresión /// coincidencia, devuelve `Some(Ok(markup))` o `Some(Err(e))` según el resultado de la expresión
@ -402,7 +455,7 @@ pub type ThemeRef = &'static dyn Theme;
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,ignore /// ```rust,ignore
/// fn handle_component( /// fn render_component(
/// &self, /// &self,
/// component: &dyn Component, /// component: &dyn Component,
/// cx: &mut Context, /// cx: &mut Context,
@ -417,15 +470,14 @@ pub type ThemeRef = &'static dyn Theme;
/// Ok(html! { h2.display-4 { (h.text()) } }) /// Ok(html! { h2.display-4 { (h.text()) } })
/// } /// }
/// ``` /// ```
///
/// [`Theme::render_component()`]: crate::core::theme::Theme::render_component
#[macro_export] #[macro_export]
macro_rules! render_component { macro_rules! render_component {
($component:expr, { $($type:ty => |$var:ident| $body:expr),* $(,)? }) => { ($component:expr, { $($type:ty => |$var:ident| $body:expr),* $(,)? }) => {
'render_component: { 'render_component: {
// Reborrow explícito como referencia compartida para que `downcast_ref` funcione
// correctamente con `&mut dyn Component` (limitación del compilador con trait objects).
let __c = &*($component);
$( $(
if let Some($var) = __c.downcast_ref::<$type>() { if let Some($var) = ($component).downcast_ref::<$type>() {
break 'render_component Some($body); break 'render_component Some($body);
} }
)* )*
@ -433,62 +485,3 @@ macro_rules! render_component {
} }
}; };
} }
// **< setup_component! >***************************************************************************
/// Muta un componente dentro de [`Theme::handle_component()`].
///
/// Evalúa `$component` contra cada tipo de componente listado en orden. En cuanto encuentra
/// coincidencia, ejecuta el bloque asociado y detiene la evaluación. Si ningún tipo coincide, no
/// hace nada.
///
/// Usa acceso mutable al componente mediante [`downcast_mut`](crate::core::AnyCast::downcast_mut),
/// lo que permite modificar su estado. El tema puede devolver `None` tras la mutación para que otro
/// nivel de la cadena se encargue del renderizado.
///
/// # Ejemplos
///
/// Solo mutación: el tema ajusta el componente y delega el renderizado al siguiente nivel:
///
/// ```rust,ignore
/// fn handle_component(
/// &self,
/// component: &mut dyn Component,
/// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> {
/// setup_component!(component, { Button => |btn| { btn.add_class("btn-primary"); } });
/// None
/// }
/// ```
///
/// Mutación y renderizado combinados: el `Button` se muta y se renderiza aquí; el `Heading` se
/// muta pero continúa la cadena para que otro nivel lo renderice:
///
/// ```rust,ignore
/// fn handle_component(
/// &self,
/// component: &mut dyn Component,
/// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> {
/// setup_component!(component, {
/// Button => |btn| { btn.add_class("btn-primary"); },
/// Heading => |h| { h.add_class("display-4"); },
/// });
/// render_component!(component, {
/// Button => |btn| Ok(html! { button.btn { (btn.label()) } }),
/// })
/// }
/// ```
#[macro_export]
macro_rules! setup_component {
($component:expr, { $($type:ty => |$var:ident| $body:expr),* $(,)? }) => {
'setup_component: {
$(
if let Some($var) = ($component).downcast_mut::<$type>() {
$body;
break 'setup_component;
}
)*
}
};
}

View file

@ -21,14 +21,14 @@ use crate::locale::Lc;
/// ///
/// Requiere [`AnyInfo`] para que un [`RegionRef`] pueda recuperarse mediante /// Requiere [`AnyInfo`] para que un [`RegionRef`] pueda recuperarse mediante
/// [`AnyCast::downcast_ref()`] hacia su tipo concreto (por ejemplo, para que un tema distinga en /// [`AnyCast::downcast_ref()`] hacia su tipo concreto (por ejemplo, para que un tema distinga en
/// [`Theme::handle_component()`] qué variante concreta está renderizando el componente [`Region`]). /// [`Theme::render_component()`] qué variante concreta está renderizando el componente [`Region`]).
/// ///
/// [`Context`]: crate::core::component::Context /// [`Context`]: crate::core::component::Context
/// [`Contextual::with_child_in()`]: crate::core::component::Contextual::with_child_in /// [`Contextual::with_child_in()`]: crate::core::component::Contextual::with_child_in
/// [`ReservedRegions`]: crate::response::ReservedRegions /// [`ReservedRegions`]: crate::response::ReservedRegions
/// [`Page`]: crate::response::Page /// [`Page`]: crate::response::Page
/// [`AnyCast::downcast_ref()`]: crate::core::AnyCast::downcast_ref /// [`AnyCast::downcast_ref()`]: crate::core::AnyCast::downcast_ref
/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component /// [`Theme::render_component()`]: crate::core::theme::Theme::render_component
/// [`Region`]: crate::base::component::layout::Region /// [`Region`]: crate::base::component::layout::Region
pub trait RegionName: Send + Sync + AnyInfo { pub trait RegionName: Send + Sync + AnyInfo {
/// Devuelve el nombre de la región. /// Devuelve el nombre de la región.
@ -117,14 +117,18 @@ impl RegionName for CoreRegions {
/// Interfaz común para las plantillas lógicas de una página. /// Interfaz común para las plantillas lógicas de una página.
/// ///
/// Representa una variante identificada por un nombre. Un tema puede usar este nombre para decidir /// Representa una variante identificada por un nombre. Un tema puede usar este nombre para decidir
/// la composición del cuerpo de una página ([`Page`](crate::response::Page)), es decir, qué /// la composición del cuerpo de una página ([`Page`]), es decir, qué regiones ([`RegionName`])
/// regiones ([`RegionName`]) renderizar y en qué orden. /// renderizar y en qué orden.
/// ///
/// Requiere [`AnyInfo`] por el mismo motivo que [`RegionName`], para que un [`TemplateRef`] pueda /// Requiere [`AnyInfo`] por el mismo motivo que [`RegionName`], para que un [`TemplateRef`] pueda
/// recuperarse mediante [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia su /// recuperarse mediante [`AnyCast::downcast_ref()`] hacia su tipo concreto (por ejemplo, para que
/// tipo concreto (por ejemplo, para que un tema distinga en /// un tema distinga en [`Theme::render_component()`] qué variante concreta está renderizando el
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) qué variante concreta /// componente [`Template`]).
/// está renderizando el componente [`Template`](crate::base::component::layout::Template)). ///
/// [`Page`]: crate::response::Page
/// [`AnyCast::downcast_ref()`]: crate::core::AnyCast::downcast_ref
/// [`Theme::render_component()`]: crate::core::theme::Theme::render_component
/// [`Template`]: crate::base::component::layout::Template
pub trait TemplateName: Send + Sync + AnyInfo { pub trait TemplateName: Send + Sync + AnyInfo {
/// Devuelve el nombre de la plantilla. /// Devuelve el nombre de la plantilla.
fn name(&self) -> &'static str; fn name(&self) -> &'static str;

View file

@ -112,11 +112,13 @@ impl Page {
/// Crea una nueva instancia de página con la plantilla [`CoreTemplates::Admin`]. /// Crea una nueva instancia de página con la plantilla [`CoreTemplates::Admin`].
/// ///
/// Cada tema puede maquetarla de forma distinta capturando /// Cada tema puede maquetarla de forma distinta capturando [`Template`] vía
/// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la /// [`Theme::render_component()`], pero la plantilla en sí es la misma constante para cualquier
/// plantilla en sí es la misma constante para cualquier tema. /// tema.
/// ///
/// [`CoreTemplates::Admin`]: crate::core::theme::CoreTemplates::Admin /// [`CoreTemplates::Admin`]: crate::core::theme::CoreTemplates::Admin
/// [`Template`]: crate::base::component::layout::Template
/// [`Theme::render_component()`]: crate::core::theme::Theme::render_component
pub fn admin(request: HttpRequest) -> Self { pub fn admin(request: HttpRequest) -> Self {
Page { Page {
context: Context::admin(request), context: Context::admin(request),

View file

@ -13,7 +13,7 @@ async fn setup() {
/// Replaces the default `Template` composition (`Header` + `Content` + `Footer`) with a fixed /// Replaces the default `Template` composition (`Header` + `Content` + `Footer`) with a fixed
/// marker string, for both `CoreTemplates::Standard` and `CoreTemplates::Admin`. Mirrors how /// marker string, for both `CoreTemplates::Standard` and `CoreTemplates::Admin`. Mirrors how
/// a real theme (e.g. `pagetop-bootsier`) tells its own layout apart from PageTop's default: by /// a real theme (e.g. `pagetop-bootsier`) tells its own layout apart from PageTop's default: by
/// intercepting the `Template` component in `handle_component()`, not by swapping which /// intercepting the `Template` component in `render_component()`, not by swapping which
/// `TemplateRef` gets resolved. /// `TemplateRef` gets resolved.
struct MarkerTheme; struct MarkerTheme;
@ -26,12 +26,12 @@ impl Extension for MarkerTheme {
#[async_trait] #[async_trait]
impl Theme for MarkerTheme { impl Theme for MarkerTheme {
async fn handle_component( async fn render_component(
&self, &self,
component: &mut dyn Component, component: &dyn Component,
_cx: &mut Context, _cx: &mut Context,
) -> Option<Result<Markup, ComponentError>> { ) -> Option<Result<Markup, ComponentError>> {
let template = (*component).downcast_ref::<layout::Template>()?; let template = component.downcast_ref::<layout::Template>()?;
template.template().downcast_ref::<CoreTemplates>()?; template.template().downcast_ref::<CoreTemplates>()?;
Some(Ok(html! { "marker-template-output" })) Some(Ok(html! { "marker-template-output" }))
} }
@ -42,7 +42,7 @@ impl Theme for MarkerTheme {
// `Theme::default_template()`/`admin_template()` were removed: `Context::template()` always // `Theme::default_template()`/`admin_template()` were removed: `Context::template()` always
// resolves `Default`/`Admin` to the core `CoreTemplates::Standard`/`Admin` identity, regardless // resolves `Default`/`Admin` to the core `CoreTemplates::Standard`/`Admin` identity, regardless
// of which theme is active. Themes customize the actual rendering by intercepting the `Template` // of which theme is active. Themes customize the actual rendering by intercepting the `Template`
// component in `handle_component()` instead (see the tests further below). // component in `render_component()` instead (see the tests further below).
#[pagetop::test] #[pagetop::test]
async fn default_template_identity_is_independent_of_theme() { async fn default_template_identity_is_independent_of_theme() {
@ -74,7 +74,7 @@ async fn explicit_template_is_not_overridden_by_a_later_with_theme() {
assert_eq!(cx.template().name(), "admin"); assert_eq!(cx.template().name(), "admin");
} }
// **< A theme customizes rendering via `handle_component()` >************************************** // **< A theme customizes rendering via `render_component()` >**************************************
#[pagetop::test] #[pagetop::test]
async fn without_a_matching_theme_the_default_composition_is_used() { async fn without_a_matching_theme_the_default_composition_is_used() {
@ -89,7 +89,7 @@ async fn without_a_matching_theme_the_default_composition_is_used() {
} }
#[pagetop::test] #[pagetop::test]
async fn theme_replaces_template_rendering_via_handle_component() { async fn theme_replaces_template_rendering_via_render_component() {
setup().await; setup().await;
let mut template = layout::Template::default(); let mut template = layout::Template::default();
@ -99,7 +99,7 @@ async fn theme_replaces_template_rendering_via_handle_component() {
assert_eq!(html, "marker-template-output"); assert_eq!(html, "marker-template-output");
} }
// **< Page::render() reaches the active theme's `handle_component()` >***************************** // **< Page::render() reaches the active theme's `render_component()` >*****************************
#[pagetop::test] #[pagetop::test]
async fn page_admin_render_reflects_the_active_theme_template() { async fn page_admin_render_reflects_the_active_theme_template() {