diff --git a/src/base/component/html.rs b/src/base/component/html.rs
index 0937605..228bfe8 100644
--- a/src/base/component/html.rs
+++ b/src/base/component/html.rs
@@ -1,28 +1,76 @@
use crate::prelude::*;
-/// Componente básico para renderizar directamente código HTML.
-#[derive(AutoDefault)]
-pub struct Html(Markup);
+/// Componente básico para renderizar dinámicamente código HTML recibiendo el contexto.
+///
+/// Este componente permite generar contenido HTML arbitrario, usando la macro `html!` y accediendo
+/// opcionalmente al contexto de renderizado.
+///
+/// # Ejemplo
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// let component = Html::with(|_| {
+/// html! {
+/// div class="example" {
+/// p { "Hello from PageTop." }
+/// }
+/// }
+/// });
+/// ```
+///
+/// Para renderizar contenido que dependa del contexto, se puede acceder a él dentro del *closure*:
+///
+/// ```rust
+/// use pagetop::prelude::*;
+///
+/// let component = Html::with(|cx| {
+/// let user = cx.get_param::("username").unwrap_or(String::from("visitor"));
+/// html! {
+/// h1 { "Hello, " (user) }
+/// }
+/// });
+/// ```
+pub struct Html(Box Markup + Send + Sync>);
+impl Default for Html {
+ fn default() -> Self {
+ Html::with(|_| html! {})
+ }
+}
impl ComponentTrait for Html {
fn new() -> Self {
Html::default()
}
- fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
- PrepareMarkup::With(html! { (self.0) })
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ PrepareMarkup::With((self.0)(cx))
}
}
impl Html {
- /// Crear una instancia con el código HTML del argumento.
- pub fn with(html: Markup) -> Self {
- Html(html)
+ /// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
+ ///
+ /// El método [`prepare_component`](crate::core::component::ComponentTrait::prepare_component)
+ /// delega el renderizado en la función proporcionada, que recibe una referencia mutable
+ /// al contexto de renderizado ([`Context`]).
+ pub fn with(f: F) -> Self
+ where
+ F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
+ {
+ Html(Box::new(f))
}
- /// Modifica el código HTML de la instancia con el nuevo código del argumento.
- pub fn alter_html(&mut self, html: Markup) -> &mut Self {
- self.0 = html;
+ /// Sustituye la función que genera el `Markup`.
+ ///
+ /// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando
+ /// [`prepare_component`](crate::core::component::ComponentTrait::prepare_component) invoque
+ /// esta instancia. La nueva función también recibe una referencia al contexto ([`Context`]).
+ pub fn alter_html(&mut self, f: F) -> &mut Self
+ where
+ F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
+ {
+ self.0 = Box::new(f);
self
}
}
diff --git a/src/base/extension/welcome.rs b/src/base/extension/welcome.rs
index f0664d2..d8b1259 100644
--- a/src/base/extension/welcome.rs
+++ b/src/base/extension/welcome.rs
@@ -24,11 +24,11 @@ impl ExtensionTrait for Welcome {
async fn homepage(request: HttpRequest) -> ResultPage {
let app = &global::SETTINGS.app.name;
- Page::new(request)
+ Page::new(Some(request))
.with_title(L10n::l("welcome_page"))
.with_theme("Basic")
.with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css")))
- .with_component(Html::with(html! {
+ .with_component(Html::with(move |_| html! {
div id="main-header" {
header {
h1 id="header-title" aria-label=(L10n::l("welcome_aria").with_arg("app", app)) {
diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs
index 63264b7..a9a4f81 100644
--- a/src/core/component/definition.rs
+++ b/src/core/component/definition.rs
@@ -53,8 +53,11 @@ pub trait ComponentTrait: AnyInfo + ComponentRender + Send + Sync {
/// Devuelve una representación estructurada del componente lista para renderizar.
///
- /// Puede sobrescribirse para generar dinámicamente el contenido HTML. Por defecto, devuelve
- /// [`PrepareMarkup::None`].
+ /// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente
+ /// durante el proceso de construcción del documento. Puede sobrescribirse para generar
+ /// dinámicamente el contenido HTML con acceso al contexto de renderizado.
+ ///
+ /// Por defecto, devuelve [`PrepareMarkup::None`].
#[allow(unused_variables)]
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::None
diff --git a/src/html/context.rs b/src/html/context.rs
index a1df987..456ca7a 100644
--- a/src/html/context.rs
+++ b/src/html/context.rs
@@ -107,7 +107,7 @@ impl Error for ErrorParam {}
/// ```
#[rustfmt::skip]
pub struct Context {
- request : HttpRequest, // Solicitud HTTP de origen.
+ request : Option, // Solicitud HTTP de origen.
langid : &'static LanguageIdentifier, // Identificador de idioma.
theme : ThemeRef, // Referencia al tema para renderizar.
layout : &'static str, // Composición del documento para renderizar.
@@ -124,7 +124,7 @@ impl Context {
/// El contexto inicializa el idioma, tema y composición por defecto, sin favicon ni recursos
/// cargados.
#[rustfmt::skip]
- pub fn new(request: HttpRequest) -> Self {
+ pub fn new(request: Option) -> Self {
Context {
request,
langid : &DEFAULT_LANGID,
@@ -197,9 +197,9 @@ impl Context {
// Context GETTERS *****************************************************************************
- /// Devuelve la solicitud HTTP asociada al documento.
- pub fn request(&self) -> &HttpRequest {
- &self.request
+ /// Devuelve una referencia a la solicitud HTTP asociada, si existe.
+ pub fn request(&self) -> Option<&HttpRequest> {
+ self.request.as_ref()
}
/// Devuelve el identificador de idioma asociado al documento.
diff --git a/src/response/page.rs b/src/response/page.rs
index b252848..0964509 100644
--- a/src/response/page.rs
+++ b/src/response/page.rs
@@ -30,9 +30,12 @@ pub struct Page {
}
impl Page {
- /// Crea una nueva instancia de página con contexto basado en la petición HTTP.
+ /// Crea una nueva instancia de página.
+ ///
+ /// Si se proporciona la solicitud HTTP, se guardará en el contexto de renderizado de la página
+ /// para poder ser recuperada por los componentes si es necesario.
#[rustfmt::skip]
- pub fn new(request: HttpRequest) -> Self {
+ pub fn new(request: Option) -> Self {
Page {
title : OptionTranslated::default(),
description : OptionTranslated::default(),
@@ -165,7 +168,7 @@ impl Page {
}
/// Devuelve la solicitud HTTP asociada.
- pub fn request(&self) -> &HttpRequest {
+ pub fn request(&self) -> Option<&HttpRequest> {
self.context.request()
}
diff --git a/src/response/page/error.rs b/src/response/page/error.rs
index 3d952c6..3a2511c 100644
--- a/src/response/page/error.rs
+++ b/src/response/page/error.rs
@@ -28,12 +28,12 @@ impl fmt::Display for ErrorPage {
ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"),
// Error 403.
ErrorPage::AccessDenied(request) => {
- let mut error_page = Page::new(request.clone());
+ let mut error_page = Page::new(Some(request.clone()));
let error403 = error_page.theme().error403(&mut error_page);
if let Ok(page) = error_page
.with_title(L10n::n("Error FORBIDDEN"))
.with_layout("error")
- .with_component(Html::with(error403))
+ .with_component(Html::with(move |_| error403.clone()))
.render()
{
write!(f, "{}", page.into_string())
@@ -43,12 +43,12 @@ impl fmt::Display for ErrorPage {
}
// Error 404.
ErrorPage::NotFound(request) => {
- let mut error_page = Page::new(request.clone());
+ let mut error_page = Page::new(Some(request.clone()));
let error404 = error_page.theme().error404(&mut error_page);
if let Ok(page) = error_page
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
.with_layout("error")
- .with_component(Html::with(error404))
+ .with_component(Html::with(move |_| error404.clone()))
.render()
{
write!(f, "{}", page.into_string())
diff --git a/tests/component_html.rs b/tests/component_html.rs
new file mode 100644
index 0000000..978248f
--- /dev/null
+++ b/tests/component_html.rs
@@ -0,0 +1,74 @@
+use pagetop::prelude::*;
+
+#[pagetop::test]
+async fn component_html_renders_static_markup() {
+ let component = Html::with(|_| {
+ html! {
+ p { "Test" }
+ }
+ });
+
+ let markup = component
+ .prepare_component(&mut Context::new(None))
+ .render();
+
+ assert_eq!(markup.0, "Test
");
+}
+
+#[pagetop::test]
+async fn component_html_renders_using_context_param() {
+ let mut cx = Context::new(None).with_param("username", String::from("Alice"));
+
+ let component = Html::with(|cx| {
+ let name = cx.get_param::("username").unwrap_or_default();
+ html! {
+ span { (name) }
+ }
+ });
+
+ let markup = component.prepare_component(&mut cx).render();
+
+ assert_eq!(markup.0, "Alice");
+}
+
+#[pagetop::test]
+async fn component_html_allows_replacing_render_function() {
+ let mut component = Html::with(|_| html! { div { "Original" } });
+
+ component.alter_html(|_| html! { div { "Modified" } });
+
+ let markup = component
+ .prepare_component(&mut Context::new(None))
+ .render();
+
+ assert_eq!(markup.0, "Modified
");
+}
+
+#[pagetop::test]
+async fn component_html_default_renders_empty_markup() {
+ let component = Html::default();
+
+ let markup = component
+ .prepare_component(&mut Context::new(None))
+ .render();
+
+ assert_eq!(markup.0, "");
+}
+
+#[pagetop::test]
+async fn component_html_can_access_http_method() {
+ let req = service::test::TestRequest::with_uri("/").to_http_request();
+ let mut cx = Context::new(Some(req));
+
+ let component = Html::with(|cx| {
+ let method = cx
+ .request()
+ .map(|r| r.method().to_string())
+ .unwrap_or_default();
+ html! { span { (method) } }
+ });
+
+ let markup = component.prepare_component(&mut cx).render();
+
+ assert_eq!(markup.0, "GET");
+}