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
/// 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
/// [`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`].
///
/// 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) --
/// 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
/// [`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 {
let reg = global();
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()
}
async fn handle_component(
&self,
component: &mut dyn Component,
cx: &mut Context,
) -> Option<Result<Markup, ComponentError>> {
fn setup_component(&self, component: &mut dyn Component, _cx: &mut Context) {
setup_component!(component, {
Badge => |c| theme::bs::badge::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::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, {
layout::Region => |c| theme::bs::layout::region::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
//! 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:
//! intercepta el componente `Template` en `handle_component()` (ver `bs::layout`).
//! intercepta el componente `Template` en `render_component()` (ver `bs::layout`).
//!
//! ```rust,no_run
//! use pagetop::prelude::*;

View file

@ -31,7 +31,7 @@ impl Component for Icon {
self.props.get_id()
}
fn setup(&mut self, _cx: &Context) {
fn setup(&mut self, _cx: &mut Context) {
if !matches!(self.icon_kind(), IconKind::None) {
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
/// `<ul class="sidebar-menu">`, sin el `<div>` envolvente que añade
/// [`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
/// [`bs::sidebar::Item`](crate::theme::bs::sidebar::Item) y
/// [`bs::sidebar::Section`](crate::theme::bs::sidebar::Section).

View file

@ -71,7 +71,7 @@ impl Component for Offcanvas {
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.
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>
/// ```
///
/// 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`
/// 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

View file

@ -33,7 +33,7 @@ impl Component for RoleTable {
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::prepend_classes("user-admin-table-wrapper"));
}

View file

@ -34,7 +34,7 @@ impl Component for UserTable {
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::prepend_classes("user-admin-table-wrapper"));
}

View file

@ -33,7 +33,7 @@ impl Component for Badge {
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!(
"badge badge-",
self.intent().color(cx)

View file

@ -24,7 +24,7 @@ impl Component for Block {
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.
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()
}
fn setup(&mut self, _cx: &Context) {
fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes("brand"));
}

View file

@ -44,7 +44,7 @@ impl Component for Breadcrumb {
self.props.get_id()
}
fn setup(&mut self, cx: &Context) {
fn setup(&mut self, cx: &mut Context) {
for crumb in self.crumbs.iter_mut() {
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`.
pub(super) fn setup(&mut self, _cx: &Context) {
pub(super) fn setup(&mut self, _cx: &mut Context) {
if *self.is_current() {
self.alter_prop(PropsOp::prepend_classes("active"))
.alter_prop(PropsOp::set("aria-current", "page"));

View file

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

View file

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

View file

@ -62,7 +62,7 @@ impl Component for Dialog {
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.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));
self.alter_prop(PropsOp::prepend_classes("dialog"));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -67,7 +67,7 @@ impl Component for Range {
self.props.get_id()
}
fn setup(&mut self, _cx: &Context) {
fn setup(&mut self, _cx: &mut Context) {
if let Some(container_id) = self
.id()
.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()
}
fn setup(&mut self, _cx: &Context) {
fn setup(&mut self, _cx: &mut Context) {
if let Some(container_id) = self
.id()
.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()
}
fn setup(&mut self, _cx: &Context) {
fn setup(&mut self, _cx: &mut Context) {
if let Some(container_id) = self
.id()
.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()
}
fn setup(&mut self, _cx: &Context) {
fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes(match self.source() {
image::Source::Logo(_) => "image image-fluid",
image::Source::Responsive(_) => "image image-fluid",

View file

@ -9,8 +9,8 @@ use std::fmt;
/// se renderiza nada.
///
/// 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
/// [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`RegionRef`] que devuelve
/// componente en [`Theme::render_component()`](crate::core::theme::Theme::render_component) y
/// hacer [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`RegionRef`] que devuelve
/// [`Self::region()`], para compararlo con la variante deseada.
///
/// 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.
///
/// 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.
///
/// 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
/// [`Page::render()`]: crate::response::Page::render
/// [`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
#[derive(Clone, Getters)]
pub struct Template {

View file

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

View file

@ -43,7 +43,7 @@ impl Component for Nav {
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() {
nav::Layout::Default => "nav",
nav::Layout::Start => "nav nav-start",

View file

@ -77,7 +77,7 @@ impl Component for Item {
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()));
}

View file

@ -98,7 +98,7 @@ impl Component for Navbar {
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
// despliegue para referenciar el contenido colapsable con `aria-controls`.
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
&& let Some(nav) = nav.get_mut()
{

View file

@ -184,7 +184,7 @@ impl Component for Pager {
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
// identificadores de éste para no colisionar si hay varios paginadores en la misma página.
let id = cx.required_id::<Self>(self.id(), 1);

View file

@ -72,7 +72,7 @@ impl Component for Table {
self.props.get_id()
}
fn setup(&mut self, _cx: &Context) {
fn setup(&mut self, _cx: &mut Context) {
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.
///
/// Segundo paso del [ciclo de renderizado](ComponentRender): se ejecuta tras comprobar
/// [`is_renderable()`](Self::is_renderable) y antes de la acción
/// [`BeforeRender`](crate::base::action::component::BeforeRender) y de
/// [`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.
/// Segundo paso del [ciclo de renderizado](ComponentRender). Se ejecuta tras comprobar
/// [`is_renderable()`] y antes de la acción [`BeforeRender`] y de [`prepare()`]. Por defecto no
/// hace nada.
///
/// 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
/// contexto, etc. Se desaconseja utilizar para operaciones de E/S o consultas a base de datos;
/// es intencionadamente síncrono.
/// contexto, etc. Recibe `&mut Context` donde también puede aplicar ajustes que dependan del
/// 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
/// `async` precisamente para ello.
/// La frontera con [`prepare()`] es de **sincronía**, no de qué puede modificar cada uno. Los
/// 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)
/// (generación de HTML) es deliberada y no debe fusionarse.
/// [`is_renderable()`]: Self::is_renderable
/// [`prepare()`]: Self::prepare
/// [`BeforeRender`]: crate::base::action::component::BeforeRender
#[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.
///
/// 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
/// 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
/// 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.
///
/// 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
/// 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*).
///
/// [`setup()`]: Self::setup
/// [`BeforeRender`]: crate::base::action::component::BeforeRender
/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component
/// [`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
/// [`Child`]: crate::core::component::Child
/// [`BeforeRender`]: crate::base::action::component::BeforeRender
/// [`Html`]: crate::base::component::Html
#[allow(unused_variables)]
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
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:
///
/// 1. Ejecuta [`is_renderable()`](Component::is_renderable) para ver si puede renderizarse en el
/// contexto actual. Si no es así, devuelve un [`Markup`] vacío.
/// 2. Ejecuta [`setup()`](Component::setup) para que el componente
/// pueda ajustar su estructura interna.
/// 3. Despacha [`action::component::BeforeRender<C>`](crate::base::action::component::BeforeRender)
/// para que las extensiones puedan hacer ajustes previos.
/// 1. Ejecuta [`is_renderable()`] para ver si puede renderizarse en el contexto actual. Si no es
/// así, devuelve un [`Markup`] vacío.
/// 2. Ejecuta [`setup()`] para que el componente pueda ajustar su estado interno y, de forma
/// síncrona, el propio [`Context`].
/// 3. Despacha [`action::component::BeforeRender<C>`] para que las extensiones puedan hacer ajustes
/// previos.
/// 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
/// nivel hasta que uno devuelva `Some`. Si ninguno lo sobrescribe, llama al
/// [`Component::prepare()`](Component::prepare) del propio componente.
/// 5. Despacha [`action::component::AfterRender<C>`](crate::base::action::component::AfterRender)
/// para que las extensiones puedan reaccionar con sus últimos ajustes.
/// 6. Finalmente despacha
/// [`action::component::TransformMarkup<C>`](crate::base::action::component::TransformMarkup)
/// para que las extensiones puedan trabajar sobre el HTML final para modificarlo antes de
/// devolverlo.
/// llamando en cada nivel primero a [`Theme::setup_component()`] y después a
/// [`Theme::render_component()`], hasta que uno devuelva `Some`. Si ninguno lo sobrescribe,
/// llama al [`Component::prepare()`] del propio componente.
/// 5. Despacha [`action::component::AfterRender<C>`] para que las extensiones puedan reaccionar con
/// sus últimos ajustes.
/// 6. Finalmente despacha [`action::component::TransformMarkup<C>`] para que las extensiones puedan
/// trabajar sobre el HTML final para modificarlo antes de devolverlo.
/// 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]
impl<C: Component> ComponentRender for C {
async fn render(&mut self, cx: &mut Context) -> Markup {
@ -183,7 +199,7 @@ impl<C: Component> ComponentRender for C {
return html! {};
}
// Configura el componente antes de preparar.
// Configura el componente (y, de forma síncrona, el contexto) antes de preparar.
self.setup(cx);
// Acciones de las extensiones antes de renderizar el componente.
@ -193,7 +209,8 @@ impl<C: Component> ComponentRender for C {
let result = 'resolve: {
let mut t: Option<ThemeRef> = Some(cx.theme());
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;
}
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
// 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
// un desbordamiento de pila (en los métodos predefinidos de `Theme` que delegan recursivamente en
// el tema padre).
// 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 el
// tema padre.
fn check_theme_parent_chain(theme: ThemeRef) {
let mut chain: Vec<ThemeRef> = vec![theme];
let mut current = theme;

View file

@ -1,15 +1,13 @@
//! API para añadir y gestionar nuevos temas.
//!
//! 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
//! maquetar los contenidos en base a regiones ([`Region`](crate::base::component::layout::Region)).
//! Cada región es un contenedor lógico identificado por un nombre para agrupar y renderizar
//! componentes.
//! interactivos. Usa plantillas ([`Template`]) para maquetar los contenidos en base a regiones
//! ([`Region`]). Cada región es un contenedor lógico identificado por un nombre para agrupar y
//! renderizar componentes.
//!
//! Una página ([`Page`](crate::response::Page)) es un documento HTML completo. Implementa
//! [`Contextual`](crate::core::component::Contextual) para gestionar su propio
//! [`Context`](crate::core::component::Context), donde mantiene el tema activo, la plantilla
//! seleccionada y los componentes asociados a cada región a renderizar.
//! Una página ([`Page`]) es un documento HTML completo. Implementa [`Contextual`] para gestionar su
//! propio [`Context`], donde mantiene el tema activo, la plantilla seleccionada y los componentes
//! asociados a cada región a renderizar.
//!
//! # Temas hijo, herencia y componentes
//!
@ -17,26 +15,29 @@
//! 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
//! 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
//! [`Theme::intent_color()`], la captura de componentes con [`Theme::handle_component()`], las
//! páginas de error, etc.). Un tema hijo puede ser a su vez padre de otro, basta declararlo cada
//! vez con [`Theme::parent()`].
//! `<head>`, la definición de los recursos necesarios, la traducción de puntos de corte y colores
//! por intención vía [`Theme::breakpoint_min_width()`] y [`Theme::intent_color()`], la captura de
//! componentes para alterar su comportamiento usando [`Theme::setup_component()`] y
//! [`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
//! 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
//! en los métodos predefinidos de `Theme` que delegan recursivamente en el padre. Para evitarlo,
//! PageTop recorre la cadena de cada tema al registrarlo y **aborta el arranque de la aplicación**
//! si detecta una referencia circular.
//! ciclo causaría un bucle infinito recorriendo la cadena de temas en [`Theme::setup_component()`]
//! y [`Theme::render_component()`], o un desbordamiento de pila en los métodos predefinidos de
//! `Theme` que delegan recursivamente en el padre. Para evitarlo, PageTop recorre la cadena de cada
//! 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
//! 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
//! (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
//! [`Theme::handle_component()`] para adaptarlo es una decisión propia del tema, no una relación de
//! parentesco: cualquier tema de la cadena de herencia puede interceptar cualquier componente,
//! venga de la extensión que venga, sin que exista ningún vínculo de diseño previo entre ambos.
//! [`Theme::setup_component()`] o [`Theme::render_component()`] para adaptarlo es una decisión
//! propia del tema, no una relación de parentesco. Cualquier tema de la cadena de herencia puede
//! 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
//! recursos que necesita para verse y comportarse correctamente: sus propios estilos y JavaScript,
@ -45,10 +46,9 @@
//!
//! # Cómo crear un tema nuevo
//!
//! Un tema mínimo es una extensión que implementa [`Extension`](crate::core::extension::Extension)
//! y también [`Theme`] para que [`Extension::theme()`](crate::core::extension::Extension::theme)
//! devuelva `Some(&Self)`. Basta con un `impl Theme for MyTheme {}` vacío, ya que todos los
//! métodos de [`Theme`] tienen implementación por defecto.
//! Un tema mínimo es una extensión que implementa [`Extension`] y también [`Theme`] para que
//! [`Extension::theme()`] devuelva `Some(&Self)`. Basta con un `impl Theme for MyTheme {}` vacío,
//! ya que todos los 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
//! 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
//! 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`.
//! 3. **Cambiar cómo se renderiza** una región, una plantilla o un componente ya existente, se hace
//! capturando el componente ([`Region`](crate::base::component::layout::Region) o
//! [`Template`](crate::base::component::layout::Template), o el componente que sea) en
//! [`Theme::handle_component()`]. En el caso de regiones y plantillas, para distinguir *qué*
//! región o plantilla concreta envuelve el componente, sin comparar cadenas, basta con encadenar
//! el *getter* correspondiente
//! 3. **Cambiar cómo se renderiza o se ajusta** una región, una plantilla o un componente ya
//! existente. Para sobrescribir su renderizado se captura el componente (por ejemplo, [`Region`]
//! o [`Template`], o el componente que sea) usando [`Theme::render_component()`]. En el caso de
//! regiones y plantillas, para distinguir *qué* región o plantilla concreta envuelve el
//! componente, sin comparar cadenas, basta con encadenar el *getter* correspondiente
//! ([`Region::region()`](crate::base::component::layout::Region::region) o
//! [`Template::template()`](crate::base::component::layout::Template::template)) con
//! [`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
//! 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
//! [`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
@ -95,14 +95,14 @@
//! 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
//! 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
//! `setup()`/`render()` vía [`Theme::handle_component()`]), o aportar componentes propios.
//! [`PropsOp::set_extra()`] para consumirlos en el `setup()` vía [`Theme::setup_component()`] o
//! en el `render()` vía [`Theme::render_component()`]), o aportar componentes propios.
//! `pagetop-bootsier` combina las tres estrategias: reexporta `Form`/`Fieldset` sin cambios,
//! extiende `Button`/`Badge`/`Dropdown`/`Nav`/`Navbar` con sus propios traits (`ButtonBootsier`,
//! `BadgeBootsier`, etc.), y añade componentes propios como `Offcanvas`.
//!
//! 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
//! 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
//!
//! Los componentes añadidos a una página con
//! [`with_child_in()`](crate::core::component::Contextual::with_child_in) sólo existen para esa
//! petición concreta: hay que volver a añadirlos cada vez que se construya la página. [`InRegion`]
//! resuelve el caso contrario: un componente que se debe procesar en todas las páginas, o en todas
//! las de un tema concreto, sin tener que registrarlo en el código de cada página.
//! Los componentes añadidos a una página con [`with_child_in()`] sólo existen para esa petición
//! concreta: hay que volver a añadirlos cada vez que se construya la página. [`InRegion`] resuelve
//! el caso contrario: un componente que se debe procesar en todas las páginas, o en todas 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
//! 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
//! mutaciones entre peticiones.
//!
//! Como cualquier otro componente, antes de renderizarse pasa por
//! [`is_renderable()`](crate::core::component::Component::is_renderable), el primer paso del
//! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una
//! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o
//! si el usuario actual está autenticado.
//! Como cualquier otro componente, antes de renderizarse pasa por [`is_renderable()`], el primer
//! paso del [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite
//! registrarlo una sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta
//! de la petición o si el usuario actual está autenticado.
//!
//! [`PropsOp::set_extra()`]: crate::html::PropsOp::set_extra
//! [`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;
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
/// renderizado que el propio componente o el tema padre ofrece para 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. Devuelve:
/// Recibe una referencia compartida al componente (como objeto dinámico [`Component`]), ya con
/// 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
/// sistema continúa con el siguiente tema de la cadena y, si ninguno lo sobrescribe, usa
/// [`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(Err(e))` si el tema intentó renderizarlo pero falló.
///
/// Para renderizar usa [`render_component!`](crate::render_component), que devuelve `None` si
/// ningún tipo coincide. Para mutar sin renderizar usa
/// [`setup_component!`](crate::setup_component) y devuelve `None` explícitamente:
/// Usa la macro [`render_component!`](crate::render_component), que devuelve `None` si ningún
/// tipo coincide:
///
/// ```rust,ignore
/// fn handle_component(
/// fn render_component(
/// &self,
/// component: &mut dyn Component,
/// component: &dyn Component,
/// cx: &mut Context,
/// ) -> 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, {
/// Button => |btn| Ok(html! { button.btn.btn-primary { (btn.label()) } }),
/// Heading => |h| Ok(html! { h2.display-4 { (h.text()) } }),
@ -275,9 +290,9 @@ pub trait Theme: Extension + Send + Sync {
/// }
/// ```
#[allow(unused_variables)]
async fn handle_component(
async fn render_component(
&self,
component: &mut dyn Component,
component: &dyn Component,
cx: &mut Context,
) -> Option<Result<Markup, ComponentError>> {
None
@ -390,9 +405,47 @@ pub trait Theme: Extension + Send + Sync {
/// Referencia estática a un tema.
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! >**************************************************************************
/// 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
/// 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
///
/// ```rust,ignore
/// fn handle_component(
/// fn render_component(
/// &self,
/// component: &dyn Component,
/// cx: &mut Context,
@ -417,15 +470,14 @@ pub type ThemeRef = &'static dyn Theme;
/// Ok(html! { h2.display-4 { (h.text()) } })
/// }
/// ```
///
/// [`Theme::render_component()`]: crate::core::theme::Theme::render_component
#[macro_export]
macro_rules! render_component {
($component:expr, { $($type:ty => |$var:ident| $body:expr),* $(,)? }) => {
'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);
}
)*
@ -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
/// [`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
/// [`Contextual::with_child_in()`]: crate::core::component::Contextual::with_child_in
/// [`ReservedRegions`]: crate::response::ReservedRegions
/// [`Page`]: crate::response::Page
/// [`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
pub trait RegionName: Send + Sync + AnyInfo {
/// 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.
///
/// 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é
/// regiones ([`RegionName`]) renderizar y en qué orden.
/// la composición del cuerpo de una página ([`Page`]), es decir, qué regiones ([`RegionName`])
/// renderizar y en qué orden.
///
/// 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
/// tipo concreto (por ejemplo, para que un tema distinga en
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) qué variante concreta
/// está renderizando el componente [`Template`](crate::base::component::layout::Template)).
/// recuperarse mediante [`AnyCast::downcast_ref()`] hacia su tipo concreto (por ejemplo, para que
/// un tema distinga en [`Theme::render_component()`] qué variante concreta está renderizando el
/// componente [`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 {
/// Devuelve el nombre de la plantilla.
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`].
///
/// Cada tema puede maquetarla de forma distinta capturando
/// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la
/// plantilla en sí es la misma constante para cualquier tema.
/// Cada tema puede maquetarla de forma distinta capturando [`Template`] vía
/// [`Theme::render_component()`], pero la plantilla en sí es la misma constante para cualquier
/// tema.
///
/// [`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 {
Page {
context: Context::admin(request),

View file

@ -13,7 +13,7 @@ async fn setup() {
/// Replaces the default `Template` composition (`Header` + `Content` + `Footer`) with a fixed
/// 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
/// 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.
struct MarkerTheme;
@ -26,12 +26,12 @@ impl Extension for MarkerTheme {
#[async_trait]
impl Theme for MarkerTheme {
async fn handle_component(
async fn render_component(
&self,
component: &mut dyn Component,
component: &dyn Component,
_cx: &mut Context,
) -> Option<Result<Markup, ComponentError>> {
let template = (*component).downcast_ref::<layout::Template>()?;
let template = component.downcast_ref::<layout::Template>()?;
template.template().downcast_ref::<CoreTemplates>()?;
Some(Ok(html! { "marker-template-output" }))
}
@ -42,7 +42,7 @@ impl Theme for MarkerTheme {
// `Theme::default_template()`/`admin_template()` were removed: `Context::template()` always
// 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`
// component in `handle_component()` instead (see the tests further below).
// component in `render_component()` instead (see the tests further below).
#[pagetop::test]
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");
}
// **< A theme customizes rendering via `handle_component()` >**************************************
// **< A theme customizes rendering via `render_component()` >**************************************
#[pagetop::test]
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]
async fn theme_replaces_template_rendering_via_handle_component() {
async fn theme_replaces_template_rendering_via_render_component() {
setup().await;
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");
}
// **< Page::render() reaches the active theme's `handle_component()` >*****************************
// **< Page::render() reaches the active theme's `render_component()` >*****************************
#[pagetop::test]
async fn page_admin_render_reflects_the_active_theme_template() {