From 1e2d1718052c61d2e42d03edb4e5d6651190b41c Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 6 Sep 2026 09:04:44 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(pagetop):=20setup()=20rec?= =?UTF-8?q?ibe=20&mut=20Context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../pagetop-bootsier/src/theme/bs/icon.rs | 2 +- .../src/theme/bs/offcanvas/component.rs | 2 +- .../src/component/admin/role_table.rs | 2 +- .../src/component/admin/user_table.rs | 2 +- src/base/component/badge.rs | 2 +- src/base/component/block.rs | 2 +- src/base/component/brand.rs | 2 +- src/base/component/breadcrumb/component.rs | 2 +- src/base/component/breadcrumb/crumb.rs | 2 +- src/base/component/button/component.rs | 2 +- src/base/component/container.rs | 2 +- src/base/component/dialog.rs | 2 +- src/base/component/dropdown/component.rs | 2 +- src/base/component/form/check.rs | 2 +- src/base/component/form/checkbox.rs | 2 +- src/base/component/form/component.rs | 2 +- src/base/component/form/input.rs | 2 +- src/base/component/form/number.rs | 2 +- src/base/component/form/radio.rs | 2 +- src/base/component/form/range.rs | 2 +- src/base/component/form/select.rs | 2 +- src/base/component/form/textarea.rs | 2 +- src/base/component/image/component.rs | 2 +- src/base/component/messages.rs | 2 +- src/base/component/nav/component.rs | 2 +- src/base/component/nav/item.rs | 2 +- src/base/component/navbar/component.rs | 2 +- src/base/component/navbar/item.rs | 2 +- src/base/component/pager.rs | 2 +- src/base/component/table/component.rs | 2 +- src/core/component/definition.rs | 70 +++++++++++-------- 31 files changed, 71 insertions(+), 59 deletions(-) diff --git a/extensions/pagetop-bootsier/src/theme/bs/icon.rs b/extensions/pagetop-bootsier/src/theme/bs/icon.rs index d3c628a7..69d94073 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/icon.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/icon.rs @@ -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")); } diff --git a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs index 2e902a1e..0fd73efa 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs @@ -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::(1))); diff --git a/extensions/pagetop-user/src/component/admin/role_table.rs b/extensions/pagetop-user/src/component/admin/role_table.rs index 10123264..d03bf7dd 100644 --- a/extensions/pagetop-user/src/component/admin/role_table.rs +++ b/extensions/pagetop-user/src/component/admin/role_table.rs @@ -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")); } diff --git a/extensions/pagetop-user/src/component/admin/user_table.rs b/extensions/pagetop-user/src/component/admin/user_table.rs index 851574e1..495b190d 100644 --- a/extensions/pagetop-user/src/component/admin/user_table.rs +++ b/extensions/pagetop-user/src/component/admin/user_table.rs @@ -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")); } diff --git a/src/base/component/badge.rs b/src/base/component/badge.rs index b27b0ca8..5078410c 100644 --- a/src/base/component/badge.rs +++ b/src/base/component/badge.rs @@ -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) diff --git a/src/base/component/block.rs b/src/base/component/block.rs index 39d6534a..f440702a 100644 --- a/src/base/component/block.rs +++ b/src/base/component/block.rs @@ -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::(1))); diff --git a/src/base/component/brand.rs b/src/base/component/brand.rs index 773ff6a8..4035423f 100644 --- a/src/base/component/brand.rs +++ b/src/base/component/brand.rs @@ -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")); } diff --git a/src/base/component/breadcrumb/component.rs b/src/base/component/breadcrumb/component.rs index 2543a5b5..622fb04f 100644 --- a/src/base/component/breadcrumb/component.rs +++ b/src/base/component/breadcrumb/component.rs @@ -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); } diff --git a/src/base/component/breadcrumb/crumb.rs b/src/base/component/breadcrumb/crumb.rs index bbfc3501..ebf1f894 100644 --- a/src/base/component/breadcrumb/crumb.rs +++ b/src/base/component/breadcrumb/crumb.rs @@ -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")); diff --git a/src/base/component/button/component.rs b/src/base/component/button/component.rs index 224c98ef..7ee6a965 100644 --- a/src/base/component/button/component.rs +++ b/src/base/component/button/component.rs @@ -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() { diff --git a/src/base/component/container.rs b/src/base/component/container.rs index 19ad829a..4fbce6c4 100644 --- a/src/base/component/container.rs +++ b/src/base/component/container.rs @@ -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); } diff --git a/src/base/component/dialog.rs b/src/base/component/dialog.rs index cdde8399..2a161930 100644 --- a/src/base/component/dialog.rs +++ b/src/base/component/dialog.rs @@ -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::(1))); self.alter_prop(PropsOp::prepend_classes("dialog")); diff --git a/src/base/component/dropdown/component.rs b/src/base/component/dropdown/component.rs index 3e9170cd..015fbd94 100644 --- a/src/base/component/dropdown/component.rs +++ b/src/base/component/dropdown/component.rs @@ -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")); } diff --git a/src/base/component/form/check.rs b/src/base/component/form/check.rs index 8697700b..485abbf7 100644 --- a/src/base/component/form/check.rs +++ b/src/base/component/form/check.rs @@ -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 diff --git a/src/base/component/form/checkbox.rs b/src/base/component/form/checkbox.rs index 25ee5500..8a2f1b0b 100644 --- a/src/base/component/form/checkbox.rs +++ b/src/base/component/form/checkbox.rs @@ -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 diff --git a/src/base/component/form/component.rs b/src/base/component/form/component.rs index d38ac554..240fa493 100644 --- a/src/base/component/form/component.rs +++ b/src/base/component/form/component.rs @@ -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")); } diff --git a/src/base/component/form/input.rs b/src/base/component/form/input.rs index 3a36755b..6a5862d8 100644 --- a/src/base/component/form/input.rs +++ b/src/base/component/form/input.rs @@ -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))) diff --git a/src/base/component/form/number.rs b/src/base/component/form/number.rs index 78dfe219..6edc5018 100644 --- a/src/base/component/form/number.rs +++ b/src/base/component/form/number.rs @@ -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))) diff --git a/src/base/component/form/radio.rs b/src/base/component/form/radio.rs index 821472ac..37fe43a4 100644 --- a/src/base/component/form/radio.rs +++ b/src/base/component/form/radio.rs @@ -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 diff --git a/src/base/component/form/range.rs b/src/base/component/form/range.rs index 75b5f242..136bc65a 100644 --- a/src/base/component/form/range.rs +++ b/src/base/component/form/range.rs @@ -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))) diff --git a/src/base/component/form/select.rs b/src/base/component/form/select.rs index 54ec2773..a89a4b39 100644 --- a/src/base/component/form/select.rs +++ b/src/base/component/form/select.rs @@ -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))) diff --git a/src/base/component/form/textarea.rs b/src/base/component/form/textarea.rs index f5c58b4b..e8db63f1 100644 --- a/src/base/component/form/textarea.rs +++ b/src/base/component/form/textarea.rs @@ -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))) diff --git a/src/base/component/image/component.rs b/src/base/component/image/component.rs index 649a783d..4cc0b4ad 100644 --- a/src/base/component/image/component.rs +++ b/src/base/component/image/component.rs @@ -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", diff --git a/src/base/component/messages.rs b/src/base/component/messages.rs index 827d5760..d7b40d28 100644 --- a/src/base/component/messages.rs +++ b/src/base/component/messages.rs @@ -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")); } diff --git a/src/base/component/nav/component.rs b/src/base/component/nav/component.rs index eb0b779a..11a56049 100644 --- a/src/base/component/nav/component.rs +++ b/src/base/component/nav/component.rs @@ -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", diff --git a/src/base/component/nav/item.rs b/src/base/component/nav/item.rs index c3744a5f..a747633c 100644 --- a/src/base/component/nav/item.rs +++ b/src/base/component/nav/item.rs @@ -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())); } diff --git a/src/base/component/navbar/component.rs b/src/base/component/navbar/component.rs index 7a8663d5..7ba5cb99 100644 --- a/src/base/component/navbar/component.rs +++ b/src/base/component/navbar/component.rs @@ -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::(1))); diff --git a/src/base/component/navbar/item.rs b/src/base/component/navbar/item.rs index 77acf01c..0161abc7 100644 --- a/src/base/component/navbar/item.rs +++ b/src/base/component/navbar/item.rs @@ -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() { diff --git a/src/base/component/pager.rs b/src/base/component/pager.rs index f4e143f3..1c43cf17 100644 --- a/src/base/component/pager.rs +++ b/src/base/component/pager.rs @@ -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.id(), 1); diff --git a/src/base/component/table/component.rs b/src/base/component/table/component.rs index a857f09e..ea7e5393 100644 --- a/src/base/component/table/component.rs +++ b/src/base/component/table/component.rs @@ -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")); } diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index e0c52207..d4bb14ff 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -89,24 +89,30 @@ 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. /// @@ -154,27 +160,33 @@ impl 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`](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`] 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`](crate::base::action::component::AfterRender) -/// para que las extensiones puedan reaccionar con sus últimos ajustes. -/// 6. Finalmente despacha -/// [`action::component::TransformMarkup`](crate::base::action::component::TransformMarkup) -/// para que las extensiones puedan trabajar sobre el HTML final para modificarlo antes de -/// devolverlo. +/// llamando a [`Theme::handle_component()`] en cada nivel hasta que uno devuelva `Some`. Si +/// ninguno lo sobrescribe, llama al [`Component::prepare()`] del propio componente. +/// 5. Despacha [`action::component::AfterRender`] para que las extensiones puedan reaccionar con +/// sus últimos ajustes. +/// 6. Finalmente despacha [`action::component::TransformMarkup`] 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`]: crate::base::action::component::BeforeRender +/// [`action::component::AfterRender`]: crate::base::action::component::AfterRender +/// [`action::component::TransformMarkup`]: crate::base::action::component::TransformMarkup +/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component #[async_trait] impl ComponentRender for C { async fn render(&mut self, cx: &mut Context) -> Markup { @@ -183,7 +195,7 @@ impl 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.