Compare commits
No commits in common. "74dbcde5c5736dc06a51f5cdf3db5f10ffdf3a89" and "b9d9cdf6016738024d50b31321c8761bcd913e51" have entirely different histories.
74dbcde5c5
...
b9d9cdf601
8 changed files with 114 additions and 154 deletions
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
use pagetop_htmx::prelude::*;
|
use pagetop_htmx::HtmxRequestExt;
|
||||||
|
|
||||||
struct TestApp;
|
struct TestApp;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(&mut context).await;
|
||||||
/// child.render(context).await;
|
|
||||||
/// }
|
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
impl<T: Component + Clone + 'static> ComponentGlobal for T {
|
||||||
// Devuelve los prototipos registrados para la región indicada, si hay alguno.
|
#[inline]
|
||||||
fn get(&self, region_name: &str) -> Option<&Vec<Arc<dyn Component>>> {
|
fn as_child(&self) -> Child {
|
||||||
self.0
|
Child::with(self.clone())
|
||||||
.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.
|
|
||||||
//
|
|
||||||
// Se comparte como `Arc<dyn Component>`. El prototipo no se clona aquí ni al ensamblar la
|
|
||||||
// región (sólo se clona el `Arc`, barato). El único clonado real del componente ocurre en
|
|
||||||
// `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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
src/web.rs
20
src/web.rs
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue