Compare commits
6 commits
b9d9cdf601
...
74dbcde5c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 74dbcde5c5 | |||
| 7e6a9f8557 | |||
| 845d91d8c1 | |||
| ce6b4d2581 | |||
| 65c8a788c2 | |||
| 39f494539e |
8 changed files with 154 additions and 114 deletions
|
|
@ -381,7 +381,8 @@ 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 [`HtmxRequestExt`](crate::HtmxRequestExt).
|
/// directamente, aunque lo habitual es usar el trait
|
||||||
|
/// [`HtmxRequestExt`](crate::request::HtmxRequestExt).
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
|
|
@ -417,7 +418,8 @@ 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 [`HtmxResponse`](crate::HtmxResponse).
|
/// manualmente, aunque lo habitual es usar el constructor
|
||||||
|
/// [`HtmxResponse`](crate::response::HtmxResponse).
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
|
|
@ -431,38 +433,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::HtmxResponse::location) y
|
/// [`HtmxResponse::location()`](crate::response::HtmxResponse::location) y
|
||||||
/// [`HtmxResponse::location_json()`](crate::HtmxResponse::location_json).
|
/// [`HtmxResponse::location_json()`](crate::response::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::HtmxResponse::push_url).
|
/// [`HtmxResponse::push_url()`](crate::response::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::HtmxResponse::redirect).
|
/// [`HtmxResponse::redirect()`](crate::response::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::HtmxResponse::refresh).
|
/// [`HtmxResponse::refresh()`](crate::response::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::HtmxResponse::replace_url).
|
/// [`HtmxResponse::replace_url()`](crate::response::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::HtmxResponse::reswap).
|
/// [`HtmxResponse::reswap()`](crate::response::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::HtmxResponse::retarget).
|
/// [`HtmxResponse::retarget()`](crate::response::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::HtmxResponse::reselect).
|
/// [`HtmxResponse::reselect()`](crate::response::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::HtmxResponse::trigger).
|
/// [`HtmxResponse::trigger()`](crate::response::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::HtmxResponse::trigger_after_settle).
|
/// [`HtmxResponse::trigger_after_settle()`](crate::response::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::HtmxResponse::trigger_after_swap).
|
/// [`HtmxResponse::trigger_after_swap()`](crate::response::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,18 +93,13 @@ include_locales!(LOCALES_HTMX);
|
||||||
|
|
||||||
pub mod hx;
|
pub mod hx;
|
||||||
pub mod hx_table;
|
pub mod hx_table;
|
||||||
|
pub mod request;
|
||||||
mod request;
|
pub mod response;
|
||||||
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,8 +4,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
// **< HtmxRequestExt >*****************************************************************************
|
// **< HtmxRequestExt >*****************************************************************************
|
||||||
|
|
||||||
/// Extiende [`HttpRequest`](pagetop::web::HttpRequest) con métodos para detectar y leer peticiones
|
/// Extiende [`HttpRequest`] con métodos para detectar y leer peticiones HTMX.
|
||||||
/// 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`](pagetop::web::IntoResponse) para HTMX.
|
//! Implementación de [`HtmxResponse`] e [`IntoResponse`] para HTMX.
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
|
@ -10,8 +10,7 @@ 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`](pagetop::web::IntoResponse), por lo que puede devolverse
|
/// Implementa [`IntoResponse`], por lo que puede devolverse directamente desde cualquier handler.
|
||||||
/// directamente desde cualquier handler.
|
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
use pagetop_htmx::HtmxRequestExt;
|
use pagetop_htmx::prelude::*;
|
||||||
|
|
||||||
struct TestApp;
|
struct TestApp;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ 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;
|
||||||
|
|
@ -11,14 +9,19 @@ 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<RwLock<Box<dyn Component>>>>);
|
pub struct Child(Option<Arc<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.read().name()),
|
Some(c) => write!(f, "Child({})", c.name()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +29,14 @@ 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(RwLock::new(Box::new(component)))))
|
Child(Some(Arc::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 >**************************************************************************
|
||||||
|
|
@ -36,7 +46,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(RwLock::new(Box::new(c) as Box<dyn Component>)));
|
self.0 = component.map(|c| Arc::new(c) as Arc<dyn Component>);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +55,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.read().id())
|
self.0.as_ref().and_then(|c| c.id())
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Child RENDER >***************************************************************************
|
// **< Child RENDER >***************************************************************************
|
||||||
|
|
@ -55,7 +65,7 @@ impl Child {
|
||||||
match &self.0 {
|
match &self.0 {
|
||||||
None => html! {},
|
None => html! {},
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
let mut component = m.read().clone_box();
|
let mut component = m.clone_box();
|
||||||
component.render(cx).await
|
component.render(cx).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,39 +76,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.read().type_id())
|
self.0.as_ref().map(|c| c.type_id())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Component + 'static> From<Embed<C>> for Child {
|
impl<C: Component> 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`] y se necesita añadirlo a una lista [`Children`]:
|
/// Útil cuando se tiene un [`Embed`] para añadir a una lista [`Children`]:
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,no_run
|
||||||
/// children.with_child(Child::from(my_embed));
|
/// # use pagetop::prelude::*;
|
||||||
/// // o equivalentemente:
|
/// let my_embed = Embed::with(Html::with(|_| html! { "Text" }));
|
||||||
/// children.with_child(my_embed.into());
|
/// let children = Children::new().with_child(Child::from(my_embed));
|
||||||
|
///
|
||||||
|
/// // 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 {
|
||||||
match embed.0 {
|
Child(embed.0.map(|arc| arc as Arc<dyn Component>))
|
||||||
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 + 'static> From<T> for Child {
|
impl<T: Component> 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 + 'static> From<T> for ChildOp {
|
impl<T: Component> 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]
|
||||||
|
|
@ -270,12 +280,13 @@ 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,ignore
|
/// ```rust,no_run
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
/// // Añadir al final de la lista (implícito):
|
/// // Añadir al final de la lista (implícito):
|
||||||
/// children.with_child(MiComponente::new());
|
/// let children = Children::new().with_child(Html::new());
|
||||||
///
|
///
|
||||||
/// // Operación explícita:
|
/// // Operación explícita:
|
||||||
/// children.with_child(ChildOp::Prepend(MiComponente::new().into()));
|
/// let children = children.with_child(ChildOp::Prepend(Html::new().into()));
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
pub struct Children(Vec<Child>);
|
pub struct Children(Vec<Child>);
|
||||||
|
|
@ -370,8 +381,12 @@ 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]
|
||||||
fn add_many<I>(&mut self, iter: I) -> &mut Self
|
pub(crate) fn add_many<I>(&mut self, iter: I) -> &mut Self
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = Child>,
|
I: IntoIterator<Item = Child>,
|
||||||
{
|
{
|
||||||
|
|
@ -458,8 +473,9 @@ impl IntoIterator for Children {
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,no_run
|
||||||
/// let children = Children::new().with_child(child1).with_child(child2);
|
/// # use pagetop::prelude::*;
|
||||||
|
/// 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());
|
||||||
/// }
|
/// }
|
||||||
|
|
@ -477,8 +493,9 @@ impl<'a> IntoIterator for &'a Children {
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,no_run
|
||||||
/// let children = Children::new().with_child(child1).with_child(child2);
|
/// # use pagetop::prelude::*;
|
||||||
|
/// 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());
|
||||||
/// }
|
/// }
|
||||||
|
|
@ -496,10 +513,12 @@ impl<'a> IntoIterator for &'a mut Children {
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,no_run
|
||||||
/// let mut children = Children::new().with_child(child1).with_child(child2);
|
/// # use pagetop::prelude::*;
|
||||||
|
/// 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,26 +7,39 @@ use parking_lot::RwLock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
// Permite almacenar un componente como prototipo en regiones globales.
|
// Lista de prototipos de componentes por nombre de región.
|
||||||
//
|
//
|
||||||
// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. En cada
|
// Utiliza Vec en lugar de HashMap. El número de regiones registradas por tema o aplicación es casi
|
||||||
// llamada a [`as_child`](Self::as_child) produce un clon fresco del estado original, de modo que
|
// siempre de un dígito, así que una búsqueda lineal por igualdad de `&str` evita el coste de
|
||||||
// cada página renderiza el componente desde su estado inicial sin acumular mutaciones de peticiones
|
// hashear la clave. Además, el trabajo para recorrer regiones vacías es mínimo.
|
||||||
// anteriores.
|
//
|
||||||
trait ComponentGlobal: Send + Sync {
|
// La clave es `&'static str` (lo que ya devuelve `RegionName::name()`) en lugar de `String`. No
|
||||||
// Devuelve un nuevo [`Child`] con una copia independiente del componente original.
|
// hace falta reservar en el heap una copia de un dato que ya vive de forma estática.
|
||||||
fn as_child(&self) -> Child;
|
#[derive(AutoDefault)]
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Component + Clone + 'static> ComponentGlobal for T {
|
// Añade un prototipo a la región indicada, creando la entrada si es la primera.
|
||||||
#[inline]
|
//
|
||||||
fn as_child(&self) -> Child {
|
// Se comparte como `Arc<dyn Component>`. El prototipo no se clona aquí ni al ensamblar la
|
||||||
Child::with(self.clone())
|
// 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>>> =
|
||||||
|
|
@ -34,13 +47,13 @@ static THEME_REGIONS: LazyLock<RwLock<HashMap<UniqueId, RegionComponents>>> =
|
||||||
|
|
||||||
// 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(HashMap::new()));
|
LazyLock::new(|| RwLock::new(RegionComponents::default()));
|
||||||
|
|
||||||
// *************************************************************************************************
|
// *************************************************************************************************
|
||||||
|
|
||||||
// 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<String, Children>);
|
pub(crate) struct ChildrenInRegions(HashMap<&'static str, Children>);
|
||||||
|
|
||||||
impl ChildrenInRegions {
|
impl ChildrenInRegions {
|
||||||
pub fn with(region: RegionRef, child: Child) -> Self {
|
pub fn with(region: RegionRef, child: Child) -> Self {
|
||||||
|
|
@ -55,7 +68,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.to_owned(), children);
|
self.0.insert(region_name, children);
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -64,38 +77,39 @@ 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 clonan en cada petición
|
/// 1. Prototipos globales comunes, disponibles en cualquier tema. Se comparten como `Arc` (sin
|
||||||
|
/// 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. También se clonan para asegurar
|
/// 3. Prototipos del tema activo, exclusivos del tema en curso. Se comparten igual que los
|
||||||
/// que llegan a `setup()` con el mismo estado inicial.
|
/// comunes.
|
||||||
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(protos) = common.get(region_name) {
|
if let Some(global_protos) = COMMON_REGIONS.read().get(region_name) {
|
||||||
for proto in protos {
|
result.add_many(
|
||||||
result.add(proto.as_child());
|
global_protos
|
||||||
}
|
.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) {
|
||||||
for child in page_children {
|
result.add_many(page_children);
|
||||||
result.add(child);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 3. Prototipos del tema activo.
|
// 3. Prototipos del tema activo.
|
||||||
if let Some(theme_map) = themed.get(&theme.type_id()) {
|
if let Some(theme_region) = THEME_REGIONS.read().get(&theme.type_id())
|
||||||
if let Some(protos) = theme_map.get(region_name) {
|
&& let Some(theme_protos) = theme_region.get(region_name)
|
||||||
for proto in protos {
|
{
|
||||||
result.add(proto.as_child());
|
result.add_many(
|
||||||
}
|
theme_protos
|
||||||
}
|
.iter()
|
||||||
|
.map(|proto| Child::from_arc(Arc::clone(proto))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
|
|
@ -168,8 +182,8 @@ impl InRegion {
|
||||||
/// html! { "Aviso legal" }
|
/// html! { "Aviso legal" }
|
||||||
/// }));
|
/// }));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn add(&self, component: impl Component + Clone + 'static) -> &Self {
|
pub fn add(&self, component: impl Component) -> &Self {
|
||||||
let proto: Arc<dyn ComponentGlobal> = Arc::new(component);
|
let proto: Arc<dyn Component> = 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),
|
||||||
|
|
@ -178,20 +192,14 @@ impl InRegion {
|
||||||
.write()
|
.write()
|
||||||
.entry(theme.type_id())
|
.entry(theme.type_id())
|
||||||
.or_default()
|
.or_default()
|
||||||
.entry((*region).name().to_owned())
|
.push((*region).name(), proto);
|
||||||
.or_default()
|
|
||||||
.push(proto);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn add_to_common(region: RegionRef, proto: Arc<dyn ComponentGlobal>) {
|
fn add_to_common(region: RegionRef, proto: Arc<dyn Component>) {
|
||||||
COMMON_REGIONS
|
COMMON_REGIONS.write().push(region.name(), proto);
|
||||||
.write()
|
|
||||||
.entry(region.name().to_owned())
|
|
||||||
.or_default()
|
|
||||||
.push(proto);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
src/web.rs
20
src/web.rs
|
|
@ -347,6 +347,7 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -356,6 +357,7 @@ 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -365,6 +367,7 @@ 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -375,6 +378,20 @@ 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
|
||||||
|
|
@ -391,6 +408,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
@ -401,7 +419,7 @@ pub mod test {
|
||||||
let uri = self.uri.parse().unwrap();
|
let uri = self.uri.parse().unwrap();
|
||||||
super::HttpRequest {
|
super::HttpRequest {
|
||||||
uri,
|
uri,
|
||||||
headers: axum::http::HeaderMap::new(),
|
headers: self.headers,
|
||||||
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