pagetop/src/core/component/definition.rs
Manuel Cillero 6f82da220b 🐛 (web): HttpRequest clona ahora las extensiones
Documenta el nuevo comportamiento y aprovecha para pulir varios
comentarios y restringir visibilidad de símbolos internos.
2026-07-17 00:54:35 +02:00

218 lines
10 KiB
Rust

use crate::async_trait;
use crate::base::action;
use crate::core::component::{ComponentError, Context, Contextual};
use crate::core::theme::ThemeRef;
use crate::core::{AnyInfo, TypeInfo};
use crate::html::{Markup, html};
/// Habilita el clonado de componentes.
///
/// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. El método
/// [`clone_box`](Self::clone_box) devuelve un clon del componente original encapsulado en un
/// `Box<dyn Component>`, lo que permite clonar componentes sin conocer su tipo concreto en tiempo
/// de compilación.
pub trait ComponentClone {
/// Devuelve un clon del componente encapsulado en un [`Box<dyn Component>`].
fn clone_box(&self) -> Box<dyn Component>;
}
/// Define la función de renderizado para todos los componentes.
///
/// Este *trait* se implementa automáticamente en cualquier tipo (componente) que implemente
/// [`Component`], por lo que no requiere ninguna codificación manual.
#[async_trait]
pub trait ComponentRender {
/// Renderiza el componente usando el contexto proporcionado.
async fn render(&mut self, cx: &mut Context) -> Markup;
}
/// Interfaz común que debe implementar un componente renderizable en PageTop.
///
/// Se recomienda que los componentes declaren sus campos como privados, que deriven
/// [`AutoDefault`](crate::AutoDefault) o implementen [`Default`] para inicializarlos por defecto, y
/// [`Getters`](crate::Getters) para acceder a sus datos. Deberán implementar explícitamente el
/// método [`new()`](Self::new) y podrán sobrescribir los demás métodos para personalizar su
/// comportamiento.
///
/// # Requisito: derivar `Clone`
///
/// Todo tipo que implemente `Component` **debe** derivar también [`Clone`]. Aunque el compilador
/// no lo exige directamente (hacerlo rompería la seguridad de objeto de `dyn Component`),
/// [`ComponentClone`] se implementa automáticamente mediante una *impl blanket* sólo para los tipos
/// que sean `Component + Clone + 'static`. Sin `Clone`, habría que implementar [`ComponentClone`] a
/// mano, y el componente no podría registrarse en [`InRegion`](crate::core::theme::InRegion).
#[async_trait]
pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// Crea una nueva instancia del componente.
///
/// Por convención suele devolver `Self::default()`.
fn new() -> Self
where
Self: Sized;
/// Devuelve el nombre del componente.
///
/// Por defecto se obtiene del nombre corto del tipo usando [`TypeInfo::ShortName`].
fn name(&self) -> &'static str {
TypeInfo::ShortName.of::<Self>()
}
/// Devuelve una descripción del componente, si existe.
///
/// Por defecto, no se proporciona ninguna descripción (`None`).
fn description(&self) -> Option<String> {
None
}
/// Devuelve el identificador del componente, si existe.
///
/// Este identificador puede usarse para referenciar el componente en el HTML. Por defecto, no
/// tiene ningún identificador (`None`).
fn id(&self) -> Option<String> {
None
}
/// Indica si el componente es renderizable.
///
/// Por defecto, todos los componentes son renderizables (`true`). Sin embargo, este método
/// puede sobrescribirse para decidir dinámicamente si los componentes de este tipo se
/// renderizan o no en función del contexto de renderizado. Recibe sólo una referencia
/// compartida al contexto porque su único propósito es consultar datos, no modificarlos.
///
/// También **puede asignarse una función [`FnIsRenderable`](super::FnIsRenderable) a un campo
/// del componente** para permitir que instancias concretas del mismo puedan decidir
/// dinámicamente si se renderizan o no.
#[allow(unused_variables)]
fn is_renderable(&self, cx: &Context) -> bool {
true
}
/// 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.
///
/// 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.
///
/// La carga y el acceso a datos en general corresponden a [`prepare()`](Self::prepare), que es
/// `async` precisamente para ello.
///
/// La separación entre `setup()` (mutación de estado) y [`prepare()`](Self::prepare)
/// (generación de HTML) es deliberada y no debe fusionarse.
#[allow(unused_variables)]
fn setup(&mut self, cx: &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()`](Self::setup) del componente y despachar la acción
/// [`BeforeRender`](crate::base::action::component::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()`](crate::core::theme::Theme::handle_component) para
/// este componente.
///
/// Es `async`, a diferencia de [`setup()`](Self::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.
///
/// Los campos que representen contenido no deben almacenar [`Markup`] ya generado, sino que
/// guardarán el dato en bruto, por ejemplo [`L10n`](crate::locale::L10n) para textos
/// traducibles, o un componente anidado como [`Html`](crate::base::component::Html), o
/// cualquier otro `Component`, normalmente a través de [`Embed`](crate::core::component::Embed)
/// o [`Child`](crate::core::component::Child); y será este método quien lo convierta en HTML en
/// el momento del renderizado, no quien construye la instancia.
///
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`). En caso de error, devuelve un
/// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*).
#[allow(unused_variables)]
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! {})
}
}
// *************************************************************************************************
impl<T: Component + Clone + 'static> ComponentClone for T {
fn clone_box(&self) -> Box<dyn Component> {
Box::new(self.clone())
}
}
// *************************************************************************************************
/// Implementa [`render()`](ComponentRender::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.
/// 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.
/// 7. Devuelve el [`Markup`] resultante.
#[async_trait]
impl<C: Component> ComponentRender for C {
async fn render(&mut self, cx: &mut Context) -> Markup {
// Si no es renderizable, devuelve un bloque HTML vacío.
if !self.is_renderable(cx) {
return html! {};
}
// Configura el componente antes de preparar.
self.setup(cx);
// Acciones de las extensiones antes de renderizar el componente.
action::component::BeforeRender::dispatch(self, cx);
// Prepara el renderizado: recorre la cadena de temas, luego el componente.
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 {
break 'resolve r;
}
t = theme.parent();
}
self.prepare(cx).await
};
let prepare = match result {
Ok(markup) => markup,
Err(error) => {
crate::trace::error!(
path = cx.request().map(|r| r.path()).unwrap_or("<unknown>"),
component = self.name(),
id = self.id().as_deref().unwrap_or("<not set>"),
"render failed, using fallback: {}",
error.message()
);
error.into_fallback()
}
};
// Acciones de las extensiones después de renderizar el componente.
action::component::AfterRender::dispatch(self, cx);
// Acciones de las extensiones que transforman el HTML final antes de devolverlo.
action::component::TransformMarkup::dispatch(self, cx, prepare)
}
}