♻️ Refactoriza la API de Children e InRegion

- Patrón prototipo en `InRegion`: cada petición recibe clones profundos.
- `ComponentClone` habilita clonar `dyn Component` de forma segura.
- `ChildTyped<C>` renombrado a `Slot<C>`, elimina `ChildTypedOp`.
- `Mutex` en lugar de `Arc<RwLock>` en `Child` y `Slot`.
- `is_renderable` y `setup_before_prepare` reciben `&Context`.
- Nuevos tests para `Children`, `ChildOp` y `Slot`.
This commit is contained in:
Manuel Cillero 2026-03-26 20:36:32 +01:00 committed by Manuel Cillero
parent f1d3218a68
commit 2626234163
33 changed files with 740 additions and 314 deletions

View file

@ -4,7 +4,7 @@ use crate::prelude::*;
///
/// Los bloques se utilizan como contenedores de otros componentes o contenidos, con un título
/// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*).
#[derive(AutoDefault, Debug, Getters)]
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Block {
#[getters(skip)]
id: AttrId,
@ -25,11 +25,11 @@ impl Component for Block {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
fn setup(&mut self, _cx: &Context) {
self.alter_classes(ClassesOp::Prepend, "block");
}
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let block_body = self.children().render(cx);
if block_body.is_empty() {

View file

@ -1,6 +1,7 @@
use crate::prelude::*;
use std::fmt;
use std::sync::Arc;
/// Componente básico que renderiza dinámicamente código HTML según el contexto.
///
@ -31,7 +32,8 @@ use std::fmt;
/// }
/// });
/// ```
pub struct Html(Box<dyn Fn(&mut Context) -> Markup + Send + Sync>);
#[derive(Clone)]
pub struct Html(Arc<dyn Fn(&mut Context) -> Markup + Send + Sync>);
impl fmt::Debug for Html {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -52,7 +54,7 @@ impl Component for Html {
Self::default()
}
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(self.html(cx))
}
}
@ -62,26 +64,26 @@ impl Html {
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
///
/// El método [`Self::prepare_component()`] delega el renderizado a la función que aquí se
/// proporciona, que recibe una referencia mutable al [`Context`].
/// El método [`Self::prepare()`] delega el renderizado a la función que aquí se proporciona,
/// con una llamada que requiere una referencia mutable al [`Context`].
pub fn with<F>(f: F) -> Self
where
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
{
Html(Box::new(f))
Html(Arc::new(f))
}
/// Sustituye la función que genera el `Markup`.
///
/// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando
/// [`Self::prepare_component()`] invoque esta instancia. La nueva función también recibe una
/// referencia al [`Context`].
/// [`Self::prepare()`] invoque esta instancia. La nueva función también recibe una referencia
/// mutable al [`Context`].
#[builder_fn]
pub fn with_fn<F>(mut self, f: F) -> Self
where
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
{
self.0 = Box::new(f);
self.0 = Arc::new(f);
self
}
@ -91,8 +93,8 @@ impl Html {
///
/// Normalmente no se invoca manualmente, ya que el proceso de renderizado de los componentes lo
/// invoca automáticamente durante la construcción de la página. Puede usarse, no obstante, para
/// sobrescribir [`prepare_component()`](crate::core::component::Component::prepare_component)
/// y alterar el comportamiento del componente.
/// sobrescribir [`prepare()`](crate::core::component::Component::prepare) y alterar el
/// comportamiento del componente.
pub fn html(&self, cx: &mut Context) -> Markup {
(self.0)(cx)
}

View file

@ -76,7 +76,7 @@ pub enum IntroOpening {
/// })),
/// );
/// ```
#[derive(Debug, Getters)]
#[derive(Clone, Debug, Getters)]
pub struct Intro {
/// Devuelve el título de entrada.
title: L10n,
@ -109,13 +109,10 @@ impl Component for Intro {
Self::default()
}
fn setup_before_prepare(&mut self, cx: &mut Context) {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
cx.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION),
));
}
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
if *self.opening() == IntroOpening::PageTop {
cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
util::indoc!(r#"

View file

@ -8,7 +8,7 @@ const LINK: &str = "<a href=\"https://pagetop.cillero.es\" rel=\"noopener norefe
/// Por defecto, usando [`default()`](Self::default) sólo se muestra un reconocimiento a PageTop.
/// Sin embargo, se puede usar [`new()`](Self::new) para crear una instancia con un texto de
/// copyright predeterminado.
#[derive(AutoDefault, Debug, Getters)]
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct PoweredBy {
/// Devuelve el texto de copyright actual, si existe.
copyright: Option<String>,
@ -25,7 +25,7 @@ impl Component for PoweredBy {
PoweredBy { copyright: Some(c) }
}
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! {
div id=[self.id()] class="poweredby" {
@if let Some(c) = self.copyright() {