♻️ (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.
This commit is contained in:
Manuel Cillero 2026-09-06 09:04:44 +02:00
parent f2a7507317
commit 1e2d171805
31 changed files with 71 additions and 59 deletions

View file

@ -31,7 +31,7 @@ impl Component for Icon {
self.props.get_id() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
if !matches!(self.icon_kind(), IconKind::None) { if !matches!(self.icon_kind(), IconKind::None) {
self.alter_prop(PropsOp::prepend_classes("icon")); self.alter_prop(PropsOp::prepend_classes("icon"));
} }

View file

@ -71,7 +71,7 @@ impl Component for Offcanvas {
self.props.get_id() 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. // Asegura que el panel tiene un identificador único.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1))); self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));

View file

@ -33,7 +33,7 @@ impl Component for RoleTable {
self.props.get_id() 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::set_id("role-table-wrapper"));
self.alter_prop(PropsOp::prepend_classes("user-admin-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() 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::set_id("user-table-wrapper"));
self.alter_prop(PropsOp::prepend_classes("user-admin-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() 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!( self.alter_prop(PropsOp::prepend_classes(util::join!(
"badge badge-", "badge badge-",
self.intent().color(cx) self.intent().color(cx)

View file

@ -24,7 +24,7 @@ impl Component for Block {
self.props.get_id() 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. // Asegura que el bloque tiene un identificador único.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1))); 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() self.props.get_id()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes("brand")); self.alter_prop(PropsOp::prepend_classes("brand"));
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -43,7 +43,7 @@ impl Component for Nav {
self.props.get_id() 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() { self.alter_prop(PropsOp::prepend_classes(match self.nav_layout() {
nav::Layout::Default => "nav", nav::Layout::Default => "nav",
nav::Layout::Start => "nav nav-start", nav::Layout::Start => "nav nav-start",

View file

@ -77,7 +77,7 @@ impl Component for Item {
self.props.get_id() 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())); 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() 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 // 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`. // despliegue para referenciar el contenido colapsable con `aria-controls`.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1))); 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 if let Self::Nav(nav) = self
&& let Some(nav) = nav.get_mut() && let Some(nav) = nav.get_mut()
{ {

View file

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

View file

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

View file

@ -89,24 +89,30 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// Configura el estado interno del componente antes de generar el marcado. /// Configura el estado interno del componente antes de generar el marcado.
/// ///
/// Segundo paso del [ciclo de renderizado](ComponentRender): se ejecuta tras comprobar /// Segundo paso del [ciclo de renderizado](ComponentRender). Se ejecuta tras comprobar
/// [`is_renderable()`](Self::is_renderable) y antes de la acción /// [`is_renderable()`] y antes de la acción [`BeforeRender`] y de [`prepare()`]. Por defecto no
/// [`BeforeRender`](crate::base::action::component::BeforeRender) y de /// hace nada.
/// [`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 /// 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 /// 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; /// contexto, etc. Recibe `&mut Context` donde también puede aplicar ajustes que dependan del
/// es intencionadamente síncrono. /// 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 /// La frontera con [`prepare()`] es de **sincronía**, no de qué puede modificar cada uno. Los
/// `async` precisamente para ello. /// 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) /// [`is_renderable()`]: Self::is_renderable
/// (generación de HTML) es deliberada y no debe fusionarse. /// [`prepare()`]: Self::prepare
/// [`BeforeRender`]: crate::base::action::component::BeforeRender
#[allow(unused_variables)] #[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. /// Genera el marcado HTML del componente cuando ningún tema lo sobrescribe.
/// ///
@ -154,27 +160,33 @@ 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: /// El proceso de renderizado de cada componente sigue esta secuencia:
/// ///
/// 1. Ejecuta [`is_renderable()`](Component::is_renderable) para ver si puede renderizarse en el /// 1. Ejecuta [`is_renderable()`] para ver si puede renderizarse en el contexto actual. Si no es
/// contexto actual. Si no es así, devuelve un [`Markup`] vacío. /// así, devuelve un [`Markup`] vacío.
/// 2. Ejecuta [`setup()`](Component::setup) para que el componente /// 2. Ejecuta [`setup()`] para que el componente pueda ajustar su estado interno y, de forma
/// pueda ajustar su estructura interna. /// síncrona, el propio [`Context`].
/// 3. Despacha [`action::component::BeforeRender<C>`](crate::base::action::component::BeforeRender) /// 3. Despacha [`action::component::BeforeRender<C>`] para que las extensiones puedan hacer ajustes
/// para que las extensiones puedan hacer ajustes previos. /// previos.
/// 4. Prepara el renderizado del componente, recorre la cadena de temas (hijo > padre > abuelo...) /// 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 /// llamando a [`Theme::handle_component()`] en cada nivel hasta que uno devuelva `Some`. Si
/// nivel hasta que uno devuelva `Some`. Si ninguno lo sobrescribe, llama al /// ninguno lo sobrescribe, llama al [`Component::prepare()`] del propio componente.
/// [`Component::prepare()`](Component::prepare) del propio componente. /// 5. Despacha [`action::component::AfterRender<C>`] para que las extensiones puedan reaccionar con
/// 5. Despacha [`action::component::AfterRender<C>`](crate::base::action::component::AfterRender) /// sus últimos ajustes.
/// para que las extensiones puedan reaccionar con sus últimos ajustes. /// 6. Finalmente despacha [`action::component::TransformMarkup<C>`] para que las extensiones puedan
/// 6. Finalmente despacha /// trabajar sobre el HTML final para modificarlo antes de devolverlo.
/// [`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. /// 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::handle_component()`]: crate::core::theme::Theme::handle_component
#[async_trait] #[async_trait]
impl<C: Component> ComponentRender for C { impl<C: Component> ComponentRender for C {
async fn render(&mut self, cx: &mut Context) -> Markup { async fn render(&mut self, cx: &mut Context) -> Markup {
@ -183,7 +195,7 @@ impl<C: Component> ComponentRender for C {
return html! {}; return html! {};
} }
// Configura el componente antes de preparar. // Configura el componente (y, de forma síncrona, el contexto) antes de preparar.
self.setup(cx); self.setup(cx);
// Acciones de las extensiones antes de renderizar el componente. // Acciones de las extensiones antes de renderizar el componente.