🧑💻 Mejora funcionalidad del componente Html
- Amplía la documentación del componente. - Aplica la nueva funcionalidad en la página de bienvenida usando el nuevo renderizado dinámico con contexto. - Añade pruebas unitarias para el componente.
This commit is contained in:
parent
3a3e3b810f
commit
ef8d16f41f
7 changed files with 155 additions and 27 deletions
|
@ -1,28 +1,76 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
/// Componente básico para renderizar directamente código HTML.
|
/// Componente básico para renderizar dinámicamente código HTML recibiendo el contexto.
|
||||||
#[derive(AutoDefault)]
|
///
|
||||||
pub struct Html(Markup);
|
/// 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::<String>("username").unwrap_or(String::from("visitor"));
|
||||||
|
/// html! {
|
||||||
|
/// h1 { "Hello, " (user) }
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub struct Html(Box<dyn Fn(&mut Context) -> Markup + Send + Sync>);
|
||||||
|
|
||||||
|
impl Default for Html {
|
||||||
|
fn default() -> Self {
|
||||||
|
Html::with(|_| html! {})
|
||||||
|
}
|
||||||
|
}
|
||||||
impl ComponentTrait for Html {
|
impl ComponentTrait for Html {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Html::default()
|
Html::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
PrepareMarkup::With(html! { (self.0) })
|
PrepareMarkup::With((self.0)(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Html {
|
impl Html {
|
||||||
/// Crear una instancia con el código HTML del argumento.
|
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
|
||||||
pub fn with(html: Markup) -> Self {
|
///
|
||||||
Html(html)
|
/// 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: 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.
|
/// Sustituye la función que genera el `Markup`.
|
||||||
pub fn alter_html(&mut self, html: Markup) -> &mut Self {
|
///
|
||||||
self.0 = html;
|
/// 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<F>(&mut self, f: F) -> &mut Self
|
||||||
|
where
|
||||||
|
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.0 = Box::new(f);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,11 @@ impl ExtensionTrait for Welcome {
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
let app = &global::SETTINGS.app.name;
|
let app = &global::SETTINGS.app.name;
|
||||||
|
|
||||||
Page::new(request)
|
Page::new(Some(request))
|
||||||
.with_title(L10n::l("welcome_page"))
|
.with_title(L10n::l("welcome_page"))
|
||||||
.with_theme("Basic")
|
.with_theme("Basic")
|
||||||
.with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css")))
|
.with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css")))
|
||||||
.with_component(Html::with(html! {
|
.with_component(Html::with(move |_| html! {
|
||||||
div id="main-header" {
|
div id="main-header" {
|
||||||
header {
|
header {
|
||||||
h1 id="header-title" aria-label=(L10n::l("welcome_aria").with_arg("app", app)) {
|
h1 id="header-title" aria-label=(L10n::l("welcome_aria").with_arg("app", app)) {
|
||||||
|
|
|
@ -53,8 +53,11 @@ pub trait ComponentTrait: AnyInfo + ComponentRender + Send + Sync {
|
||||||
|
|
||||||
/// Devuelve una representación estructurada del componente lista para renderizar.
|
/// Devuelve una representación estructurada del componente lista para renderizar.
|
||||||
///
|
///
|
||||||
/// Puede sobrescribirse para generar dinámicamente el contenido HTML. Por defecto, devuelve
|
/// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente
|
||||||
/// [`PrepareMarkup::None`].
|
/// 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)]
|
#[allow(unused_variables)]
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
PrepareMarkup::None
|
PrepareMarkup::None
|
||||||
|
|
|
@ -107,7 +107,7 @@ impl Error for ErrorParam {}
|
||||||
/// ```
|
/// ```
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
request : HttpRequest, // Solicitud HTTP de origen.
|
request : Option<HttpRequest>, // Solicitud HTTP de origen.
|
||||||
langid : &'static LanguageIdentifier, // Identificador de idioma.
|
langid : &'static LanguageIdentifier, // Identificador de idioma.
|
||||||
theme : ThemeRef, // Referencia al tema para renderizar.
|
theme : ThemeRef, // Referencia al tema para renderizar.
|
||||||
layout : &'static str, // Composición del documento 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
|
/// El contexto inicializa el idioma, tema y composición por defecto, sin favicon ni recursos
|
||||||
/// cargados.
|
/// cargados.
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub fn new(request: HttpRequest) -> Self {
|
pub fn new(request: Option<HttpRequest>) -> Self {
|
||||||
Context {
|
Context {
|
||||||
request,
|
request,
|
||||||
langid : &DEFAULT_LANGID,
|
langid : &DEFAULT_LANGID,
|
||||||
|
@ -197,9 +197,9 @@ impl Context {
|
||||||
|
|
||||||
// Context GETTERS *****************************************************************************
|
// Context GETTERS *****************************************************************************
|
||||||
|
|
||||||
/// Devuelve la solicitud HTTP asociada al documento.
|
/// Devuelve una referencia a la solicitud HTTP asociada, si existe.
|
||||||
pub fn request(&self) -> &HttpRequest {
|
pub fn request(&self) -> Option<&HttpRequest> {
|
||||||
&self.request
|
self.request.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve el identificador de idioma asociado al documento.
|
/// Devuelve el identificador de idioma asociado al documento.
|
||||||
|
|
|
@ -30,9 +30,12 @@ pub struct Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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]
|
#[rustfmt::skip]
|
||||||
pub fn new(request: HttpRequest) -> Self {
|
pub fn new(request: Option<HttpRequest>) -> Self {
|
||||||
Page {
|
Page {
|
||||||
title : OptionTranslated::default(),
|
title : OptionTranslated::default(),
|
||||||
description : OptionTranslated::default(),
|
description : OptionTranslated::default(),
|
||||||
|
@ -165,7 +168,7 @@ impl Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la solicitud HTTP asociada.
|
/// Devuelve la solicitud HTTP asociada.
|
||||||
pub fn request(&self) -> &HttpRequest {
|
pub fn request(&self) -> Option<&HttpRequest> {
|
||||||
self.context.request()
|
self.context.request()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,12 +28,12 @@ impl fmt::Display for ErrorPage {
|
||||||
ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"),
|
ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"),
|
||||||
// Error 403.
|
// Error 403.
|
||||||
ErrorPage::AccessDenied(request) => {
|
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);
|
let error403 = error_page.theme().error403(&mut error_page);
|
||||||
if let Ok(page) = error_page
|
if let Ok(page) = error_page
|
||||||
.with_title(L10n::n("Error FORBIDDEN"))
|
.with_title(L10n::n("Error FORBIDDEN"))
|
||||||
.with_layout("error")
|
.with_layout("error")
|
||||||
.with_component(Html::with(error403))
|
.with_component(Html::with(move |_| error403.clone()))
|
||||||
.render()
|
.render()
|
||||||
{
|
{
|
||||||
write!(f, "{}", page.into_string())
|
write!(f, "{}", page.into_string())
|
||||||
|
@ -43,12 +43,12 @@ impl fmt::Display for ErrorPage {
|
||||||
}
|
}
|
||||||
// Error 404.
|
// Error 404.
|
||||||
ErrorPage::NotFound(request) => {
|
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);
|
let error404 = error_page.theme().error404(&mut error_page);
|
||||||
if let Ok(page) = error_page
|
if let Ok(page) = error_page
|
||||||
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
|
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
|
||||||
.with_layout("error")
|
.with_layout("error")
|
||||||
.with_component(Html::with(error404))
|
.with_component(Html::with(move |_| error404.clone()))
|
||||||
.render()
|
.render()
|
||||||
{
|
{
|
||||||
write!(f, "{}", page.into_string())
|
write!(f, "{}", page.into_string())
|
||||||
|
|
74
tests/component_html.rs
Normal file
74
tests/component_html.rs
Normal file
|
@ -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, "<p>Test</p>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::<String>("username").unwrap_or_default();
|
||||||
|
html! {
|
||||||
|
span { (name) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let markup = component.prepare_component(&mut cx).render();
|
||||||
|
|
||||||
|
assert_eq!(markup.0, "<span>Alice</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, "<div>Modified</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, "<span>GET</span>");
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue