Compare commits

..

No commits in common. "74dbcde5c5736dc06a51f5cdf3db5f10ffdf3a89" and "b9d9cdf6016738024d50b31321c8761bcd913e51" have entirely different histories.

8 changed files with 114 additions and 154 deletions

View file

@ -381,8 +381,7 @@ pub fn on_htmx(event: &str) -> String {
/// ///
/// Están en minúsculas porque así las normaliza el módulo `http`. Se pueden usar con /// Están en minúsculas porque así las normaliza el módulo `http`. Se pueden usar con
/// [`HttpRequest::headers()`](pagetop::web::HttpRequest::headers) para leer sus valores /// [`HttpRequest::headers()`](pagetop::web::HttpRequest::headers) para leer sus valores
/// directamente, aunque lo habitual es usar el trait /// directamente, aunque lo habitual es usar el trait [`HtmxRequestExt`](crate::HtmxRequestExt).
/// [`HtmxRequestExt`](crate::request::HtmxRequestExt).
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
@ -418,8 +417,7 @@ pub mod request {
/// Cabeceras de respuesta HTTP para HTMX. /// Cabeceras de respuesta HTTP para HTMX.
/// ///
/// Se pueden usar con [`HeaderMap`](pagetop::web::http::HeaderMap) para construir respuestas /// Se pueden usar con [`HeaderMap`](pagetop::web::http::HeaderMap) para construir respuestas
/// manualmente, aunque lo habitual es usar el constructor /// manualmente, aunque lo habitual es usar el constructor [`HtmxResponse`](crate::HtmxResponse).
/// [`HtmxResponse`](crate::response::HtmxResponse).
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
@ -433,38 +431,38 @@ pub mod request {
/// ``` /// ```
pub mod response { pub mod response {
/// Redirige mediante AJAX a la URL o configuración JSON indicada. Ver /// Redirige mediante AJAX a la URL o configuración JSON indicada. Ver
/// [`HtmxResponse::location()`](crate::response::HtmxResponse::location) y /// [`HtmxResponse::location()`](crate::HtmxResponse::location) y
/// [`HtmxResponse::location_json()`](crate::response::HtmxResponse::location_json). /// [`HtmxResponse::location_json()`](crate::HtmxResponse::location_json).
pub const LOCATION: &str = "HX-Location"; pub const LOCATION: &str = "HX-Location";
/// Empuja la URL indicada al historial del navegador. Ver /// Empuja la URL indicada al historial del navegador. Ver
/// [`HtmxResponse::push_url()`](crate::response::HtmxResponse::push_url). /// [`HtmxResponse::push_url()`](crate::HtmxResponse::push_url).
pub const PUSH_URL: &str = "HX-Push-Url"; pub const PUSH_URL: &str = "HX-Push-Url";
/// Provoca una redirección completa del navegador. Ver /// Provoca una redirección completa del navegador. Ver
/// [`HtmxResponse::redirect()`](crate::response::HtmxResponse::redirect). /// [`HtmxResponse::redirect()`](crate::HtmxResponse::redirect).
pub const REDIRECT: &str = "HX-Redirect"; pub const REDIRECT: &str = "HX-Redirect";
/// Provoca una recarga completa de la página. Ver /// Provoca una recarga completa de la página. Ver
/// [`HtmxResponse::refresh()`](crate::response::HtmxResponse::refresh). /// [`HtmxResponse::refresh()`](crate::HtmxResponse::refresh).
pub const REFRESH: &str = "HX-Refresh"; pub const REFRESH: &str = "HX-Refresh";
/// Reemplaza la URL actual en el historial. Ver /// Reemplaza la URL actual en el historial. Ver
/// [`HtmxResponse::replace_url()`](crate::response::HtmxResponse::replace_url). /// [`HtmxResponse::replace_url()`](crate::HtmxResponse::replace_url).
pub const REPLACE_URL: &str = "HX-Replace-Url"; pub const REPLACE_URL: &str = "HX-Replace-Url";
/// Anula el `hx-swap` del elemento. Ver /// Anula el `hx-swap` del elemento. Ver
/// [`HtmxResponse::reswap()`](crate::response::HtmxResponse::reswap). /// [`HtmxResponse::reswap()`](crate::HtmxResponse::reswap).
pub const RESWAP: &str = "HX-Reswap"; pub const RESWAP: &str = "HX-Reswap";
/// Anula el `hx-target` del elemento. Ver /// Anula el `hx-target` del elemento. Ver
/// [`HtmxResponse::retarget()`](crate::response::HtmxResponse::retarget). /// [`HtmxResponse::retarget()`](crate::HtmxResponse::retarget).
pub const RETARGET: &str = "HX-Retarget"; pub const RETARGET: &str = "HX-Retarget";
/// Anula el `hx-select` del elemento. Ver /// Anula el `hx-select` del elemento. Ver
/// [`HtmxResponse::reselect()`](crate::response::HtmxResponse::reselect). /// [`HtmxResponse::reselect()`](crate::HtmxResponse::reselect).
pub const RESELECT: &str = "HX-Reselect"; pub const RESELECT: &str = "HX-Reselect";
/// Dispara eventos JavaScript al completar la respuesta. Ver /// Dispara eventos JavaScript al completar la respuesta. Ver
/// [`HtmxResponse::trigger()`](crate::response::HtmxResponse::trigger). /// [`HtmxResponse::trigger()`](crate::HtmxResponse::trigger).
pub const TRIGGER: &str = "HX-Trigger"; pub const TRIGGER: &str = "HX-Trigger";
/// Dispara eventos tras la fase *settle*. Ver /// Dispara eventos tras la fase *settle*. Ver
/// [`HtmxResponse::trigger_after_settle()`](crate::response::HtmxResponse::trigger_after_settle). /// [`HtmxResponse::trigger_after_settle()`](crate::HtmxResponse::trigger_after_settle).
pub const TRIGGER_AFTER_SETTLE: &str = "HX-Trigger-After-Settle"; pub const TRIGGER_AFTER_SETTLE: &str = "HX-Trigger-After-Settle";
/// Dispara eventos tras el *swap*. Ver /// Dispara eventos tras el *swap*. Ver
/// [`HtmxResponse::trigger_after_swap()`](crate::response::HtmxResponse::trigger_after_swap). /// [`HtmxResponse::trigger_after_swap()`](crate::HtmxResponse::trigger_after_swap).
pub const TRIGGER_AFTER_SWAP: &str = "HX-Trigger-After-Swap"; pub const TRIGGER_AFTER_SWAP: &str = "HX-Trigger-After-Swap";
} }

View file

@ -93,13 +93,18 @@ include_locales!(LOCALES_HTMX);
pub mod hx; pub mod hx;
pub mod hx_table; pub mod hx_table;
pub mod request;
pub mod response; mod request;
pub use request::HtmxRequestExt;
mod response;
pub use response::HtmxResponse;
/// Prelude de `pagetop-htmx`. /// Prelude de `pagetop-htmx`.
pub mod prelude { pub mod prelude {
pub use crate::hx; pub use crate::hx;
pub use crate::hx_table; pub use crate::hx_table;
pub use crate::request::HtmxRequestExt; pub use crate::request::HtmxRequestExt;
pub use crate::response::HtmxResponse; pub use crate::response::HtmxResponse;
} }

View file

@ -4,7 +4,8 @@ use pagetop::prelude::*;
// **< HtmxRequestExt >***************************************************************************** // **< HtmxRequestExt >*****************************************************************************
/// Extiende [`HttpRequest`] con métodos para detectar y leer peticiones HTMX. /// Extiende [`HttpRequest`](pagetop::web::HttpRequest) con métodos para detectar y leer peticiones
/// HTMX.
/// ///
/// HTMX añade cabeceras especiales a cada petición AJAX. Este trait permite acceder a ellas de /// HTMX añade cabeceras especiales a cada petición AJAX. Este trait permite acceder a ellas de
/// forma expresiva, sin manipular [`pagetop::web::http::HeaderMap`] directamente. /// forma expresiva, sin manipular [`pagetop::web::http::HeaderMap`] directamente.

View file

@ -1,4 +1,4 @@
//! Implementación de [`HtmxResponse`] e [`IntoResponse`] para HTMX. //! Implementación de [`HtmxResponse`] e [`IntoResponse`](pagetop::web::IntoResponse) para HTMX.
use pagetop::prelude::*; use pagetop::prelude::*;
@ -10,7 +10,8 @@ use pagetop::prelude::*;
/// parciales acompañados de cabeceras especiales que instruyen al cliente sobre qué hacer con la /// parciales acompañados de cabeceras especiales que instruyen al cliente sobre qué hacer con la
/// respuesta: actualizar la URL del historial, disparar eventos JavaScript, redirigir, etc. /// respuesta: actualizar la URL del historial, disparar eventos JavaScript, redirigir, etc.
/// ///
/// Implementa [`IntoResponse`], por lo que puede devolverse directamente desde cualquier handler. /// Implementa [`IntoResponse`](pagetop::web::IntoResponse), por lo que puede devolverse
/// directamente desde cualquier handler.
/// ///
/// # Ejemplo /// # Ejemplo
/// ///

View file

@ -1,5 +1,5 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use pagetop_htmx::prelude::*; use pagetop_htmx::HtmxRequestExt;
struct TestApp; struct TestApp;

View file

@ -2,6 +2,8 @@ use crate::core::component::{Component, Context};
use crate::html::{Markup, html}; use crate::html::{Markup, html};
use crate::{AutoDefault, UniqueId, builder_fn}; use crate::{AutoDefault, UniqueId, builder_fn};
use parking_lot::RwLock;
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
use std::vec::IntoIter; use std::vec::IntoIter;
@ -9,19 +11,14 @@ use std::vec::IntoIter;
// **< Child >************************************************************************************** // **< Child >**************************************************************************************
/// Representa un componente hijo encapsulado para su uso en una lista [`Children`]. /// Representa un componente hijo encapsulado para su uso en una lista [`Children`].
///
/// Envuelve el componente en `Arc<dyn Component>`, compartido y de sólo lectura. Clonar un `Child`
/// sólo incrementa el contador de referencias. Para renderizar obtiene una copia propia con
/// [`ComponentClone::clone_box()`](crate::core::component::ComponentClone::clone_box), de modo que
/// el componente original nunca se modifica.
#[derive(AutoDefault, Clone)] #[derive(AutoDefault, Clone)]
pub struct Child(Option<Arc<dyn Component>>); pub struct Child(Option<Arc<RwLock<Box<dyn Component>>>>);
impl fmt::Debug for Child { impl fmt::Debug for Child {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 { match &self.0 {
None => write!(f, "Child(None)"), None => write!(f, "Child(None)"),
Some(c) => write!(f, "Child({})", c.name()), Some(c) => write!(f, "Child({})", c.read().name()),
} }
} }
} }
@ -29,14 +26,7 @@ impl fmt::Debug for Child {
impl Child { impl Child {
/// Crea un nuevo `Child` a partir de un componente. /// Crea un nuevo `Child` a partir de un componente.
pub fn with(component: impl Component) -> Self { pub fn with(component: impl Component) -> Self {
Child(Some(Arc::new(component))) Child(Some(Arc::new(RwLock::new(Box::new(component)))))
}
// Envuelve un `Arc` ya construido, sin clonar el componente. La usa `core::theme::regions` para
// registrar los prototipos de `InRegion` sin clonar su estado hasta que `render()` obtenga la
// copia propia que necesita mutar.
pub(crate) fn from_arc(component: Arc<dyn Component>) -> Self {
Child(Some(component))
} }
// **< Child BUILDER >************************************************************************** // **< Child BUILDER >**************************************************************************
@ -46,7 +36,7 @@ impl Child {
/// Si se proporciona `Some(component)`, se encapsula como [`Child`]; y si es `None`, se limpia. /// Si se proporciona `Some(component)`, se encapsula como [`Child`]; y si es `None`, se limpia.
#[builder_fn] #[builder_fn]
pub fn with_component<C: Component>(mut self, component: Option<C>) -> Self { pub fn with_component<C: Component>(mut self, component: Option<C>) -> Self {
self.0 = component.map(|c| Arc::new(c) as Arc<dyn Component>); self.0 = component.map(|c| Arc::new(RwLock::new(Box::new(c) as Box<dyn Component>)));
self self
} }
@ -55,7 +45,7 @@ impl Child {
/// Devuelve el identificador del componente, si existe y está definido. /// Devuelve el identificador del componente, si existe y está definido.
#[inline] #[inline]
pub fn id(&self) -> Option<String> { pub fn id(&self) -> Option<String> {
self.0.as_ref().and_then(|c| c.id()) self.0.as_ref().and_then(|c| c.read().id())
} }
// **< Child RENDER >*************************************************************************** // **< Child RENDER >***************************************************************************
@ -65,7 +55,7 @@ impl Child {
match &self.0 { match &self.0 {
None => html! {}, None => html! {},
Some(m) => { Some(m) => {
let mut component = m.clone_box(); let mut component = m.read().clone_box();
component.render(cx).await component.render(cx).await
} }
} }
@ -76,39 +66,39 @@ impl Child {
// Devuelve el [`UniqueId`] del tipo del componente, si el Child no está vacío. // Devuelve el [`UniqueId`] del tipo del componente, si el Child no está vacío.
#[inline] #[inline]
fn type_id(&self) -> Option<UniqueId> { fn type_id(&self) -> Option<UniqueId> {
self.0.as_ref().map(|c| c.type_id()) self.0.as_ref().map(|c| c.read().type_id())
} }
} }
impl<C: Component> From<Embed<C>> for Child { impl<C: Component + 'static> From<Embed<C>> for Child {
/// Convierte un [`Embed<C>`] en un [`Child`], consumiendo el componente tipado. /// Convierte un [`Embed<C>`] en un [`Child`], consumiendo el componente tipado.
/// ///
/// Útil cuando se tiene un [`Embed`] para añadir a una lista [`Children`]: /// Útil cuando se tiene un [`Embed`] y se necesita añadirlo a una lista [`Children`]:
/// ///
/// ```rust,no_run /// ```rust,ignore
/// # use pagetop::prelude::*; /// children.with_child(Child::from(my_embed));
/// let my_embed = Embed::with(Html::with(|_| html! { "Text" })); /// // o equivalentemente:
/// let children = Children::new().with_child(Child::from(my_embed)); /// children.with_child(my_embed.into());
///
/// // De forma equivalente se puede usar la conversión implícita hacia `Child`:
/// let my_embed = Embed::with(Html::with(|_| html! { "Text" }));
/// let child: Child = my_embed.into();
/// let children = children.with_child(child);
/// ``` /// ```
fn from(embed: Embed<C>) -> Self { fn from(embed: Embed<C>) -> Self {
Child(embed.0.map(|arc| arc as Arc<dyn Component>)) match embed.0 {
None => Child(None),
Some(arc) => Child(Some(Arc::new(RwLock::new(match Arc::try_unwrap(arc) {
Ok(c) => Box::new(c) as Box<dyn Component>,
Err(arc) => arc.clone_box(),
})))),
}
} }
} }
impl<T: Component> From<T> for Child { impl<T: Component + 'static> From<T> for Child {
/// Convierte cualquier componente en un [`Child`], equivalente a [`Child::with()`].
#[inline] #[inline]
fn from(component: T) -> Self { fn from(component: T) -> Self {
Child::with(component) Child::with(component)
} }
} }
impl<T: Component> From<T> for ChildOp { impl<T: Component + 'static> From<T> for ChildOp {
/// Convierte un componente en [`ChildOp::Add`], permitiendo pasar componentes directamente a /// Convierte un componente en [`ChildOp::Add`], permitiendo pasar componentes directamente a
/// métodos como [`Children::with_child`] sin envolverlos explícitamente. /// métodos como [`Children::with_child`] sin envolverlos explícitamente.
#[inline] #[inline]
@ -280,13 +270,12 @@ pub enum ChildOp {
/// Gracias a esto, [`with_child`](Self::with_child) acepta un componente directamente o cualquier /// Gracias a esto, [`with_child`](Self::with_child) acepta un componente directamente o cualquier
/// variante de [`ChildOp`]: /// variante de [`ChildOp`]:
/// ///
/// ```rust,no_run /// ```rust,ignore
/// # use pagetop::prelude::*;
/// // Añadir al final de la lista (implícito): /// // Añadir al final de la lista (implícito):
/// let children = Children::new().with_child(Html::new()); /// children.with_child(MiComponente::new());
/// ///
/// // Operación explícita: /// // Operación explícita:
/// let children = children.with_child(ChildOp::Prepend(Html::new().into())); /// children.with_child(ChildOp::Prepend(MiComponente::new().into()));
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]
pub struct Children(Vec<Child>); pub struct Children(Vec<Child>);
@ -381,12 +370,8 @@ impl Children {
// **< Children HELPERS >*********************************************************************** // **< Children HELPERS >***********************************************************************
// Añade más de un componente hijo al final de la lista (en el orden recibido). // Añade más de un componente hijo al final de la lista (en el orden recibido).
//
// Usa `Vec::extend()`, que reserva la capacidad necesaria de una vez a partir del `size_hint()`
// del iterador, en vez de una reasignación incremental por cada `push()`. También lo usa
// `core::theme::regions` para fusionar las fuentes de una región.
#[inline] #[inline]
pub(crate) fn add_many<I>(&mut self, iter: I) -> &mut Self fn add_many<I>(&mut self, iter: I) -> &mut Self
where where
I: IntoIterator<Item = Child>, I: IntoIterator<Item = Child>,
{ {
@ -473,9 +458,8 @@ impl IntoIterator for Children {
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,no_run /// ```rust,ignore
/// # use pagetop::prelude::*; /// let children = Children::new().with_child(child1).with_child(child2);
/// let children = Children::new().with_child(Html::new()).with_child(Html::new());
/// for child in children { /// for child in children {
/// println!("{:?}", child.id()); /// println!("{:?}", child.id());
/// } /// }
@ -493,9 +477,8 @@ impl<'a> IntoIterator for &'a Children {
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,no_run /// ```rust,ignore
/// # use pagetop::prelude::*; /// let children = Children::new().with_child(child1).with_child(child2);
/// let children = Children::new().with_child(Html::new()).with_child(Html::new());
/// for child in &children { /// for child in &children {
/// println!("{:?}", child.id()); /// println!("{:?}", child.id());
/// } /// }
@ -513,12 +496,10 @@ impl<'a> IntoIterator for &'a mut Children {
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,no_run /// ```rust,ignore
/// # use pagetop::prelude::*; /// let mut children = Children::new().with_child(child1).with_child(child2);
/// async fn render_all(mut children: Children, context: &mut Context) {
/// for child in &mut children { /// for child in &mut children {
/// child.render(context).await; /// child.render(&mut context).await;
/// }
/// } /// }
/// ``` /// ```
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {

View file

@ -7,53 +7,40 @@ use parking_lot::RwLock;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
// Lista de prototipos de componentes por nombre de región. // Permite almacenar un componente como prototipo en regiones globales.
// //
// Utiliza Vec en lugar de HashMap. El número de regiones registradas por tema o aplicación es casi // Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. En cada
// siempre de un dígito, así que una búsqueda lineal por igualdad de `&str` evita el coste de // llamada a [`as_child`](Self::as_child) produce un clon fresco del estado original, de modo que
// hashear la clave. Además, el trabajo para recorrer regiones vacías es mínimo. // cada página renderiza el componente desde su estado inicial sin acumular mutaciones de peticiones
// // anteriores.
// La clave es `&'static str` (lo que ya devuelve `RegionName::name()`) en lugar de `String`. No trait ComponentGlobal: Send + Sync {
// hace falta reservar en el heap una copia de un dato que ya vive de forma estática. // Devuelve un nuevo [`Child`] con una copia independiente del componente original.
#[derive(AutoDefault)] fn as_child(&self) -> Child;
struct RegionComponents(Vec<(&'static str, Vec<Arc<dyn Component>>)>);
impl RegionComponents {
// Devuelve los prototipos registrados para la región indicada, si hay alguno.
fn get(&self, region_name: &str) -> Option<&Vec<Arc<dyn Component>>> {
self.0
.iter()
.find(|(name, _)| *name == region_name)
.map(|(_, protos)| protos)
} }
// Añade un prototipo a la región indicada, creando la entrada si es la primera. impl<T: Component + Clone + 'static> ComponentGlobal for T {
// #[inline]
// Se comparte como `Arc<dyn Component>`. El prototipo no se clona aquí ni al ensamblar la fn as_child(&self) -> Child {
// región (sólo se clona el `Arc`, barato). El único clonado real del componente ocurre en Child::with(self.clone())
// `Child::render()`, cuando cada petición necesita su propia copia mutable para pasar por
// `setup()` desde un estado inicial limpio.
fn push(&mut self, region_name: &'static str, proto: Arc<dyn Component>) {
match self.0.iter_mut().find(|(name, _)| *name == region_name) {
Some((_, protos)) => protos.push(proto),
None => self.0.push((region_name, vec![proto])),
}
} }
} }
// Mapa de nombre de región a lista de prototipos de componentes.
type RegionComponents = HashMap<String, Vec<Arc<dyn ComponentGlobal>>>;
// Regiones globales con prototipos asociados a un tema específico. // Regiones globales con prototipos asociados a un tema específico.
static THEME_REGIONS: LazyLock<RwLock<HashMap<UniqueId, RegionComponents>>> = static THEME_REGIONS: LazyLock<RwLock<HashMap<UniqueId, RegionComponents>>> =
LazyLock::new(|| RwLock::new(HashMap::new())); LazyLock::new(|| RwLock::new(HashMap::new()));
// Regiones globales con prototipos comunes a todos los temas. // Regiones globales con prototipos comunes a todos los temas.
static COMMON_REGIONS: LazyLock<RwLock<RegionComponents>> = static COMMON_REGIONS: LazyLock<RwLock<RegionComponents>> =
LazyLock::new(|| RwLock::new(RegionComponents::default())); LazyLock::new(|| RwLock::new(HashMap::new()));
// ************************************************************************************************* // *************************************************************************************************
// Contenedor interno de componentes agrupados por región. // Contenedor interno de componentes agrupados por región.
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub(crate) struct ChildrenInRegions(HashMap<&'static str, Children>); pub(crate) struct ChildrenInRegions(HashMap<String, Children>);
impl ChildrenInRegions { impl ChildrenInRegions {
pub fn with(region: RegionRef, child: Child) -> Self { pub fn with(region: RegionRef, child: Child) -> Self {
@ -68,7 +55,7 @@ impl ChildrenInRegions {
region.alter_child(child); region.alter_child(child);
} else { } else {
let children = Children::new().with_child(child); let children = Children::new().with_child(child);
self.0.insert(region_name, children); self.0.insert(region_name.to_owned(), children);
} }
self self
} }
@ -77,39 +64,38 @@ impl ChildrenInRegions {
/// ///
/// Se recogen desde tres fuentes disponibles, en el siguiente orden: /// Se recogen desde tres fuentes disponibles, en el siguiente orden:
/// ///
/// 1. Prototipos globales comunes, disponibles en cualquier tema. Se comparten como `Arc` (sin /// 1. Prototipos globales comunes, disponibles en cualquier tema. Se clonan en cada petición
/// clonar el componente); `Child::render()` obtiene su propia copia mutable más adelante,
/// para que `setup()` parta siempre de un estado inicial limpio. /// para que `setup()` parta siempre de un estado inicial limpio.
/// 2. Componentes propios de la página, registrados para esta petición concreta. Se mueven en /// 2. Componentes propios de la página, registrados para esta petición concreta. Se mueven en
/// lugar de clonarse, ya que son de un único uso. /// lugar de clonarse, ya que son de un único uso.
/// 3. Prototipos del tema activo, exclusivos del tema en curso. Se comparten igual que los /// 3. Prototipos del tema activo, exclusivos del tema en curso. También se clonan para asegurar
/// comunes. /// que llegan a `setup()` con el mismo estado inicial.
pub fn assemble_region(&mut self, theme: ThemeRef, region: RegionRef) -> Children { pub fn assemble_region(&mut self, theme: ThemeRef, region: RegionRef) -> Children {
let region_name = region.name();
let common = COMMON_REGIONS.read();
let themed = THEME_REGIONS.read();
let mut result = Children::new(); let mut result = Children::new();
let region_name = region.name();
// 1. Prototipos globales comunes. // 1. Prototipos globales comunes.
if let Some(global_protos) = COMMON_REGIONS.read().get(region_name) { if let Some(protos) = common.get(region_name) {
result.add_many( for proto in protos {
global_protos result.add(proto.as_child());
.iter() }
.map(|proto| Child::from_arc(Arc::clone(proto))),
);
} }
// 2. Componentes propios de la página: se mueven, no se clonan. // 2. Componentes propios de la página: se mueven, no se clonan.
if let Some(page_children) = self.0.remove(region_name) { if let Some(page_children) = self.0.remove(region_name) {
result.add_many(page_children); for child in page_children {
result.add(child);
}
} }
// 3. Prototipos del tema activo. // 3. Prototipos del tema activo.
if let Some(theme_region) = THEME_REGIONS.read().get(&theme.type_id()) if let Some(theme_map) = themed.get(&theme.type_id()) {
&& let Some(theme_protos) = theme_region.get(region_name) if let Some(protos) = theme_map.get(region_name) {
{ for proto in protos {
result.add_many( result.add(proto.as_child());
theme_protos }
.iter() }
.map(|proto| Child::from_arc(Arc::clone(proto))),
);
} }
result result
@ -182,8 +168,8 @@ impl InRegion {
/// html! { "Aviso legal" } /// html! { "Aviso legal" }
/// })); /// }));
/// ``` /// ```
pub fn add(&self, component: impl Component) -> &Self { pub fn add(&self, component: impl Component + Clone + 'static) -> &Self {
let proto: Arc<dyn Component> = Arc::new(component); let proto: Arc<dyn ComponentGlobal> = Arc::new(component);
match self { match self {
InRegion::Content => Self::add_to_common(&CoreRegion::Content, proto), InRegion::Content => Self::add_to_common(&CoreRegion::Content, proto),
InRegion::Global(region) => Self::add_to_common(*region, proto), InRegion::Global(region) => Self::add_to_common(*region, proto),
@ -192,14 +178,20 @@ impl InRegion {
.write() .write()
.entry(theme.type_id()) .entry(theme.type_id())
.or_default() .or_default()
.push((*region).name(), proto); .entry((*region).name().to_owned())
.or_default()
.push(proto);
} }
} }
self self
} }
#[inline] #[inline]
fn add_to_common(region: RegionRef, proto: Arc<dyn Component>) { fn add_to_common(region: RegionRef, proto: Arc<dyn ComponentGlobal>) {
COMMON_REGIONS.write().push(region.name(), proto); COMMON_REGIONS
.write()
.entry(region.name().to_owned())
.or_default()
.push(proto);
} }
} }

View file

@ -347,7 +347,6 @@ pub mod test {
pub struct TestRequest { pub struct TestRequest {
method: http::Method, method: http::Method,
uri: String, uri: String,
headers: http::HeaderMap,
extensions: http::Extensions, extensions: http::Extensions,
} }
@ -357,7 +356,6 @@ pub mod test {
Self { Self {
method: http::Method::GET, method: http::Method::GET,
uri: "/".to_owned(), uri: "/".to_owned(),
headers: http::HeaderMap::new(),
extensions: http::Extensions::new(), extensions: http::Extensions::new(),
} }
} }
@ -367,7 +365,6 @@ pub mod test {
Self { Self {
method: http::Method::POST, method: http::Method::POST,
uri: "/".to_owned(), uri: "/".to_owned(),
headers: http::HeaderMap::new(),
extensions: http::Extensions::new(), extensions: http::Extensions::new(),
} }
} }
@ -378,20 +375,6 @@ pub mod test {
self self
} }
/// Añade una cabecera a la petición.
///
/// Si `name` o `value` no son válidos como cabecera HTTP, se descarta en silencio en lugar
/// de entrar en pánico.
pub fn header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
if let (Ok(n), Ok(v)) = (
http::HeaderName::from_bytes(name.as_ref().as_bytes()),
http::HeaderValue::from_str(value.as_ref()),
) {
self.headers.insert(n, v);
}
self
}
/// Inserta un valor en las extensiones de la petición. /// Inserta un valor en las extensiones de la petición.
/// ///
/// Útil para simular lo que un middleware haría en producción antes de que el handler /// Útil para simular lo que un middleware haría en producción antes de que el handler
@ -408,7 +391,6 @@ pub mod test {
.uri(self.uri) .uri(self.uri)
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
*req.headers_mut() = self.headers;
*req.extensions_mut() = self.extensions; *req.extensions_mut() = self.extensions;
req req
} }
@ -419,7 +401,7 @@ pub mod test {
let uri = self.uri.parse().unwrap(); let uri = self.uri.parse().unwrap();
super::HttpRequest { super::HttpRequest {
uri, uri,
headers: self.headers, headers: axum::http::HeaderMap::new(),
extensions: std::sync::Arc::new(self.extensions), extensions: std::sync::Arc::new(self.extensions),
} }
} }