♻️ (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()`.
This commit is contained in:
parent
1e2d171805
commit
28f1eee391
15 changed files with 185 additions and 170 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -119,14 +119,15 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
|
|||
/// 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
|
||||
|
|
@ -138,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! {})
|
||||
|
|
@ -171,8 +173,9 @@ impl<T: Component + Clone + 'static> ComponentClone for T {
|
|||
/// 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()`] en cada nivel hasta que uno devuelva `Some`. Si
|
||||
/// ninguno lo sobrescribe, llama al [`Component::prepare()`] del propio componente.
|
||||
/// 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
|
||||
|
|
@ -186,7 +189,8 @@ impl<T: Component + Clone + 'static> ComponentClone for T {
|
|||
/// [`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::handle_component()`]: crate::core::theme::Theme::handle_component
|
||||
/// [`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 {
|
||||
|
|
@ -205,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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue