🎨 Convierte el ciclo de renderizado en async
`prepare()` de `Component`, `render()` de `Region` y `Template`, y la implementación de `ComponentRender` pasan a ser funciones async. Se actualizan todos los componentes base, helpers, tests y ejemplos.
This commit is contained in:
parent
9acd8cc51a
commit
9354894b3a
48 changed files with 436 additions and 365 deletions
|
|
@ -51,6 +51,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct HelloWorld;
|
struct HelloWorld;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for HelloWorld {
|
impl Extension for HelloWorld {
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
router.route("/", web::get(hello_world))
|
router.route("/", web::get(hello_world))
|
||||||
|
|
@ -60,7 +61,7 @@ impl Extension for HelloWorld {
|
||||||
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
.with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||||
.render()
|
.render().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
|
|
@ -72,7 +73,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
Este programa implementa una extensión llamada `HelloWorld` que sirve una página web en la ruta raíz
|
Este programa implementa una extensión llamada `HelloWorld` que sirve una página web en la ruta raíz
|
||||||
(`/`) mostrando el texto "Hello World!" dentro de un elemento HTML `<h1>`.
|
(`/`) mostrando el texto "Hello World!" dentro de un elemento HTML `<h1>`.
|
||||||
|
|
||||||
|
|
||||||
## Proyecto
|
## Proyecto
|
||||||
|
|
||||||
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
||||||
|
|
@ -111,7 +111,6 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
|
||||||
* **[pagetop-seaorm](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm)**,
|
* **[pagetop-seaorm](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm)**,
|
||||||
integra [SeaORM](https://www.sea-ql.org/SeaORM) para acceder a bases de datos relacionales.
|
integra [SeaORM](https://www.sea-ql.org/SeaORM) para acceder a bases de datos relacionales.
|
||||||
|
|
||||||
|
|
||||||
## Pruebas
|
## Pruebas
|
||||||
|
|
||||||
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
||||||
|
|
@ -132,14 +131,12 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
|
||||||
> * Los alias suprimen las trazas del registro de eventos. Para activarlas usa directamente
|
> * Los alias suprimen las trazas del registro de eventos. Para activarlas usa directamente
|
||||||
> `cargo test`.
|
> `cargo test`.
|
||||||
|
|
||||||
|
|
||||||
## Advertencia
|
## Advertencia
|
||||||
|
|
||||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||||
hasta que se libere la versión **1.0.0**.
|
hasta que se libere la versión **1.0.0**.
|
||||||
|
|
||||||
|
|
||||||
## Licencia
|
## Licencia
|
||||||
|
|
||||||
El código está disponible bajo una doble licencia:
|
El código está disponible bajo una doble licencia:
|
||||||
|
|
@ -153,7 +150,6 @@ El código está disponible bajo una doble licencia:
|
||||||
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||||
el ecosistema Rust.
|
el ecosistema Rust.
|
||||||
|
|
||||||
|
|
||||||
## Contribuir
|
## Contribuir
|
||||||
|
|
||||||
PageTop mantiene **un único repositorio oficial**:
|
PageTop mantiene **un único repositorio oficial**:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
struct FormControls;
|
struct FormControls;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for FormControls {
|
impl Extension for FormControls {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
||||||
|
|
@ -297,6 +298,7 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.render()
|
.render()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn form_lists() -> Form {
|
fn form_lists() -> Form {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct HelloName;
|
struct HelloName;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for HelloName {
|
impl Extension for HelloName {
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
router.route("/hello/{name}", web::get(hello_name))
|
router.route("/hello/{name}", web::get(hello_name))
|
||||||
|
|
@ -19,6 +20,7 @@ async fn hello_name(
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.render()
|
.render()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct HelloWorld;
|
struct HelloWorld;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for HelloWorld {
|
impl Extension for HelloWorld {
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
router.route("/", web::get(hello_world))
|
router.route("/", web::get(hello_world))
|
||||||
|
|
@ -16,6 +17,7 @@ async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.render()
|
.render()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
struct IntroColors;
|
struct IntroColors;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for IntroColors {
|
impl Extension for IntroColors {
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
router.route("/", web::get(intro_colors))
|
router.route("/", web::get(intro_colors))
|
||||||
|
|
@ -74,6 +75,7 @@ async fn intro_colors(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.render()
|
.render()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct MyApp;
|
struct MyApp;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for MyApp {
|
impl Extension for MyApp {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![
|
vec![
|
||||||
|
|
@ -68,7 +69,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
p { (L10n::l("sample_content").using(cx)) }
|
p { (L10n::l("sample_content").using(cx)) }
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.render()
|
.render().await
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct MyApp;
|
struct MyApp;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for MyApp {
|
impl Extension for MyApp {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![
|
vec![
|
||||||
|
|
@ -69,7 +70,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
p { (L10n::l("sample_content").using(cx)) }
|
p { (L10n::l("sample_content").using(cx)) }
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.render()
|
.render().await
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
|
|
@ -89,6 +90,7 @@ include_locales!(LOCALES_ALINER);
|
||||||
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.
|
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.
|
||||||
pub struct Aliner;
|
pub struct Aliner;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for Aliner {
|
impl Extension for Aliner {
|
||||||
fn name(&self) -> L10n {
|
fn name(&self) -> L10n {
|
||||||
L10n::t("extension_name", &LOCALES_ALINER)
|
L10n::t("extension_name", &LOCALES_ALINER)
|
||||||
|
|
@ -108,6 +110,7 @@ impl Extension for Aliner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Theme for Aliner {
|
impl Theme for Aliner {
|
||||||
fn before_render_page_body(&self, page: &mut Page) {
|
fn before_render_page_body(&self, page: &mut Page) {
|
||||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
pub struct MyExtension;
|
pub struct MyExtension;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for MyExtension {
|
impl Extension for MyExtension {
|
||||||
fn configure_router(&self, mut router: Router) -> Router {
|
fn configure_router(&self, mut router: Router) -> Router {
|
||||||
serve_static_files!(router, ["./static/css", app_css] => "/public/css");
|
serve_static_files!(router, ["./static/css", app_css] => "/public/css");
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
pub struct MyExtension;
|
pub struct MyExtension;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for MyExtension {
|
impl Extension for MyExtension {
|
||||||
fn configure_router(&self, mut router: Router) -> Router {
|
fn configure_router(&self, mut router: Router) -> Router {
|
||||||
serve_static_files!(router, ["./static/css", app_css] => "/public/css");
|
serve_static_files!(router, ["./static/css", app_css] => "/public/css");
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ mod figfont;
|
||||||
use crate::core::{extension, extension::ExtensionRef};
|
use crate::core::{extension, extension::ExtensionRef};
|
||||||
use crate::html::Markup;
|
use crate::html::Markup;
|
||||||
use crate::locale::Locale;
|
use crate::locale::Locale;
|
||||||
use crate::response::page::ErrorPage;
|
use crate::response::page::{ErrorPage, render_error_pages};
|
||||||
use crate::web::{HttpRequest, Router};
|
use crate::web::{HttpRequest, Router};
|
||||||
use crate::{PAGETOP_VERSION, global, trace};
|
use crate::{PAGETOP_VERSION, global, trace};
|
||||||
|
|
||||||
|
|
@ -109,7 +109,9 @@ impl Application {
|
||||||
fn build_router() -> Router {
|
fn build_router() -> Router {
|
||||||
let router = extension::all::configure_routes(Router::new());
|
let router = extension::all::configure_routes(Router::new());
|
||||||
let router = extension::all::configure_middleware(router);
|
let router = extension::all::configure_middleware(router);
|
||||||
router.fallback(route_not_found)
|
router
|
||||||
|
.fallback(route_not_found)
|
||||||
|
.layer(axum::middleware::from_fn(render_error_pages))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arranca el servidor web de la aplicación.
|
/// Arranca el servidor web de la aplicación.
|
||||||
|
|
@ -123,6 +125,7 @@ impl Application {
|
||||||
///
|
///
|
||||||
/// struct MyApp;
|
/// struct MyApp;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyApp {}
|
/// impl Extension for MyApp {}
|
||||||
///
|
///
|
||||||
/// #[pagetop::main]
|
/// #[pagetop::main]
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ pub type FnCheckPermission = fn(cx: &Context, key: &str, granted: &mut bool);
|
||||||
///
|
///
|
||||||
/// pub struct MyAuth;
|
/// pub struct MyAuth;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyAuth {
|
/// impl Extension for MyAuth {
|
||||||
/// fn actions(&self) -> Vec<ActionBox> {
|
/// fn actions(&self) -> Vec<ActionBox> {
|
||||||
/// actions![CheckPermission::new(check_my_permissions)]
|
/// actions![CheckPermission::new(check_my_permissions)]
|
||||||
|
|
@ -162,7 +163,7 @@ impl CheckPermission {
|
||||||
/// if !has_permission(page.context(), "myapp.edit") {
|
/// if !has_permission(page.context(), "myapp.edit") {
|
||||||
/// return Err(ErrorPage::NotFound(request));
|
/// return Err(ErrorPage::NotFound(request));
|
||||||
/// }
|
/// }
|
||||||
/// page.render()
|
/// page.render().await
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn has_permission(cx: &Context, key: &str) -> bool {
|
pub fn has_permission(cx: &Context, key: &str) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub struct Block {
|
||||||
children: Children,
|
children: Children,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Block {
|
impl Component for Block {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -31,8 +32,8 @@ impl Component for Block {
|
||||||
self.alter_prop(PropsOp::prepend_classes("block"));
|
self.alter_prop(PropsOp::prepend_classes("block"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let block_body = self.children().render(cx);
|
let block_body = self.children().render(cx).await;
|
||||||
|
|
||||||
if block_body.is_empty() {
|
if block_body.is_empty() {
|
||||||
return Ok(html! {});
|
return Ok(html! {});
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ pub struct Button {
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Button {
|
impl Component for Button {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -94,7 +95,7 @@ impl Component for Button {
|
||||||
self.alter_prop(PropsOp::prepend_classes("button"));
|
self.alter_prop(PropsOp::prepend_classes("button"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
button
|
button
|
||||||
type=(self.kind())
|
type=(self.kind())
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ pub struct Field {
|
||||||
inline: bool,
|
inline: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Field {
|
impl Component for Field {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -145,7 +146,7 @@ impl Component for Field {
|
||||||
self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes"));
|
self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
|
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
|
||||||
let name = self.name().get().unwrap();
|
let name = self.name().get().unwrap();
|
||||||
let container_id = self.id().unwrap();
|
let container_id = self.id().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ pub struct Checkbox {
|
||||||
reverse: bool,
|
reverse: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Checkbox {
|
impl Component for Checkbox {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -95,7 +96,7 @@ impl Component for Checkbox {
|
||||||
self.alter_prop(PropsOp::prepend_classes(classes));
|
self.alter_prop(PropsOp::prepend_classes(classes));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
|
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
|
||||||
let name = self.name().get().unwrap();
|
let name = self.name().get().unwrap();
|
||||||
let container_id = self.id().unwrap();
|
let container_id = self.id().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ pub struct Form {
|
||||||
children: Children,
|
children: Children,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Form {
|
impl Component for Form {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -70,7 +71,7 @@ impl Component for Form {
|
||||||
self.alter_prop(PropsOp::prepend_classes("form"));
|
self.alter_prop(PropsOp::prepend_classes("form"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let method = match self.method() {
|
let method = match self.method() {
|
||||||
form::Method::Post => Some("post"),
|
form::Method::Post => Some("post"),
|
||||||
form::Method::Get => None,
|
form::Method::Get => None,
|
||||||
|
|
@ -82,7 +83,7 @@ impl Component for Form {
|
||||||
method=[method]
|
method=[method]
|
||||||
accept-charset=[self.charset().get()]
|
accept-charset=[self.charset().get()]
|
||||||
{
|
{
|
||||||
(self.children().render(cx))
|
(self.children().render(cx).await)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ pub struct Fieldset {
|
||||||
children: Children,
|
children: Children,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Fieldset {
|
impl Component for Fieldset {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -45,8 +46,8 @@ impl Component for Fieldset {
|
||||||
self.props.get_id()
|
self.props.get_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let children = self.children().render(cx);
|
let children = self.children().render(cx).await;
|
||||||
|
|
||||||
if children.is_empty() {
|
if children.is_empty() {
|
||||||
return Ok(html! {});
|
return Ok(html! {});
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,13 @@ pub struct Hidden {
|
||||||
value: AttrValue,
|
value: AttrValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Hidden {
|
impl Component for Hidden {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
input
|
input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,7 @@ pub struct Field {
|
||||||
inputmode: Attr<Mode>,
|
inputmode: Attr<Mode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Field {
|
impl Component for Field {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -181,7 +182,7 @@ impl Component for Field {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let container_id = self.id();
|
let container_id = self.id();
|
||||||
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
|
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
|
||||||
let input_class = if *self.plaintext() {
|
let input_class = if *self.plaintext() {
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ pub struct Field {
|
||||||
inline: bool,
|
inline: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Field {
|
impl Component for Field {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -136,7 +137,7 @@ impl Component for Field {
|
||||||
self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios"));
|
self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
|
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
|
||||||
let name = self.name().get().unwrap();
|
let name = self.name().get().unwrap();
|
||||||
let container_id = self.id().unwrap();
|
let container_id = self.id().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ pub struct Range {
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Range {
|
impl Component for Range {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -74,7 +75,7 @@ impl Component for Range {
|
||||||
self.alter_prop(PropsOp::prepend_classes("form-field form-field-range"));
|
self.alter_prop(PropsOp::prepend_classes("form-field form-field-range"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let container_id = self.id();
|
let container_id = self.id();
|
||||||
let range_id = container_id.as_deref().map(|id| util::join!(id, "-range"));
|
let range_id = container_id.as_deref().map(|id| util::join!(id, "-range"));
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@ pub struct Field {
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Field {
|
impl Component for Field {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -233,7 +234,7 @@ impl Component for Field {
|
||||||
self.alter_prop(PropsOp::prepend_classes("form-field form-field-select"));
|
self.alter_prop(PropsOp::prepend_classes("form-field form-field-select"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let container_id = self.id();
|
let container_id = self.id();
|
||||||
let select_id = container_id.as_deref().map(|id| util::join!(id, "-select"));
|
let select_id = container_id.as_deref().map(|id| util::join!(id, "-select"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ pub struct Textarea {
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Textarea {
|
impl Component for Textarea {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -84,7 +85,7 @@ impl Component for Textarea {
|
||||||
self.alter_prop(PropsOp::prepend_classes("form-field form-field-textarea"));
|
self.alter_prop(PropsOp::prepend_classes("form-field form-field-textarea"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let container_id = self.id();
|
let container_id = self.id();
|
||||||
let textarea_id = container_id
|
let textarea_id = container_id
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,13 @@ impl Default for Html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Html {
|
impl Component for Html {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(self.html(cx))
|
Ok(self.html(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,12 +107,13 @@ impl Default for Intro {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for Intro {
|
impl Component for Intro {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
cx.alter_assets(AssetsOp::AddStyleSheet(
|
cx.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION),
|
StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION),
|
||||||
));
|
));
|
||||||
|
|
@ -146,7 +147,7 @@ impl Component for Intro {
|
||||||
}
|
}
|
||||||
aside class="intro-header-img" aria-hidden="true" {
|
aside class="intro-header-img" aria-hidden="true" {
|
||||||
div class="intro-header-mascot" {
|
div class="intro-header-mascot" {
|
||||||
(PageTopSvg::Color.render(cx))
|
(PageTopSvg::Color.markup(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +192,7 @@ impl Component for Intro {
|
||||||
(L10n::l("intro_text2").using(cx))
|
(L10n::l("intro_text2").using(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(self.children().render(cx))
|
(self.children().render(cx).await)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +200,7 @@ impl Component for Intro {
|
||||||
div class="intro-footer" {
|
div class="intro-footer" {
|
||||||
section class="intro-footer-body" {
|
section class="intro-footer-body" {
|
||||||
div class="intro-footer-logo" {
|
div class="intro-footer-logo" {
|
||||||
(PageTopSvg::LineLight.render(cx))
|
(PageTopSvg::LineLight.markup(cx))
|
||||||
}
|
}
|
||||||
div class="intro-footer-links" {
|
div class="intro-footer-links" {
|
||||||
a href="https://crates.io/crates/pagetop" target="_blank" rel="noopener noreferrer" { ("Crates.io") }
|
a href="https://crates.io/crates/pagetop" target="_blank" rel="noopener noreferrer" { ("Crates.io") }
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub struct PoweredBy {
|
||||||
copyright: Option<String>,
|
copyright: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for PoweredBy {
|
impl Component for PoweredBy {
|
||||||
/// Crea una nueva instancia de `PoweredBy`.
|
/// Crea una nueva instancia de `PoweredBy`.
|
||||||
///
|
///
|
||||||
|
|
@ -25,7 +26,7 @@ impl Component for PoweredBy {
|
||||||
PoweredBy { copyright: Some(c) }
|
PoweredBy { copyright: Some(c) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
div id=[self.id()] class="poweredby" {
|
div id=[self.id()] class="poweredby" {
|
||||||
@if let Some(c) = self.copyright() {
|
@if let Some(c) = self.copyright() {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use crate::prelude::*;
|
||||||
/// Resulta útil en demos o para comprobar rápidamente que el servidor ha arrancado correctamente.
|
/// Resulta útil en demos o para comprobar rápidamente que el servidor ha arrancado correctamente.
|
||||||
pub struct Welcome;
|
pub struct Welcome;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for Welcome {
|
impl Extension for Welcome {
|
||||||
fn name(&self) -> L10n {
|
fn name(&self) -> L10n {
|
||||||
L10n::l("welcome_extension_name")
|
L10n::l("welcome_extension_name")
|
||||||
|
|
@ -62,4 +63,5 @@ async fn home(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.render()
|
.render()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@ use crate::prelude::*;
|
||||||
/// Tema básico por defecto que extiende el funcionamiento predeterminado de [`Theme`].
|
/// Tema básico por defecto que extiende el funcionamiento predeterminado de [`Theme`].
|
||||||
pub struct Basic;
|
pub struct Basic;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for Basic {
|
impl Extension for Basic {
|
||||||
fn theme(&self) -> Option<ThemeRef> {
|
fn theme(&self) -> Option<ThemeRef> {
|
||||||
Some(&Self)
|
Some(&Self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Theme for Basic {
|
impl Theme for Basic {
|
||||||
fn before_render_page_body(&self, page: &mut Page) {
|
fn before_render_page_body(&self, page: &mut Page) {
|
||||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ pub use all::{dispatch_actions, try_dispatch_actions};
|
||||||
/// Extensión que ajusta un botón antes de renderizarlo y transforma su HTML final:
|
/// Extensión que ajusta un botón antes de renderizarlo y transforma su HTML final:
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyExtension {
|
/// impl Extension for MyExtension {
|
||||||
/// fn actions(&self) -> Vec<ActionBox> {
|
/// fn actions(&self) -> Vec<ActionBox> {
|
||||||
/// actions![
|
/// actions![
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ pub use definition::{Component, ComponentClone, ComponentRender};
|
||||||
|
|
||||||
mod children;
|
mod children;
|
||||||
pub use children::Children;
|
pub use children::Children;
|
||||||
pub use children::ComponentGuard;
|
|
||||||
pub use children::{Child, ChildOp, Embed};
|
pub use children::{Child, ChildOp, Embed};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
|
|
@ -34,6 +33,7 @@ pub use context::{AssetsOp, Context, ContextError, Contextual};
|
||||||
/// renderable: Option<FnIsRenderable>,
|
/// renderable: Option<FnIsRenderable>,
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Component for SampleComponent {
|
/// impl Component for SampleComponent {
|
||||||
/// fn new() -> Self {
|
/// fn new() -> Self {
|
||||||
/// Self::default()
|
/// Self::default()
|
||||||
|
|
@ -44,7 +44,7 @@ pub use context::{AssetsOp, Context, ContextError, Contextual};
|
||||||
/// self.renderable.map_or(true, |f| f(cx))
|
/// self.renderable.map_or(true, |f| f(cx))
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
/// async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
/// Ok(html! { "Visible component" })
|
/// Ok(html! { "Visible component" })
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,23 @@ 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::Mutex;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
pub use parking_lot::MutexGuard as ComponentGuard;
|
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::vec::IntoIter;
|
use std::vec::IntoIter;
|
||||||
|
|
||||||
/// Representa un componente hijo encapsulado para su uso en una lista [`Children`].
|
// **< Child >**************************************************************************************
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Child(Option<Mutex<Box<dyn Component>>>);
|
|
||||||
|
|
||||||
impl Clone for Child {
|
/// Representa un componente hijo encapsulado para su uso en una lista [`Children`].
|
||||||
fn clone(&self) -> Self {
|
#[derive(AutoDefault, Clone)]
|
||||||
Child(self.0.as_ref().map(|m| Mutex::new(m.lock().clone_box())))
|
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.lock().name()),
|
Some(c) => write!(f, "Child({})", c.read().name()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +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(Mutex::new(Box::new(component))))
|
Child(Some(Arc::new(RwLock::new(Box::new(component)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Child BUILDER >**************************************************************************
|
// **< Child BUILDER >**************************************************************************
|
||||||
|
|
@ -41,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| Mutex::new(Box::new(c) as Box<dyn Component>));
|
self.0 = component.map(|c| Arc::new(RwLock::new(Box::new(c) as Box<dyn Component>)));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,22 +45,28 @@ 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.lock().id())
|
self.0.as_ref().and_then(|c| c.read().id())
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Child RENDER >***************************************************************************
|
// **< Child RENDER >***************************************************************************
|
||||||
|
|
||||||
/// Renderiza el componente con el contexto proporcionado.
|
/// Renderiza el componente con el contexto proporcionado.
|
||||||
pub fn render(&self, cx: &mut Context) -> Markup {
|
pub async fn render(&self, cx: &mut Context) -> Markup {
|
||||||
self.0.as_ref().map_or(html! {}, |c| c.lock().render(cx))
|
match &self.0 {
|
||||||
|
None => html! {},
|
||||||
|
Some(m) => {
|
||||||
|
let mut component = m.read().clone_box();
|
||||||
|
component.render(cx).await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Child HELPERS >**************************************************************************
|
// **< Child HELPERS >**************************************************************************
|
||||||
|
|
||||||
/// Devuelve el [`UniqueId`] del tipo del componente, si existe.
|
// 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.lock().type_id())
|
self.0.as_ref().map(|c| c.read().type_id())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,12 +81,12 @@ impl<C: Component + 'static> From<Embed<C>> for Child {
|
||||||
/// children.with_child(my_embed.into());
|
/// children.with_child(my_embed.into());
|
||||||
/// ```
|
/// ```
|
||||||
fn from(embed: Embed<C>) -> Self {
|
fn from(embed: Embed<C>) -> Self {
|
||||||
if let Some(m) = embed.0 {
|
match embed.0 {
|
||||||
Child(Some(Mutex::new(
|
None => Child(None),
|
||||||
Box::new(m.into_inner()) as Box<dyn Component>
|
Some(arc) => Child(Some(Arc::new(RwLock::new(match Arc::try_unwrap(arc) {
|
||||||
)))
|
Ok(c) => Box::new(c) as Box<dyn Component>,
|
||||||
} else {
|
Err(arc) => arc.clone_box(),
|
||||||
Child(None)
|
})))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +115,7 @@ impl From<Child> for ChildOp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// *************************************************************************************************
|
// **< Embed >**************************************************************************************
|
||||||
|
|
||||||
/// Contenedor tipado para un *único* componente de un tipo concreto conocido.
|
/// Contenedor tipado para un *único* componente de un tipo concreto conocido.
|
||||||
///
|
///
|
||||||
|
|
@ -125,11 +126,12 @@ impl From<Child> for ChildOp {
|
||||||
/// Se usa habitualmente para incrustar un componente dentro de otro cuando no se necesita una lista
|
/// Se usa habitualmente para incrustar un componente dentro de otro cuando no se necesita una lista
|
||||||
/// completa de hijos ([`Children`]), sino un único componente tipado en un campo concreto.
|
/// completa de hijos ([`Children`]), sino un único componente tipado en un campo concreto.
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub struct Embed<C: Component>(Option<Mutex<C>>);
|
pub struct Embed<C: Component>(Option<Arc<C>>);
|
||||||
|
|
||||||
impl<C: Component + Clone> Clone for Embed<C> {
|
// Arc<C>: Clone no requiere C: Clone, pero #[derive(Clone)] añadiría ese bound innecesariamente.
|
||||||
|
impl<C: Component> Clone for Embed<C> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Embed(self.0.as_ref().map(|m| Mutex::new(m.lock().clone())))
|
Embed(self.0.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,7 +139,7 @@ impl<C: Component> fmt::Debug for Embed<C> {
|
||||||
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, "Embed(None)"),
|
None => write!(f, "Embed(None)"),
|
||||||
Some(c) => write!(f, "Embed({})", c.lock().name()),
|
Some(c) => write!(f, "Embed({})", c.name()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +147,7 @@ impl<C: Component> fmt::Debug for Embed<C> {
|
||||||
impl<C: Component> Embed<C> {
|
impl<C: Component> Embed<C> {
|
||||||
/// Crea un nuevo `Embed` a partir de un componente.
|
/// Crea un nuevo `Embed` a partir de un componente.
|
||||||
pub fn with(component: C) -> Self {
|
pub fn with(component: C) -> Self {
|
||||||
Embed(Some(Mutex::new(component)))
|
Embed(Some(Arc::new(component)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Embed BUILDER >**************************************************************************
|
// **< Embed BUILDER >**************************************************************************
|
||||||
|
|
@ -155,7 +157,7 @@ impl<C: Component> Embed<C> {
|
||||||
/// Si se proporciona `Some(component)`, se encapsula como [`Embed`]; y si es `None`, se limpia.
|
/// Si se proporciona `Some(component)`, se encapsula como [`Embed`]; y si es `None`, se limpia.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_component(mut self, component: Option<C>) -> Self {
|
pub fn with_component(mut self, component: Option<C>) -> Self {
|
||||||
self.0 = component.map(Mutex::new);
|
self.0 = component.map(Arc::new);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,55 +166,69 @@ impl<C: Component> Embed<C> {
|
||||||
/// 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.lock().id())
|
self.0.as_deref().and_then(|c| c.id())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve un acceso al componente incrustado.
|
/// Devuelve una referencia inmutable al componente incrustado, si existe.
|
||||||
///
|
///
|
||||||
/// - Devuelve `Some(ComponentGuard<C>)` si existe el componente, o `None` si está vacío.
|
/// Para acceso mutable, usa [`get_mut`](Embed::get_mut).
|
||||||
/// - El acceso es **exclusivo**: mientras el *guard* esté activo, no habrá otros accesos.
|
|
||||||
/// - Se recomienda mantener el *guard* **el menor tiempo posible** para evitar bloqueos
|
|
||||||
/// innecesarios.
|
|
||||||
/// - Para modificar el componente, declara el *guard* como `mut`:
|
|
||||||
/// `if let Some(mut c) = embed.get() { c.alter_title(...); }`.
|
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// let embed = Embed::with(Html::with(|_| html! { "Prueba" }));
|
/// let embed = Embed::with(Html::with(|_| html! { "Prueba" }));
|
||||||
/// {
|
|
||||||
/// if let Some(component) = embed.get() {
|
/// if let Some(component) = embed.get() {
|
||||||
/// assert_eq!(component.name(), "Html");
|
/// assert_eq!(component.name(), "Html");
|
||||||
/// }
|
/// }
|
||||||
/// }; // El *guard* se libera aquí, antes del *drop* de `embed`.
|
/// ```
|
||||||
|
pub fn get(&self) -> Option<&C> {
|
||||||
|
self.0.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve una referencia mutable al componente incrustado, si existe.
|
||||||
///
|
///
|
||||||
/// let embed = Embed::with(Block::new().with_title(L10n::n("Title")));
|
/// Si el [`Arc`] interno es compartido (por ejemplo, justo después de clonar el componente
|
||||||
/// {
|
/// padre), aplica *copy-on-write*: clona `C` antes de devolver la referencia mutable. El
|
||||||
/// if let Some(mut component) = embed.get() {
|
/// prototipo almacenado queda intacto.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// let mut embed = Embed::with(Block::new().with_title(L10n::n("Title")));
|
||||||
|
/// if let Some(component) = embed.get_mut() {
|
||||||
/// component.alter_title(L10n::n("New Title"));
|
/// component.alter_title(L10n::n("New Title"));
|
||||||
/// }
|
/// }
|
||||||
/// }; // El *guard* se libera aquí, antes del *drop* de `embed`.
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn get(&self) -> Option<ComponentGuard<'_, C>> {
|
pub fn get_mut(&mut self) -> Option<&mut C>
|
||||||
self.0.as_ref().map(|m| m.lock())
|
where
|
||||||
|
C: Clone,
|
||||||
|
{
|
||||||
|
self.0.as_mut().map(Arc::make_mut)
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Embed RENDER >***************************************************************************
|
// **< Embed RENDER >***************************************************************************
|
||||||
|
|
||||||
/// Renderiza el componente con el contexto proporcionado.
|
/// Renderiza el componente con el contexto proporcionado.
|
||||||
pub fn render(&self, cx: &mut Context) -> Markup {
|
pub async fn render(&self, cx: &mut Context) -> Markup {
|
||||||
self.0.as_ref().map_or(html! {}, |c| c.lock().render(cx))
|
match &self.0 {
|
||||||
|
None => html! {},
|
||||||
|
Some(m) => {
|
||||||
|
let mut component = m.clone_box();
|
||||||
|
component.render(cx).await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// *************************************************************************************************
|
// **< Children >***********************************************************************************
|
||||||
|
|
||||||
/// Operaciones para componentes hijo [`Child`] en una lista [`Children`].
|
/// Operaciones para componentes hijo [`Child`] en una lista [`Children`].
|
||||||
pub enum ChildOp {
|
pub enum ChildOp {
|
||||||
/// Añade un hijo al final de la lista.
|
/// Añade un hijo al final de la lista.
|
||||||
Add(Child),
|
Add(Child),
|
||||||
/// Añade un hijo solo si la lista está vacía.
|
/// Añade un hijo sólo si la lista está vacía.
|
||||||
AddIfEmpty(Child),
|
AddIfEmpty(Child),
|
||||||
/// Añade varios hijos al final de la lista, en el orden recibido.
|
/// Añade varios hijos al final de la lista, en el orden recibido.
|
||||||
AddMany(Vec<Child>),
|
AddMany(Vec<Child>),
|
||||||
|
|
@ -243,13 +259,10 @@ pub enum ChildOp {
|
||||||
/// - [`Child`]: representa un componente hijo encapsulado dentro de la lista. Almacena cualquier
|
/// - [`Child`]: representa un componente hijo encapsulado dentro de la lista. Almacena cualquier
|
||||||
/// componente sin necesidad de conocer su tipo concreto.
|
/// componente sin necesidad de conocer su tipo concreto.
|
||||||
/// - [`Embed<C>`]: contenedor tipado para un *único* componente de tipo `C`. Preferible a
|
/// - [`Embed<C>`]: contenedor tipado para un *único* componente de tipo `C`. Preferible a
|
||||||
/// `Children` cuando el padre solo necesita un componente y quiere acceso directo a los métodos
|
/// `Children` cuando el padre sólo necesita un componente y quiere acceso directo a los métodos
|
||||||
/// de `C`.
|
/// de `C`.
|
||||||
/// - [`ChildOp`]: operaciones disponibles sobre la lista. Cuando se necesita algo más que añadir al
|
/// - [`ChildOp`]: operaciones disponibles sobre la lista. Cuando se necesita algo más que añadir al
|
||||||
/// final, se construye la variante adecuada y se pasa a [`with_child`](Self::with_child).
|
/// final, se construye la variante adecuada y se pasa a [`with_child`](Self::with_child).
|
||||||
/// - [`ComponentGuard`]: devuelto por [`Embed::get`] para garantizar acceso exclusivo al componente
|
|
||||||
/// tipado. Mientras está activo bloquea cualquier otro acceso por lo que conviene liberarlo
|
|
||||||
/// cuanto antes.
|
|
||||||
///
|
///
|
||||||
/// # Conversiones implícitas
|
/// # Conversiones implícitas
|
||||||
///
|
///
|
||||||
|
|
@ -297,14 +310,14 @@ impl Children {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un componente hijo al final de la lista.
|
// Añade un componente hijo al final de la lista.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn add(&mut self, child: Child) -> &mut Self {
|
pub(crate) fn add(&mut self, child: Child) -> &mut Self {
|
||||||
self.0.push(child);
|
self.0.push(child);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un componente hijo en la lista sólo si está vacía.
|
// Añade un componente hijo en la lista sólo si está vacía.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn add_if_empty(&mut self, child: Child) -> &mut Self {
|
pub(crate) fn add_if_empty(&mut self, child: Child) -> &mut Self {
|
||||||
if self.0.is_empty() {
|
if self.0.is_empty() {
|
||||||
|
|
@ -345,17 +358,17 @@ impl Children {
|
||||||
// **< Children RENDER >************************************************************************
|
// **< Children RENDER >************************************************************************
|
||||||
|
|
||||||
/// Renderiza todos los componentes hijo, en orden.
|
/// Renderiza todos los componentes hijo, en orden.
|
||||||
pub fn render(&self, cx: &mut Context) -> Markup {
|
pub async fn render(&self, cx: &mut Context) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
@for c in &self.0 {
|
@for c in &self.0 {
|
||||||
(c.render(cx))
|
(c.render(cx).await)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< 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).
|
||||||
#[inline]
|
#[inline]
|
||||||
fn add_many<I>(&mut self, iter: I) -> &mut Self
|
fn add_many<I>(&mut self, iter: I) -> &mut Self
|
||||||
where
|
where
|
||||||
|
|
@ -365,7 +378,7 @@ impl Children {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra.
|
// Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn insert_after_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
fn insert_after_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
||||||
let id = Some(id.as_ref());
|
let id = Some(id.as_ref());
|
||||||
|
|
@ -376,7 +389,7 @@ impl Children {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserta un hijo antes del componente con el `id` dado, o al principio si no se encuentra.
|
// Inserta un hijo antes del componente con el `id` dado, o al principio si no se encuentra.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn insert_before_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
fn insert_before_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
||||||
let id = Some(id.as_ref());
|
let id = Some(id.as_ref());
|
||||||
|
|
@ -387,14 +400,14 @@ impl Children {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserta un hijo al principio de la lista.
|
// Inserta un hijo al principio de la lista.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn prepend(&mut self, child: Child) -> &mut Self {
|
fn prepend(&mut self, child: Child) -> &mut Self {
|
||||||
self.0.insert(0, child);
|
self.0.insert(0, child);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserta más de un componente hijo al principio de la lista (manteniendo el orden recibido).
|
// Inserta más de un componente hijo al principio de la lista (manteniendo el orden recibido).
|
||||||
#[inline]
|
#[inline]
|
||||||
fn prepend_many<I>(&mut self, iter: I) -> &mut Self
|
fn prepend_many<I>(&mut self, iter: I) -> &mut Self
|
||||||
where
|
where
|
||||||
|
|
@ -405,7 +418,7 @@ impl Children {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina el primer hijo con el `id` dado.
|
// Elimina el primer hijo con el `id` dado.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn remove_by_id(&mut self, id: impl AsRef<str>) -> &mut Self {
|
fn remove_by_id(&mut self, id: impl AsRef<str>) -> &mut Self {
|
||||||
let id = Some(id.as_ref());
|
let id = Some(id.as_ref());
|
||||||
|
|
@ -415,7 +428,7 @@ impl Children {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sustituye el primer hijo con el `id` dado por otro componente.
|
// Sustituye el primer hijo con el `id` dado por otro componente.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn replace_by_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
fn replace_by_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
||||||
let id = Some(id.as_ref());
|
let id = Some(id.as_ref());
|
||||||
|
|
@ -428,7 +441,7 @@ impl Children {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina todos los componentes hijo de la lista.
|
// Elimina todos los componentes hijo de la lista.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn reset(&mut self) -> &mut Self {
|
fn reset(&mut self) -> &mut Self {
|
||||||
self.0.clear();
|
self.0.clear();
|
||||||
|
|
@ -445,7 +458,7 @@ impl IntoIterator for Children {
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// let children = Children::new().with(child1).with(child2);
|
/// let children = Children::new().with_child(child1).with_child(child2);
|
||||||
/// for child in children {
|
/// for child in children {
|
||||||
/// println!("{:?}", child.id());
|
/// println!("{:?}", child.id());
|
||||||
/// }
|
/// }
|
||||||
|
|
@ -464,7 +477,7 @@ impl<'a> IntoIterator for &'a Children {
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// let children = Children::new().with(child1).with(child2);
|
/// let children = Children::new().with_child(child1).with_child(child2);
|
||||||
/// for child in &children {
|
/// for child in &children {
|
||||||
/// println!("{:?}", child.id());
|
/// println!("{:?}", child.id());
|
||||||
/// }
|
/// }
|
||||||
|
|
@ -483,9 +496,9 @@ impl<'a> IntoIterator for &'a mut Children {
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// let mut children = Children::new().with(child1).with(child2);
|
/// let mut children = Children::new().with_child(child1).with_child(child2);
|
||||||
/// for child in &mut children {
|
/// for child in &mut children {
|
||||||
/// child.render(&mut context);
|
/// child.render(&mut context).await;
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||||
use crate::web::HttpRequest;
|
use crate::web::HttpRequest;
|
||||||
use crate::{CowStr, builder_fn, util};
|
use crate::{CowStr, builder_fn, util};
|
||||||
|
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
|
@ -132,7 +133,7 @@ pub trait Contextual: LangId {
|
||||||
/// .with_param("flags", vec!["a", "b"]);
|
/// .with_param("flags", vec!["a", "b"]);
|
||||||
/// ```
|
/// ```
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_param<T: 'static>(self, key: &'static str, value: T) -> Self;
|
fn with_param<T: Send + Sync + 'static>(self, key: &'static str, value: T) -> Self;
|
||||||
|
|
||||||
/// Define los recursos del contexto usando [`AssetsOp`].
|
/// Define los recursos del contexto usando [`AssetsOp`].
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
|
@ -172,7 +173,7 @@ pub trait Contextual: LangId {
|
||||||
/// if page.current_user().is_authenticated() {
|
/// if page.current_user().is_authenticated() {
|
||||||
/// // Personalizar la página para el usuario autenticado.
|
/// // Personalizar la página para el usuario autenticado.
|
||||||
/// }
|
/// }
|
||||||
/// page.render()
|
/// page.render().await
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
fn current_user(&self) -> &CurrentUser;
|
fn current_user(&self) -> &CurrentUser;
|
||||||
|
|
@ -313,10 +314,10 @@ pub struct Context {
|
||||||
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
||||||
stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS.
|
stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||||
javascripts : Assets<JavaScript>, // Scripts JavaScript.
|
javascripts : Assets<JavaScript>, // Scripts JavaScript.
|
||||||
body_props : Props, // Identificador, clases CSS y atributos del <body>.
|
body_props : Props, // Id, clases CSS y atributos del <body>.
|
||||||
regions : ChildrenInRegions, // Regiones de componentes para renderizar.
|
regions : ChildrenInRegions, // Regiones de componentes para renderizar.
|
||||||
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
|
params : HashMap<&'static str, (Box<dyn Any + Send + Sync>, &'static str)>, // Parámetros.
|
||||||
id_counters : RefCell<HashMap<TypeId, usize>>, // RefCell permite mutar desde build_id(&self).
|
id_counters : Mutex<HashMap<TypeId, usize>>, // Mutex permite mutar desde build_id(&self).
|
||||||
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
|
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,7 +348,7 @@ impl Context {
|
||||||
body_props : Props::default(),
|
body_props : Props::default(),
|
||||||
regions : ChildrenInRegions::default(),
|
regions : ChildrenInRegions::default(),
|
||||||
params : HashMap::default(),
|
params : HashMap::default(),
|
||||||
id_counters: RefCell::new(HashMap::new()),
|
id_counters: Mutex::new(HashMap::new()),
|
||||||
messages : Vec::new(),
|
messages : Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -390,10 +391,11 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renderiza los componentes de una región.
|
/// Renderiza los componentes de una región.
|
||||||
pub fn render_region(&mut self, region_ref: RegionRef) -> Markup {
|
pub async fn render_region_named(&mut self, region_name: &str) -> Markup {
|
||||||
self.regions
|
self.regions
|
||||||
.children_for(self.theme, region_ref)
|
.assemble_region(self.theme, region_name)
|
||||||
.render(self)
|
.render(self)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Context HELPERS >************************************************************************
|
// **< Context HELPERS >************************************************************************
|
||||||
|
|
@ -436,7 +438,7 @@ impl Context {
|
||||||
segments
|
segments
|
||||||
};
|
};
|
||||||
let count = {
|
let count = {
|
||||||
let mut map = self.id_counters.borrow_mut();
|
let mut map = self.id_counters.lock();
|
||||||
let n = map.entry(TypeId::of::<C>()).or_insert(0);
|
let n = map.entry(TypeId::of::<C>()).or_insert(0);
|
||||||
*n += 1;
|
*n += 1;
|
||||||
*n
|
*n
|
||||||
|
|
@ -529,7 +531,7 @@ impl Contextual for Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
|
fn with_param<T: Send + Sync + 'static>(mut self, key: &'static str, value: T) -> Self {
|
||||||
let type_name = TypeInfo::FullName.of::<T>();
|
let type_name = TypeInfo::FullName.of::<T>();
|
||||||
self.params.insert(key, (Box::new(value), type_name));
|
self.params.insert(key, (Box::new(value), type_name));
|
||||||
self
|
self
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::async_trait;
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
use crate::core::component::{ComponentError, Context, Contextual};
|
use crate::core::component::{ComponentError, Context, Contextual};
|
||||||
use crate::core::theme::ThemeRef;
|
use crate::core::theme::ThemeRef;
|
||||||
|
|
@ -7,8 +8,9 @@ use crate::html::{Markup, html};
|
||||||
/// Habilita el clonado de componentes.
|
/// Habilita el clonado de componentes.
|
||||||
///
|
///
|
||||||
/// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. El método
|
/// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. El método
|
||||||
/// [`clone_box`](Self::clone_box) devuelve una copia en la *pila* del componente original, lo que
|
/// [`clone_box`](Self::clone_box) devuelve un clon del componente original encapsulado en un
|
||||||
/// permite clonar componentes sin conocer su tipo concreto en tiempo de compilación.
|
/// `Box<dyn Component>`, lo que permite clonar componentes sin conocer su tipo concreto en tiempo
|
||||||
|
/// de compilación.
|
||||||
pub trait ComponentClone {
|
pub trait ComponentClone {
|
||||||
/// Devuelve un clon del componente encapsulado en un [`Box<dyn Component>`].
|
/// Devuelve un clon del componente encapsulado en un [`Box<dyn Component>`].
|
||||||
fn clone_box(&self) -> Box<dyn Component>;
|
fn clone_box(&self) -> Box<dyn Component>;
|
||||||
|
|
@ -18,9 +20,10 @@ pub trait ComponentClone {
|
||||||
///
|
///
|
||||||
/// Este *trait* se implementa automáticamente en cualquier tipo (componente) que implemente
|
/// Este *trait* se implementa automáticamente en cualquier tipo (componente) que implemente
|
||||||
/// [`Component`], por lo que no requiere ninguna codificación manual.
|
/// [`Component`], por lo que no requiere ninguna codificación manual.
|
||||||
|
#[async_trait]
|
||||||
pub trait ComponentRender {
|
pub trait ComponentRender {
|
||||||
/// Renderiza el componente usando el contexto proporcionado.
|
/// Renderiza el componente usando el contexto proporcionado.
|
||||||
fn render(&mut self, cx: &mut Context) -> Markup;
|
async fn render(&mut self, cx: &mut Context) -> Markup;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interfaz común que debe implementar un componente renderizable en PageTop.
|
/// Interfaz común que debe implementar un componente renderizable en PageTop.
|
||||||
|
|
@ -35,9 +38,10 @@ pub trait ComponentRender {
|
||||||
///
|
///
|
||||||
/// Todo tipo que implemente `Component` **debe** derivar también [`Clone`]. Aunque el compilador
|
/// Todo tipo que implemente `Component` **debe** derivar también [`Clone`]. Aunque el compilador
|
||||||
/// no lo exige directamente (hacerlo rompería la seguridad de objeto de `dyn Component`),
|
/// no lo exige directamente (hacerlo rompería la seguridad de objeto de `dyn Component`),
|
||||||
/// [`ComponentClone`] se implementa automáticamente mediante una *impl* blanket solo para los tipos
|
/// [`ComponentClone`] se implementa automáticamente mediante una *impl blanket* sólo para los tipos
|
||||||
/// que sean `Component + Clone + 'static`. Sin `Clone`, habría que implementar [`ComponentClone`] a
|
/// que sean `Component + Clone + 'static`. Sin `Clone`, habría que implementar [`ComponentClone`] a
|
||||||
/// mano, y el componente no podría registrarse en [`InRegion`](crate::core::theme::InRegion).
|
/// mano, y el componente no podría registrarse en [`InRegion`](crate::core::theme::InRegion).
|
||||||
|
#[async_trait]
|
||||||
pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
|
pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
|
||||||
/// Crea una nueva instancia del componente.
|
/// Crea una nueva instancia del componente.
|
||||||
///
|
///
|
||||||
|
|
@ -72,12 +76,12 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
|
||||||
///
|
///
|
||||||
/// Por defecto, todos los componentes son renderizables (`true`). Sin embargo, este método
|
/// Por defecto, todos los componentes son renderizables (`true`). Sin embargo, este método
|
||||||
/// puede sobrescribirse para decidir dinámicamente si los componentes de este tipo se
|
/// puede sobrescribirse para decidir dinámicamente si los componentes de este tipo se
|
||||||
/// renderizan o no en función del contexto de renderizado. Recibe solo una referencia
|
/// renderizan o no en función del contexto de renderizado. Recibe sólo una referencia
|
||||||
/// compartida al contexto porque su único propósito es consultar datos, no modificarlos.
|
/// compartida al contexto porque su único propósito es consultar datos, no modificarlos.
|
||||||
///
|
///
|
||||||
/// También puede asignarse una función [`FnIsRenderable`](super::FnIsRenderable) a un campo del
|
/// También **puede asignarse una función [`FnIsRenderable`](super::FnIsRenderable) a un campo
|
||||||
/// componente para permitir que instancias concretas del mismo puedan decidir dinámicamente si
|
/// del componente** para permitir que instancias concretas del mismo puedan decidir
|
||||||
/// se renderizan o no.
|
/// dinámicamente si se renderizan o no.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn is_renderable(&self, cx: &Context) -> bool {
|
fn is_renderable(&self, cx: &Context) -> bool {
|
||||||
true
|
true
|
||||||
|
|
@ -88,8 +92,19 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
|
||||||
/// Segundo paso del [ciclo de renderizado](ComponentRender): se ejecuta tras comprobar
|
/// Segundo paso del [ciclo de renderizado](ComponentRender): se ejecuta tras comprobar
|
||||||
/// [`is_renderable()`](Self::is_renderable) y antes de la acción
|
/// [`is_renderable()`](Self::is_renderable) y antes de la acción
|
||||||
/// [`BeforeRender`](crate::base::action::component::BeforeRender) y de
|
/// [`BeforeRender`](crate::base::action::component::BeforeRender) y de
|
||||||
/// [`prepare()`](Self::prepare). Recibe solo una referencia compartida al contexto porque su
|
/// [`prepare()`](Self::prepare). Recibe sólo una referencia compartida al contexto porque su
|
||||||
/// propósito es mutar el propio componente, no el contexto. Por defecto no hace nada.
|
/// propósito es mutar el propio componente, no el contexto. Por defecto no hace nada.
|
||||||
|
///
|
||||||
|
/// Está pensado para **normalizar el estado interno** del componente antes de renderizarlo. Por
|
||||||
|
/// ejemplo, calcular clases CSS, ajustar valores de campos, derivar atributos a partir del
|
||||||
|
/// contexto, etc. Se desaconseja utilizar para operaciones de E/S o consultas a base de datos;
|
||||||
|
/// es intencionadamente síncrono.
|
||||||
|
///
|
||||||
|
/// La carga y el acceso a datos en general corresponden a [`prepare()`](Self::prepare), que es
|
||||||
|
/// `async` precisamente para ello.
|
||||||
|
///
|
||||||
|
/// La separación entre `setup()` (mutación de estado) y [`prepare()`](Self::prepare)
|
||||||
|
/// (generación de HTML) es deliberada y no debe fusionarse.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn setup(&mut self, cx: &Context) {}
|
fn setup(&mut self, cx: &Context) {}
|
||||||
|
|
||||||
|
|
@ -97,8 +112,8 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
|
||||||
///
|
///
|
||||||
/// Cuarto paso del [ciclo de renderizado](ComponentRender): se invoca tras
|
/// Cuarto paso del [ciclo de renderizado](ComponentRender): se invoca tras
|
||||||
/// [`setup()`](Self::setup) y la acción
|
/// [`setup()`](Self::setup) y la acción
|
||||||
/// [`BeforeRender`](crate::base::action::component::BeforeRender), pero solo si ningún tema
|
/// [`BeforeRender`](crate::base::action::component::BeforeRender), pero solamente si ningún
|
||||||
/// en la cadena devuelve `Some` en
|
/// tema en la cadena devuelve `Some` en
|
||||||
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component).
|
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component).
|
||||||
///
|
///
|
||||||
/// Se recomienda obtener los datos del componente a través de sus propios métodos para que los
|
/// Se recomienda obtener los datos del componente a través de sus propios métodos para que los
|
||||||
|
|
@ -107,7 +122,7 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
|
||||||
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`). En caso de error, devuelve un
|
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`). En caso de error, devuelve un
|
||||||
/// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*).
|
/// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*).
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(html! {})
|
Ok(html! {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,8 +158,9 @@ impl<T: Component + Clone + 'static> ComponentClone for T {
|
||||||
/// para que las extensiones puedan trabajar sobre el HTML final para modificarlo antes de
|
/// para que las extensiones puedan trabajar sobre el HTML final para modificarlo antes de
|
||||||
/// devolverlo.
|
/// devolverlo.
|
||||||
/// 7. Devuelve el [`Markup`] resultante.
|
/// 7. Devuelve el [`Markup`] resultante.
|
||||||
|
#[async_trait]
|
||||||
impl<C: Component> ComponentRender for C {
|
impl<C: Component> ComponentRender for C {
|
||||||
fn render(&mut self, cx: &mut Context) -> Markup {
|
async fn render(&mut self, cx: &mut Context) -> Markup {
|
||||||
// Si no es renderizable, devuelve un bloque HTML vacío.
|
// Si no es renderizable, devuelve un bloque HTML vacío.
|
||||||
if !self.is_renderable(cx) {
|
if !self.is_renderable(cx) {
|
||||||
return html! {};
|
return html! {};
|
||||||
|
|
@ -160,12 +176,12 @@ impl<C: Component> ComponentRender for C {
|
||||||
let result = 'resolve: {
|
let result = 'resolve: {
|
||||||
let mut t: Option<ThemeRef> = Some(cx.theme());
|
let mut t: Option<ThemeRef> = Some(cx.theme());
|
||||||
while let Some(theme) = t {
|
while let Some(theme) = t {
|
||||||
if let Some(r) = theme.handle_component(self, cx) {
|
if let Some(r) = theme.handle_component(self, cx).await {
|
||||||
break 'resolve r;
|
break 'resolve r;
|
||||||
}
|
}
|
||||||
t = theme.parent();
|
t = theme.parent();
|
||||||
}
|
}
|
||||||
self.prepare(cx)
|
self.prepare(cx).await
|
||||||
};
|
};
|
||||||
let prepare = match result {
|
let prepare = match result {
|
||||||
Ok(markup) => markup,
|
Ok(markup) => markup,
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,10 @@ use crate::{AutoDefault, Getters};
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # #[derive(Clone)]
|
/// # #[derive(Clone)]
|
||||||
/// # struct MyComponent;
|
/// # struct MyComponent;
|
||||||
|
/// # #[async_trait]
|
||||||
/// # impl Component for MyComponent {
|
/// # impl Component for MyComponent {
|
||||||
/// # fn new() -> Self { MyComponent }
|
/// # fn new() -> Self { MyComponent }
|
||||||
/// fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
/// async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
/// Err(ComponentError::new("Database connection failed")
|
/// Err(ComponentError::new("Database connection failed")
|
||||||
/// .with_fallback(html! { p class="error" { "Content temporarily unavailable." } }))
|
/// .with_fallback(html! { p class="error" { "Content temporarily unavailable." } }))
|
||||||
/// }
|
/// }
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use crate::web::Router;
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// pub struct MyExtension;
|
/// pub struct MyExtension;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyExtension {
|
/// impl Extension for MyExtension {
|
||||||
/// fn name(&self) -> L10n {
|
/// fn name(&self) -> L10n {
|
||||||
/// L10n::n("My Extension")
|
/// L10n::n("My Extension")
|
||||||
|
|
@ -54,12 +55,14 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// pub struct MyTheme;
|
/// pub struct MyTheme;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyTheme {
|
/// impl Extension for MyTheme {
|
||||||
/// fn theme(&self) -> Option<ThemeRef> {
|
/// fn theme(&self) -> Option<ThemeRef> {
|
||||||
/// Some(&Self)
|
/// Some(&Self)
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Theme for MyTheme {}
|
/// impl Theme for MyTheme {}
|
||||||
/// ```
|
/// ```
|
||||||
fn theme(&self) -> Option<ThemeRef> {
|
fn theme(&self) -> Option<ThemeRef> {
|
||||||
|
|
@ -81,9 +84,11 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # pub struct Database;
|
/// # pub struct Database;
|
||||||
|
/// # #[async_trait]
|
||||||
/// # impl Extension for Database {}
|
/// # impl Extension for Database {}
|
||||||
/// pub struct MyApp;
|
/// pub struct MyApp;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyApp {
|
/// impl Extension for MyApp {
|
||||||
/// fn dependencies(&self) -> Vec<ExtensionRef> {
|
/// fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
/// vec![&Database]
|
/// vec![&Database]
|
||||||
|
|
@ -116,8 +121,6 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// lógica de inicialización**. PageTop lo invoca una sola vez, después de que todas las
|
/// lógica de inicialización**. PageTop lo invoca una sola vez, después de que todas las
|
||||||
/// dependencias se han inicializado y antes de aceptar cualquier petición HTTP.
|
/// dependencias se han inicializado y antes de aceptar cualquier petición HTTP.
|
||||||
///
|
///
|
||||||
/// En ese caso, el bloque `impl Extension` debe llevar `#[async_trait]`:
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
|
|
@ -130,8 +133,6 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
|
||||||
/// Las extensiones que no sobrescriben `initialize()` no necesitan `#[async_trait]`.
|
|
||||||
async fn initialize(&self) {}
|
async fn initialize(&self) {}
|
||||||
|
|
||||||
/// Registra rutas, servicios y capas de la extensión en el servidor web de la aplicación.
|
/// Registra rutas, servicios y capas de la extensión en el servidor web de la aplicación.
|
||||||
|
|
@ -160,6 +161,7 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// # async fn create_post() -> &'static str { "" }
|
/// # async fn create_post() -> &'static str { "" }
|
||||||
/// pub struct Blog;
|
/// pub struct Blog;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for Blog {
|
/// impl Extension for Blog {
|
||||||
/// fn configure_router(&self, router: Router) -> Router {
|
/// fn configure_router(&self, router: Router) -> Router {
|
||||||
/// router
|
/// router
|
||||||
|
|
@ -178,6 +180,7 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// # async fn list_users() -> &'static str { "" }
|
/// # async fn list_users() -> &'static str { "" }
|
||||||
/// pub struct Admin;
|
/// pub struct Admin;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for Admin {
|
/// impl Extension for Admin {
|
||||||
/// fn configure_router(&self, router: Router) -> Router {
|
/// fn configure_router(&self, router: Router) -> Router {
|
||||||
/// let admin = Router::new()
|
/// let admin = Router::new()
|
||||||
|
|
@ -204,6 +207,7 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// pub struct Api;
|
/// pub struct Api;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for Api {
|
/// impl Extension for Api {
|
||||||
/// fn configure_router(&self, router: Router) -> Router {
|
/// fn configure_router(&self, router: Router) -> Router {
|
||||||
/// let api = Router::new()
|
/// let api = Router::new()
|
||||||
|
|
@ -226,6 +230,7 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// pub struct MyExtension;
|
/// pub struct MyExtension;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyExtension {
|
/// impl Extension for MyExtension {
|
||||||
/// fn configure_router(&self, router: Router) -> Router {
|
/// fn configure_router(&self, router: Router) -> Router {
|
||||||
/// serve_static_files!(router, [assets] => "/static");
|
/// serve_static_files!(router, [assets] => "/static");
|
||||||
|
|
@ -257,6 +262,7 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// # ) -> axum::response::Response { next.run(req).await }
|
/// # ) -> axum::response::Response { next.run(req).await }
|
||||||
/// pub struct MyAuth;
|
/// pub struct MyAuth;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyAuth {
|
/// impl Extension for MyAuth {
|
||||||
/// fn configure_middleware(&self, router: Router) -> Router {
|
/// fn configure_middleware(&self, router: Router) -> Router {
|
||||||
/// router.layer(middleware::from_fn(session_middleware))
|
/// router.layer(middleware::from_fn(session_middleware))
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
//! Los temas pueden definir sus propias implementaciones de [`Template`] y [`Region`] (por ejemplo,
|
//! Los temas pueden definir sus propias implementaciones de [`Template`] y [`Region`] (por ejemplo,
|
||||||
//! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas.
|
//! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas.
|
||||||
|
|
||||||
|
use crate::async_trait;
|
||||||
use crate::core::component::Context;
|
use crate::core::component::Context;
|
||||||
use crate::html::{Markup, html};
|
use crate::html::{Markup, html};
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
|
|
@ -49,7 +50,8 @@ use crate::{AutoDefault, util};
|
||||||
///
|
///
|
||||||
/// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla
|
/// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla
|
||||||
/// ([`Template`]) al renderizar la página ([`Page`](crate::response::page::Page)).
|
/// ([`Template`]) al renderizar la página ([`Page`](crate::response::page::Page)).
|
||||||
pub trait Region {
|
#[async_trait]
|
||||||
|
pub trait Region: Send + Sync {
|
||||||
/// Devuelve el nombre de la región.
|
/// Devuelve el nombre de la región.
|
||||||
///
|
///
|
||||||
/// Este nombre es el identificador lógico de la región y se usa como clave en el [`Context`]
|
/// Este nombre es el identificador lógico de la región y se usa como clave en el [`Context`]
|
||||||
|
|
@ -81,12 +83,9 @@ pub trait Region {
|
||||||
///
|
///
|
||||||
/// Se puede sobrescribir este método para modificar la estructura del contenedor, las clases
|
/// Se puede sobrescribir este método para modificar la estructura del contenedor, las clases
|
||||||
/// utilizadas o la semántica del marcado generado para cada región.
|
/// utilizadas o la semántica del marcado generado para cada región.
|
||||||
fn render(&'static self, cx: &mut Context) -> Markup
|
async fn render(&self, cx: &mut Context) -> Markup {
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
html! {
|
html! {
|
||||||
@let region = cx.render_region(self);
|
@let region = cx.render_region_named(self.name()).await;
|
||||||
@if !region.is_empty() {
|
@if !region.is_empty() {
|
||||||
div
|
div
|
||||||
class=(util::join!("region region-", self.name()))
|
class=(util::join!("region region-", self.name()))
|
||||||
|
|
@ -157,7 +156,8 @@ impl Region for DefaultRegion {
|
||||||
/// Una `Template` puede proporcionar una o más variantes para decidir la composición del `<body>`
|
/// Una `Template` puede proporcionar una o más variantes para decidir la composición del `<body>`
|
||||||
/// de una página ([`Page`](crate::response::page::Page)). El tema utiliza esta información para
|
/// de una página ([`Page`](crate::response::page::Page)). El tema utiliza esta información para
|
||||||
/// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden.
|
/// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden.
|
||||||
pub trait Template {
|
#[async_trait]
|
||||||
|
pub trait Template: Send + Sync {
|
||||||
/// Renderiza el contenido de la plantilla.
|
/// Renderiza el contenido de la plantilla.
|
||||||
///
|
///
|
||||||
/// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden:
|
/// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden:
|
||||||
|
|
@ -173,11 +173,11 @@ pub trait Template {
|
||||||
/// Este método se invoca normalmente desde [`Theme::render_page_body()`] para generar el
|
/// Este método se invoca normalmente desde [`Theme::render_page_body()`] para generar el
|
||||||
/// contenido del `<body>` de una página según la plantilla devuelta por el contexto de la
|
/// contenido del `<body>` de una página según la plantilla devuelta por el contexto de la
|
||||||
/// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())).
|
/// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())).
|
||||||
fn render(&'static self, cx: &mut Context) -> Markup {
|
async fn render(&self, cx: &mut Context) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
(DefaultRegion::Header.render(cx))
|
(DefaultRegion::Header.render(cx).await)
|
||||||
(DefaultRegion::Content.render(cx))
|
(DefaultRegion::Content.render(cx).await)
|
||||||
(DefaultRegion::Footer.render(cx))
|
(DefaultRegion::Footer.render(cx).await)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,6 +204,7 @@ pub enum DefaultTemplate {
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Template for DefaultTemplate {}
|
impl Template for DefaultTemplate {}
|
||||||
|
|
||||||
// **< render_component! >**************************************************************************
|
// **< render_component! >**************************************************************************
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::async_trait;
|
||||||
use crate::base::component::{Html, Intro, IntroOpening};
|
use crate::base::component::{Html, Intro, IntroOpening};
|
||||||
use crate::core::component::{ChildOp, Component, ComponentError, Context, Contextual};
|
use crate::core::component::{ChildOp, Component, ComponentError, Context, Contextual};
|
||||||
use crate::core::extension::Extension;
|
use crate::core::extension::Extension;
|
||||||
|
|
@ -28,6 +29,7 @@ use crate::web::http::StatusCode;
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// pub struct MyTheme;
|
/// pub struct MyTheme;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyTheme {
|
/// impl Extension for MyTheme {
|
||||||
/// fn name(&self) -> L10n {
|
/// fn name(&self) -> L10n {
|
||||||
/// L10n::n("My theme")
|
/// L10n::n("My theme")
|
||||||
|
|
@ -42,8 +44,10 @@ use crate::web::http::StatusCode;
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Theme for MyTheme {}
|
/// impl Theme for MyTheme {}
|
||||||
/// ```
|
/// ```
|
||||||
|
#[async_trait]
|
||||||
pub trait Theme: Extension + Send + Sync {
|
pub trait Theme: Extension + Send + Sync {
|
||||||
/// Devuelve el tema padre del que hereda este tema, si existe.
|
/// Devuelve el tema padre del que hereda este tema, si existe.
|
||||||
///
|
///
|
||||||
|
|
@ -108,11 +112,11 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// - Envolver el contenido en contenedores adicionales.
|
/// - Envolver el contenido en contenedores adicionales.
|
||||||
/// - Implementar lógicas de composición alternativas.
|
/// - Implementar lógicas de composición alternativas.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn render_page_body(&self, page: &mut Page) -> Markup {
|
async fn render_page_body(&self, page: &mut Page) -> Markup {
|
||||||
if let Some(parent) = self.parent() {
|
if let Some(parent) = self.parent() {
|
||||||
parent.render_page_body(page)
|
parent.render_page_body(page).await
|
||||||
} else {
|
} else {
|
||||||
page.template().render(page.context())
|
page.template().render(page.context()).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,9 +156,9 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
///
|
///
|
||||||
/// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo,
|
/// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo,
|
||||||
/// *favicons* personalizados, manifest, etiquetas de analítica, etc.).
|
/// *favicons* personalizados, manifest, etiquetas de analítica, etc.).
|
||||||
fn render_page_head(&self, page: &mut Page) -> Markup {
|
async fn render_page_head(&self, page: &mut Page) -> Markup {
|
||||||
if let Some(parent) = self.parent() {
|
if let Some(parent) = self.parent() {
|
||||||
return parent.render_page_head(page);
|
return parent.render_page_head(page).await;
|
||||||
}
|
}
|
||||||
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
|
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
|
||||||
html! {
|
html! {
|
||||||
|
|
@ -222,7 +226,7 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn handle_component(
|
async fn handle_component(
|
||||||
&self,
|
&self,
|
||||||
component: &mut dyn Component,
|
component: &mut dyn Component,
|
||||||
cx: &mut Context,
|
cx: &mut Context,
|
||||||
|
|
|
||||||
|
|
@ -61,39 +61,37 @@ impl ChildrenInRegions {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construye una lista de componentes frescos para la región indicada.
|
/// Ensambla los componentes frescos de la región indicada.
|
||||||
///
|
///
|
||||||
/// El orden es: prototipos globales comunes → children propios de la página →
|
/// Se recogen desde tres fuentes disponibles, en el siguiente orden:
|
||||||
/// prototipos específicos del tema activo.
|
|
||||||
///
|
///
|
||||||
/// Los prototipos globales se clonan en cada llamada (clon profundo gracias a
|
/// 1. Prototipos globales comunes, disponibles en cualquier tema. Se clonan en cada petición
|
||||||
/// [`ComponentClone`]), garantizando que `setup()` siempre parte del estado
|
/// para que `setup()` parta siempre de un estado inicial limpio.
|
||||||
/// inicial. Los children propios de la página se mueven (son por petición y no necesitan
|
/// 2. Componentes propios de la página, registrados para esta petición concreta. Se mueven en
|
||||||
/// clonarse).
|
/// 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
|
||||||
/// [`ComponentClone`]: crate::core::component::ComponentClone
|
/// que llegan a `setup()` con el mismo estado inicial.
|
||||||
pub fn children_for(&mut self, theme_ref: ThemeRef, region_ref: RegionRef) -> Children {
|
pub fn assemble_region(&mut self, theme_ref: ThemeRef, region_name: &str) -> Children {
|
||||||
let name = region_ref.name();
|
|
||||||
let common = COMMON_REGIONS.read();
|
let common = COMMON_REGIONS.read();
|
||||||
let themed = THEME_REGIONS.read();
|
let themed = THEME_REGIONS.read();
|
||||||
|
|
||||||
let mut result = Children::new();
|
let mut result = Children::new();
|
||||||
|
|
||||||
// 1. Prototipos globales comunes - clon fresco por cada página.
|
// 1. Prototipos globales comunes.
|
||||||
if let Some(protos) = common.get(name) {
|
if let Some(protos) = common.get(region_name) {
|
||||||
for proto in protos {
|
for proto in protos {
|
||||||
result.add(proto.as_child());
|
result.add(proto.as_child());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2. Children propios de la página - se mueven (son por petición, no requieren clonado).
|
// 2. Componentes propios de la página: se mueven, no se clonan.
|
||||||
if let Some(page_children) = self.0.remove(name) {
|
if let Some(page_children) = self.0.remove(region_name) {
|
||||||
for child in page_children {
|
for child in page_children {
|
||||||
result.add(child);
|
result.add(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 3. Prototipos del tema activo - clon fresco por cada página.
|
// 3. Prototipos del tema activo.
|
||||||
if let Some(theme_map) = themed.get(&theme_ref.type_id()) {
|
if let Some(theme_map) = themed.get(&theme_ref.type_id()) {
|
||||||
if let Some(protos) = theme_map.get(name) {
|
if let Some(protos) = theme_map.get(region_name) {
|
||||||
for proto in protos {
|
for proto in protos {
|
||||||
result.add(proto.as_child());
|
result.add(proto.as_child());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,16 @@ use crate::locale::L10n;
|
||||||
/// fn render_logo(cx: &mut Context) -> Markup {
|
/// fn render_logo(cx: &mut Context) -> Markup {
|
||||||
/// html! {
|
/// html! {
|
||||||
/// div class="logo_color" {
|
/// div class="logo_color" {
|
||||||
/// (PageTopSvg::Color.render(cx))
|
/// (PageTopSvg::Color.markup(cx))
|
||||||
/// }
|
/// }
|
||||||
/// div class="line_dark" {
|
/// div class="line_dark" {
|
||||||
/// (PageTopSvg::LineDark.render(cx))
|
/// (PageTopSvg::LineDark.markup(cx))
|
||||||
/// }
|
/// }
|
||||||
/// div class="line_light" {
|
/// div class="line_light" {
|
||||||
/// (PageTopSvg::LineLight.render(cx))
|
/// (PageTopSvg::LineLight.markup(cx))
|
||||||
/// }
|
/// }
|
||||||
/// div class="line_red" {
|
/// div class="line_red" {
|
||||||
/// (PageTopSvg::LineRGB(255, 0, 0).render(cx))
|
/// (PageTopSvg::LineRGB(255, 0, 0).markup(cx))
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// };
|
/// };
|
||||||
|
|
@ -41,8 +41,8 @@ pub enum PageTopSvg {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PageTopSvg {
|
impl PageTopSvg {
|
||||||
/// Renderiza el SVG del logotipo según la variante elegida.
|
/// Devuelve el marcado SVG del logotipo según la variante elegida.
|
||||||
pub fn render(&self, cx: &Context) -> Markup {
|
pub fn markup(&self, cx: &Context) -> Markup {
|
||||||
let path_fills = match self {
|
let path_fills = match self {
|
||||||
Self::Color => self.logo_color(),
|
Self::Color => self.logo_color(),
|
||||||
Self::LineDark => self.logo_line(10, 11, 9),
|
Self::LineDark => self.logo_line(10, 11, 9),
|
||||||
|
|
|
||||||
|
|
@ -162,10 +162,11 @@ impl PropsOp {
|
||||||
/// props: Props,
|
/// props: Props,
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Component for MyButton {
|
/// impl Component for MyButton {
|
||||||
/// fn new() -> Self { Self::default() }
|
/// fn new() -> Self { Self::default() }
|
||||||
///
|
///
|
||||||
/// fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
/// async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
/// Ok(html! {
|
/// Ok(html! {
|
||||||
/// button (self.props()) {
|
/// button (self.props()) {
|
||||||
/// (self.label().using(cx))
|
/// (self.label().using(cx))
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct HelloWorld;
|
struct HelloWorld;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for HelloWorld {
|
impl Extension for HelloWorld {
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
router.route("/", web::get(hello_world))
|
router.route("/", web::get(hello_world))
|
||||||
|
|
@ -61,7 +62,7 @@ impl Extension for HelloWorld {
|
||||||
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
.with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||||
.render()
|
.render().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
|
|
@ -97,12 +98,14 @@ use std::ops::Deref;
|
||||||
///
|
///
|
||||||
/// pub struct MyTheme;
|
/// pub struct MyTheme;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyTheme {
|
/// impl Extension for MyTheme {
|
||||||
/// fn theme(&self) -> Option<ThemeRef> {
|
/// fn theme(&self) -> Option<ThemeRef> {
|
||||||
/// Some(&Self)
|
/// Some(&Self)
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Theme for MyTheme {
|
/// impl Theme for MyTheme {
|
||||||
/// fn before_render_page_body(&self, page: &mut Page) {
|
/// fn before_render_page_body(&self, page: &mut Page) {
|
||||||
/// page
|
/// page
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
pub use error::ErrorPage;
|
pub use error::ErrorPage;
|
||||||
|
pub(crate) use error::render_error_pages;
|
||||||
|
|
||||||
use crate::auth::CurrentUser;
|
use crate::auth::CurrentUser;
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
|
|
@ -200,7 +201,7 @@ impl Page {
|
||||||
/// `lang` y `dir` en la etiqueta `<html>`.
|
/// `lang` y `dir` en la etiqueta `<html>`.
|
||||||
/// 8. Compone el documento HTML completo (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) y
|
/// 8. Compone el documento HTML completo (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) y
|
||||||
/// devuelve un [`Result`] con el [`Markup`] final.
|
/// devuelve un [`Result`] con el [`Markup`] final.
|
||||||
pub fn render(&mut self) -> Result<Markup, ErrorPage> {
|
pub async fn render(&mut self) -> Result<Markup, ErrorPage> {
|
||||||
// Acciones específicas del tema antes de renderizar el <body>.
|
// Acciones específicas del tema antes de renderizar el <body>.
|
||||||
self.context.theme().before_render_page_body(self);
|
self.context.theme().before_render_page_body(self);
|
||||||
|
|
||||||
|
|
@ -209,9 +210,9 @@ impl Page {
|
||||||
|
|
||||||
// Renderiza el <body>.
|
// Renderiza el <body>.
|
||||||
let body = html! {
|
let body = html! {
|
||||||
(ReservedRegion::PageTop.render(&mut self.context))
|
(ReservedRegion::PageTop.render(&mut self.context).await)
|
||||||
(self.context.theme().render_page_body(self))
|
(self.context.theme().render_page_body(self).await)
|
||||||
(ReservedRegion::PageBottom.render(&mut self.context))
|
(ReservedRegion::PageBottom.render(&mut self.context).await)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Acciones específicas del tema después de renderizar el <body>.
|
// Acciones específicas del tema después de renderizar el <body>.
|
||||||
|
|
@ -221,7 +222,7 @@ impl Page {
|
||||||
action::page::AfterRenderBody::dispatch(self);
|
action::page::AfterRenderBody::dispatch(self);
|
||||||
|
|
||||||
// Renderiza el <head>.
|
// Renderiza el <head>.
|
||||||
let head = self.context.theme().render_page_head(self);
|
let head = self.context.theme().render_page_head(self).await;
|
||||||
|
|
||||||
// Compone la página incluyendo los atributos de idioma y dirección del texto.
|
// Compone la página incluyendo los atributos de idioma y dirección del texto.
|
||||||
let lang = &self.context.langid().language;
|
let lang = &self.context.langid().language;
|
||||||
|
|
@ -283,7 +284,7 @@ impl Contextual for Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
|
fn with_param<T: Send + Sync + 'static>(mut self, key: &'static str, value: T) -> Self {
|
||||||
self.context.alter_param(key, value);
|
self.context.alter_param(key, value);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
use axum::extract::Request;
|
||||||
|
use axum::middleware::Next;
|
||||||
|
|
||||||
use crate::core::component::Contextual;
|
use crate::core::component::Contextual;
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
@ -5,20 +8,18 @@ use crate::web::{HttpRequest, IntoResponse, Response, http};
|
||||||
|
|
||||||
use super::Page;
|
use super::Page;
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// Página de error asociada a un código de estado HTTP.
|
/// Página de error asociada a un código de estado HTTP.
|
||||||
///
|
///
|
||||||
/// Este enumerado agrupa tipos esenciales de error que pueden devolverse como página HTML completa.
|
/// Este enumerado agrupa tipos esenciales de error que pueden devolverse como página HTML completa.
|
||||||
/// Cada variante encapsula la solicitud original ([`HttpRequest`]) y se corresponde con un código
|
/// Cada variante encapsula la solicitud original ([`HttpRequest`]) y se corresponde con un código
|
||||||
/// de estado concreto.
|
/// de estado concreto.
|
||||||
///
|
///
|
||||||
/// Para cada error se construye una [`Page`] usando el tema activo, lo que permite personalizar
|
/// Para cada error se construye una [`Page`] usando el tema activo, lo que permite personalizar la
|
||||||
/// la plantilla y el contenido del mensaje mediante los métodos específicos del tema
|
/// plantilla y el contenido del mensaje mediante los métodos específicos del tema (por ejemplo,
|
||||||
/// (por ejemplo, [`Theme::error_403()`](crate::core::theme::Theme::error_403),
|
/// [`Theme::error_403()`](crate::core::theme::Theme::error_403),
|
||||||
/// [`Theme::error_404()`](crate::core::theme::Theme::error_404) o
|
/// [`Theme::error_404()`](crate::core::theme::Theme::error_404) o
|
||||||
/// [`Theme::error_fatal()`](crate::core::theme::Theme::error_fatal)).
|
/// [`Theme::error_fatal()`](crate::core::theme::Theme::error_fatal)).
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ErrorPage {
|
pub enum ErrorPage {
|
||||||
BadRequest(HttpRequest),
|
BadRequest(HttpRequest),
|
||||||
AccessDenied(HttpRequest),
|
AccessDenied(HttpRequest),
|
||||||
|
|
@ -29,26 +30,6 @@ pub enum ErrorPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorPage {
|
impl ErrorPage {
|
||||||
// Renderiza una página de error genérica usando el tema activo. Deriva las claves de
|
|
||||||
// localización del código de estado (`error<code>_title`, `_alert`, `_help`). Si el
|
|
||||||
// renderizado falla, escribe el texto plano del código de estado.
|
|
||||||
fn display_error_page(&self, f: &mut fmt::Formatter<'_>, request: &HttpRequest) -> fmt::Result {
|
|
||||||
let mut page = Page::new(request.clone());
|
|
||||||
let code = self.status_code();
|
|
||||||
page.theme().error_fatal(
|
|
||||||
&mut page,
|
|
||||||
code,
|
|
||||||
L10n::l(util::join!("error", code.as_str(), "_title")),
|
|
||||||
L10n::l(util::join!("error", code.as_str(), "_alert")),
|
|
||||||
L10n::l(util::join!("error", code.as_str(), "_help")),
|
|
||||||
);
|
|
||||||
if let Ok(rendered) = page.render() {
|
|
||||||
write!(f, "{}", rendered.into_string())
|
|
||||||
} else {
|
|
||||||
f.write_str(code.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el código de estado HTTP asociado a la variante de error.
|
/// Devuelve el código de estado HTTP asociado a la variante de error.
|
||||||
pub fn status_code(&self) -> http::StatusCode {
|
pub fn status_code(&self) -> http::StatusCode {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -60,59 +41,68 @@ impl ErrorPage {
|
||||||
ErrorPage::GatewayTimeout(_) => http::StatusCode::GATEWAY_TIMEOUT,
|
ErrorPage::GatewayTimeout(_) => http::StatusCode::GATEWAY_TIMEOUT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ErrorPage {
|
// Renderiza la página de error y construye la respuesta HTTP completa con el HTML generado. Si
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
// el renderizado falla, devuelve sólo el código de estado sin cuerpo.
|
||||||
match self {
|
async fn render_html(self) -> Response {
|
||||||
// Error 400.
|
let status = self.status_code();
|
||||||
Self::BadRequest(request) => self.display_error_page(f, request),
|
let mut page = match self {
|
||||||
|
|
||||||
// Error 403.
|
|
||||||
Self::AccessDenied(request) => {
|
Self::AccessDenied(request) => {
|
||||||
let mut page = Page::new(request.clone());
|
let mut page = Page::new(request);
|
||||||
page.theme().error_403(&mut page);
|
page.theme().error_403(&mut page);
|
||||||
if let Ok(rendered) = page.render() {
|
page
|
||||||
write!(f, "{}", rendered.into_string())
|
|
||||||
} else {
|
|
||||||
f.write_str(self.status_code().as_str())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Error 404.
|
|
||||||
Self::NotFound(request) => {
|
Self::NotFound(request) => {
|
||||||
let mut page = Page::new(request.clone());
|
let mut page = Page::new(request);
|
||||||
page.theme().error_404(&mut page);
|
page.theme().error_404(&mut page);
|
||||||
if let Ok(rendered) = page.render() {
|
page
|
||||||
write!(f, "{}", rendered.into_string())
|
|
||||||
} else {
|
|
||||||
f.write_str(self.status_code().as_str())
|
|
||||||
}
|
}
|
||||||
|
Self::BadRequest(request)
|
||||||
|
| Self::InternalError(request)
|
||||||
|
| Self::ServiceUnavailable(request)
|
||||||
|
| Self::GatewayTimeout(request) => {
|
||||||
|
let mut page = Page::new(request);
|
||||||
|
page.theme().error_fatal(
|
||||||
|
&mut page,
|
||||||
|
status,
|
||||||
|
L10n::l(util::join!("error", status.as_str(), "_title")),
|
||||||
|
L10n::l(util::join!("error", status.as_str(), "_alert")),
|
||||||
|
L10n::l(util::join!("error", status.as_str(), "_help")),
|
||||||
|
);
|
||||||
|
page
|
||||||
}
|
}
|
||||||
|
};
|
||||||
// Error 500.
|
match page.render().await {
|
||||||
Self::InternalError(request) => self.display_error_page(f, request),
|
Ok(rendered) => (
|
||||||
|
status,
|
||||||
// Error 503.
|
[(http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||||
Self::ServiceUnavailable(request) => self.display_error_page(f, request),
|
rendered.into_string(),
|
||||||
|
)
|
||||||
// Error 504.
|
.into_response(),
|
||||||
Self::GatewayTimeout(request) => self.display_error_page(f, request),
|
Err(_) => status.into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convierte un [`ErrorPage`] en una respuesta HTTP con el código de estado adecuado y el cuerpo
|
/// Convierte un [`ErrorPage`] en una respuesta HTTP que luego se convertirá en una página HTML
|
||||||
/// HTML generado por el tema activo.
|
/// completa usando el tema activo.
|
||||||
impl IntoResponse for ErrorPage {
|
impl IntoResponse for ErrorPage {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let status = self.status_code();
|
let status = self.status_code();
|
||||||
let body = self.to_string();
|
let mut response = status.into_response();
|
||||||
(
|
response.extensions_mut().insert(self);
|
||||||
status,
|
response
|
||||||
[(http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intercepta respuestas con un [`ErrorPage`] pendiente y las convierte en páginas HTML completas
|
||||||
|
// usando el tema activo.
|
||||||
|
//
|
||||||
|
// Se registra globalmente sobre el router principal desde [`crate::app`].
|
||||||
|
pub(crate) async fn render_error_pages(req: Request, next: Next) -> Response {
|
||||||
|
let mut response = next.run(req).await;
|
||||||
|
if let Some(error_page) = response.extensions_mut().remove::<ErrorPage>() {
|
||||||
|
return error_page.render_html().await;
|
||||||
|
}
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@ impl tower::Service<http::Request<Body>> for ServeEmbedded {
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// pub struct MyExtension;
|
/// pub struct MyExtension;
|
||||||
///
|
///
|
||||||
|
/// #[async_trait]
|
||||||
/// impl Extension for MyExtension {
|
/// impl Extension for MyExtension {
|
||||||
/// fn configure_router(&self, router: Router) -> Router {
|
/// fn configure_router(&self, router: Router) -> Router {
|
||||||
/// // Forma 1) Sistema de ficheros o embebido.
|
/// // Forma 1) Sistema de ficheros o embebido.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ struct TestComp {
|
||||||
text: String,
|
text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for TestComp {
|
impl Component for TestComp {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -20,7 +21,7 @@ impl Component for TestComp {
|
||||||
self.props.get_id()
|
self.props.get_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(html! { (self.text) })
|
Ok(html! { (self.text) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,27 +49,30 @@ impl TestComp {
|
||||||
async fn child_default_is_empty() {
|
async fn child_default_is_empty() {
|
||||||
let child = Child::default();
|
let child = Child::default();
|
||||||
assert!(child.id().is_none());
|
assert!(child.id().is_none());
|
||||||
assert!(child.render(&mut Context::default()).is_empty());
|
assert!(child.render(&mut Context::default()).await.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn child_with_stores_component_and_renders_it() {
|
async fn child_with_stores_component_and_renders_it() {
|
||||||
let child = Child::with(TestComp::text("hola"));
|
let child = Child::with(TestComp::text("hello"));
|
||||||
assert_eq!(child.render(&mut Context::default()).into_string(), "hola");
|
assert_eq!(
|
||||||
|
child.render(&mut Context::default()).await.into_string(),
|
||||||
|
"hello"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn child_id_returns_component_id() {
|
async fn child_id_returns_component_id() {
|
||||||
let child = Child::with(TestComp::tagged("my-id", "texto"));
|
let child = Child::with(TestComp::tagged("my-id", "text"));
|
||||||
assert_eq!(child.id(), Some("my-id".to_string()));
|
assert_eq!(child.id(), Some("my-id".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn child_from_component_is_equivalent_to_with() {
|
async fn child_from_component_is_equivalent_to_with() {
|
||||||
let child: Child = TestComp::text("desde from").into();
|
let child: Child = TestComp::text("from-trait").into();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
child.render(&mut Context::default()).into_string(),
|
child.render(&mut Context::default()).await.into_string(),
|
||||||
"desde from"
|
"from-trait"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,11 +82,11 @@ async fn child_clone_is_deep() {
|
||||||
let original = Child::with(TestComp::text("original"));
|
let original = Child::with(TestComp::text("original"));
|
||||||
let clone = original.clone();
|
let clone = original.clone();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
original.render(&mut Context::default()).into_string(),
|
original.render(&mut Context::default()).await.into_string(),
|
||||||
"original"
|
"original"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
clone.render(&mut Context::default()).into_string(),
|
clone.render(&mut Context::default()).await.into_string(),
|
||||||
"original"
|
"original"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +107,7 @@ async fn children_add_appends_in_order() {
|
||||||
.with_child(TestComp::text("b"))
|
.with_child(TestComp::text("b"))
|
||||||
.with_child(TestComp::text("c"));
|
.with_child(TestComp::text("c"));
|
||||||
assert_eq!(c.len(), 3);
|
assert_eq!(c.len(), 3);
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "abc");
|
assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -111,13 +115,13 @@ async fn children_add_if_empty_only_adds_when_list_is_empty() {
|
||||||
let mut cx = Context::default();
|
let mut cx = Context::default();
|
||||||
|
|
||||||
// Se añade porque la lista está vacía.
|
// Se añade porque la lista está vacía.
|
||||||
let c = Children::new().with_child(ChildOp::AddIfEmpty(TestComp::text("primero").into()));
|
let c = Children::new().with_child(ChildOp::AddIfEmpty(TestComp::text("first").into()));
|
||||||
assert_eq!(c.len(), 1);
|
assert_eq!(c.len(), 1);
|
||||||
|
|
||||||
// No se añade porque ya hay un elemento.
|
// No se añade porque ya hay un elemento.
|
||||||
let c = c.with_child(ChildOp::AddIfEmpty(TestComp::text("segundo").into()));
|
let c = c.with_child(ChildOp::AddIfEmpty(TestComp::text("second").into()));
|
||||||
assert_eq!(c.len(), 1);
|
assert_eq!(c.len(), 1);
|
||||||
assert_eq!(c.render(&mut cx).into_string(), "primero");
|
assert_eq!(c.render(&mut cx).await.into_string(), "first");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -128,7 +132,7 @@ async fn children_add_many_appends_all_in_order() {
|
||||||
TestComp::text("z").into(),
|
TestComp::text("z").into(),
|
||||||
]));
|
]));
|
||||||
assert_eq!(c.len(), 3);
|
assert_eq!(c.len(), 3);
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "xyz");
|
assert_eq!(c.render(&mut Context::default()).await.into_string(), "xyz");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -136,7 +140,7 @@ async fn children_prepend_inserts_at_start() {
|
||||||
let c = Children::new()
|
let c = Children::new()
|
||||||
.with_child(TestComp::text("b"))
|
.with_child(TestComp::text("b"))
|
||||||
.with_child(ChildOp::Prepend(TestComp::text("a").into()));
|
.with_child(ChildOp::Prepend(TestComp::text("a").into()));
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "ab");
|
assert_eq!(c.render(&mut Context::default()).await.into_string(), "ab");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -147,7 +151,7 @@ async fn children_prepend_many_inserts_all_at_start_maintaining_order() {
|
||||||
TestComp::text("a").into(),
|
TestComp::text("a").into(),
|
||||||
TestComp::text("b").into(),
|
TestComp::text("b").into(),
|
||||||
]));
|
]));
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "abc");
|
assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -156,7 +160,7 @@ async fn children_insert_after_id_inserts_after_matching_element() {
|
||||||
.with_child(TestComp::tagged("first", "a"))
|
.with_child(TestComp::tagged("first", "a"))
|
||||||
.with_child(TestComp::text("c"))
|
.with_child(TestComp::text("c"))
|
||||||
.with_child(ChildOp::InsertAfterId("first", TestComp::text("b").into()));
|
.with_child(ChildOp::InsertAfterId("first", TestComp::text("b").into()));
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "abc");
|
assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -164,10 +168,10 @@ async fn children_insert_after_id_appends_when_id_not_found() {
|
||||||
let c = Children::new()
|
let c = Children::new()
|
||||||
.with_child(TestComp::text("a"))
|
.with_child(TestComp::text("a"))
|
||||||
.with_child(ChildOp::InsertAfterId(
|
.with_child(ChildOp::InsertAfterId(
|
||||||
"no-existe",
|
"not-found",
|
||||||
TestComp::text("b").into(),
|
TestComp::text("b").into(),
|
||||||
));
|
));
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "ab");
|
assert_eq!(c.render(&mut Context::default()).await.into_string(), "ab");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -176,7 +180,7 @@ async fn children_insert_before_id_inserts_before_matching_element() {
|
||||||
.with_child(TestComp::text("a"))
|
.with_child(TestComp::text("a"))
|
||||||
.with_child(TestComp::tagged("last", "c"))
|
.with_child(TestComp::tagged("last", "c"))
|
||||||
.with_child(ChildOp::InsertBeforeId("last", TestComp::text("b").into()));
|
.with_child(ChildOp::InsertBeforeId("last", TestComp::text("b").into()));
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "abc");
|
assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -184,10 +188,10 @@ async fn children_insert_before_id_prepends_when_id_not_found() {
|
||||||
let c = Children::new()
|
let c = Children::new()
|
||||||
.with_child(TestComp::text("b"))
|
.with_child(TestComp::text("b"))
|
||||||
.with_child(ChildOp::InsertBeforeId(
|
.with_child(ChildOp::InsertBeforeId(
|
||||||
"no-existe",
|
"not-found",
|
||||||
TestComp::text("a").into(),
|
TestComp::text("a").into(),
|
||||||
));
|
));
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "ab");
|
assert_eq!(c.render(&mut Context::default()).await.into_string(), "ab");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -198,28 +202,28 @@ async fn children_remove_by_id_removes_first_matching_element() {
|
||||||
.with_child(TestComp::text("c"))
|
.with_child(TestComp::text("c"))
|
||||||
.with_child(ChildOp::RemoveById("drop"));
|
.with_child(ChildOp::RemoveById("drop"));
|
||||||
assert_eq!(c.len(), 2);
|
assert_eq!(c.len(), 2);
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "ac");
|
assert_eq!(c.render(&mut Context::default()).await.into_string(), "ac");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn children_remove_by_id_does_nothing_when_id_not_found() {
|
async fn children_remove_by_id_does_nothing_when_id_not_found() {
|
||||||
let c = Children::new()
|
let c = Children::new()
|
||||||
.with_child(TestComp::text("a"))
|
.with_child(TestComp::text("a"))
|
||||||
.with_child(ChildOp::RemoveById("no-existe"));
|
.with_child(ChildOp::RemoveById("not-found"));
|
||||||
assert_eq!(c.len(), 1);
|
assert_eq!(c.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn children_replace_by_id_replaces_first_matching_element() {
|
async fn children_replace_by_id_replaces_first_matching_element() {
|
||||||
let c = Children::new()
|
let c = Children::new()
|
||||||
.with_child(TestComp::tagged("target", "viejo"))
|
.with_child(TestComp::tagged("target", "old"))
|
||||||
.with_child(TestComp::text("b"))
|
.with_child(TestComp::text("b"))
|
||||||
.with_child(ChildOp::ReplaceById(
|
.with_child(ChildOp::ReplaceById("target", TestComp::text("new").into()));
|
||||||
"target",
|
|
||||||
TestComp::text("nuevo").into(),
|
|
||||||
));
|
|
||||||
assert_eq!(c.len(), 2);
|
assert_eq!(c.len(), 2);
|
||||||
assert_eq!(c.render(&mut Context::default()).into_string(), "nuevob");
|
assert_eq!(
|
||||||
|
c.render(&mut Context::default()).await.into_string(),
|
||||||
|
"newb"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -234,11 +238,11 @@ async fn children_reset_clears_all_elements() {
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn children_get_by_id_returns_first_matching_child() {
|
async fn children_get_by_id_returns_first_matching_child() {
|
||||||
let c = Children::new()
|
let c = Children::new()
|
||||||
.with_child(TestComp::tagged("uno", "a"))
|
.with_child(TestComp::tagged("one", "a"))
|
||||||
.with_child(TestComp::tagged("dos", "b"));
|
.with_child(TestComp::tagged("two", "b"));
|
||||||
assert!(c.get_by_id("uno").is_some());
|
assert!(c.get_by_id("one").is_some());
|
||||||
assert!(c.get_by_id("dos").is_some());
|
assert!(c.get_by_id("two").is_some());
|
||||||
assert!(c.get_by_id("tres").is_none());
|
assert!(c.get_by_id("three").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -246,21 +250,21 @@ async fn children_iter_by_id_yields_all_matching_children() {
|
||||||
let c = Children::new()
|
let c = Children::new()
|
||||||
.with_child(TestComp::tagged("rep", "a"))
|
.with_child(TestComp::tagged("rep", "a"))
|
||||||
.with_child(TestComp::tagged("rep", "b"))
|
.with_child(TestComp::tagged("rep", "b"))
|
||||||
.with_child(TestComp::tagged("otro", "c"));
|
.with_child(TestComp::tagged("other", "c"));
|
||||||
assert_eq!(c.iter_by_id("rep").count(), 2);
|
assert_eq!(c.iter_by_id("rep").count(), 2);
|
||||||
assert_eq!(c.iter_by_id("otro").count(), 1);
|
assert_eq!(c.iter_by_id("other").count(), 1);
|
||||||
assert_eq!(c.iter_by_id("ninguno").count(), 0);
|
assert_eq!(c.iter_by_id("none").count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn children_render_concatenates_all_outputs_in_order() {
|
async fn children_render_concatenates_all_outputs_in_order() {
|
||||||
let c = Children::new()
|
let c = Children::new()
|
||||||
.with_child(TestComp::text("uno "))
|
.with_child(TestComp::text("one "))
|
||||||
.with_child(TestComp::text("dos "))
|
.with_child(TestComp::text("two "))
|
||||||
.with_child(TestComp::text("tres"));
|
.with_child(TestComp::text("three"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
c.render(&mut Context::default()).into_string(),
|
c.render(&mut Context::default()).await.into_string(),
|
||||||
"uno dos tres"
|
"one two three"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,29 +274,29 @@ async fn children_render_concatenates_all_outputs_in_order() {
|
||||||
async fn embed_default_is_empty() {
|
async fn embed_default_is_empty() {
|
||||||
let embed: Embed<TestComp> = Embed::default();
|
let embed: Embed<TestComp> = Embed::default();
|
||||||
assert!(embed.id().is_none());
|
assert!(embed.id().is_none());
|
||||||
assert!(embed.render(&mut Context::default()).is_empty());
|
assert!(embed.render(&mut Context::default()).await.is_empty());
|
||||||
assert!(embed.get().is_none());
|
assert!(embed.get().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn embed_with_stores_component() {
|
async fn embed_with_stores_component() {
|
||||||
let embed = Embed::with(TestComp::text("contenido"));
|
let embed = Embed::with(TestComp::text("content"));
|
||||||
assert!(embed.get().is_some());
|
assert!(embed.get().is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
embed.render(&mut Context::default()).into_string(),
|
embed.render(&mut Context::default()).await.into_string(),
|
||||||
"contenido"
|
"content"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn embed_id_returns_component_id() {
|
async fn embed_id_returns_component_id() {
|
||||||
let embed = Embed::with(TestComp::tagged("embed-id", "texto"));
|
let embed = Embed::with(TestComp::tagged("embed-id", "text"));
|
||||||
assert_eq!(embed.id(), Some("embed-id".to_string()));
|
assert_eq!(embed.id(), Some("embed-id".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn embed_get_is_some_when_component_present() {
|
async fn embed_get_is_some_when_component_present() {
|
||||||
let embed = Embed::with(TestComp::tagged("abc", "hola"));
|
let embed = Embed::with(TestComp::tagged("abc", "hello"));
|
||||||
// `get()` devuelve Some; la lectura del id verifica que accede al componente correctamente.
|
// `get()` devuelve Some; la lectura del id verifica que accede al componente correctamente.
|
||||||
assert!(embed.get().is_some());
|
assert!(embed.get().is_some());
|
||||||
assert_eq!(embed.id(), Some("abc".to_string()));
|
assert_eq!(embed.id(), Some("abc".to_string()));
|
||||||
|
|
@ -300,38 +304,36 @@ async fn embed_get_is_some_when_component_present() {
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn embed_get_allows_mutating_component() {
|
async fn embed_get_allows_mutating_component() {
|
||||||
let embed = Embed::with(TestComp::tagged("orig", "texto"));
|
let mut embed = Embed::with(TestComp::tagged("orig", "text"));
|
||||||
// El `;` final convierte el `if let` en sentencia y libera el guard antes que `embed`.
|
if let Some(comp) = embed.get_mut() {
|
||||||
if let Some(mut comp) = embed.get() {
|
|
||||||
comp.props
|
comp.props
|
||||||
.alter_prop(PropsOp::set_id("modificado".to_string()));
|
.alter_prop(PropsOp::set_id("modified".to_string()));
|
||||||
};
|
};
|
||||||
assert_eq!(embed.id(), Some("modificado".to_string()));
|
assert_eq!(embed.id(), Some("modified".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn embed_with_component_replaces_content() {
|
async fn embed_with_component_replaces_content() {
|
||||||
let embed =
|
let embed = Embed::with(TestComp::text("first")).with_component(Some(TestComp::text("second")));
|
||||||
Embed::with(TestComp::text("primero")).with_component(Some(TestComp::text("segundo")));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
embed.render(&mut Context::default()).into_string(),
|
embed.render(&mut Context::default()).await.into_string(),
|
||||||
"segundo"
|
"second"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn embed_with_component_none_empties_embed() {
|
async fn embed_with_component_none_empties_embed() {
|
||||||
let embed = Embed::with(TestComp::text("algo")).with_component(None);
|
let embed = Embed::with(TestComp::text("something")).with_component(None);
|
||||||
assert!(embed.get().is_none());
|
assert!(embed.get().is_none());
|
||||||
assert!(embed.render(&mut Context::default()).is_empty());
|
assert!(embed.render(&mut Context::default()).await.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn embed_clone_is_deep() {
|
async fn embed_clone_is_deep() {
|
||||||
let original = Embed::with(TestComp::tagged("orig", "texto"));
|
let original = Embed::with(TestComp::tagged("orig", "text"));
|
||||||
let clone = original.clone();
|
let mut clone = original.clone();
|
||||||
// Mutar el clon no debe afectar al original.
|
// Mutar el clon no debe afectar al original.
|
||||||
if let Some(mut comp) = clone.get() {
|
if let Some(comp) = clone.get_mut() {
|
||||||
comp.props
|
comp.props
|
||||||
.alter_prop(PropsOp::set_id("clone-id".to_string()));
|
.alter_prop(PropsOp::set_id("clone-id".to_string()));
|
||||||
}
|
}
|
||||||
|
|
@ -341,10 +343,10 @@ async fn embed_clone_is_deep() {
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn embed_converts_into_child() {
|
async fn embed_converts_into_child() {
|
||||||
let embed = Embed::with(TestComp::text("desde embed"));
|
let embed = Embed::with(TestComp::text("from embed"));
|
||||||
let child = Child::from(embed);
|
let child = Child::from(embed);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
child.render(&mut Context::default()).into_string(),
|
child.render(&mut Context::default()).await.into_string(),
|
||||||
"desde embed"
|
"from embed"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ async fn component_html_renders_static_markup() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let markup = component.render(&mut Context::default());
|
let markup = component.render(&mut Context::default()).await;
|
||||||
assert_eq!(markup.0, "<p>Test</p>");
|
assert_eq!(markup.into_string(), "<p>Test</p>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -23,8 +23,8 @@ async fn component_html_renders_using_context_param() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let markup = component.render(&mut cx);
|
let markup = component.render(&mut cx).await;
|
||||||
assert_eq!(markup.0, "<span>Alice</span>");
|
assert_eq!(markup.into_string(), "<span>Alice</span>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -33,16 +33,16 @@ async fn component_html_allows_replacing_render_function() {
|
||||||
|
|
||||||
component.alter_fn(|_| html! { div { "Modified" } });
|
component.alter_fn(|_| html! { div { "Modified" } });
|
||||||
|
|
||||||
let markup = component.render(&mut Context::default());
|
let markup = component.render(&mut Context::default()).await;
|
||||||
assert_eq!(markup.0, "<div>Modified</div>");
|
assert_eq!(markup.into_string(), "<div>Modified</div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn component_html_default_renders_empty_markup() {
|
async fn component_html_default_renders_empty_markup() {
|
||||||
let mut component = Html::default();
|
let mut component = Html::default();
|
||||||
|
|
||||||
let markup = component.render(&mut Context::default());
|
let markup = component.render(&mut Context::default()).await;
|
||||||
assert_eq!(markup.0, "");
|
assert_eq!(markup.into_string(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -60,6 +60,6 @@ async fn component_html_can_access_request_path() {
|
||||||
html! { span { (path) } }
|
html! { span { (path) } }
|
||||||
});
|
});
|
||||||
|
|
||||||
let markup = component.render(&mut cx);
|
let markup = component.render(&mut cx).await;
|
||||||
assert_eq!(markup.0, "<span>/hello/world</span>");
|
assert_eq!(markup.into_string(), "<span>/hello/world</span>");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,13 @@ async fn poweredby_default_shows_only_pagetop_recognition() {
|
||||||
setup().await;
|
setup().await;
|
||||||
|
|
||||||
let mut p = PoweredBy::default();
|
let mut p = PoweredBy::default();
|
||||||
let html = p.render(&mut Context::default());
|
let html = p.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
// Debe mostrar el bloque de reconocimiento a PageTop.
|
// Debe mostrar el bloque de reconocimiento a PageTop.
|
||||||
assert!(html.as_str().contains("poweredby__pagetop"));
|
assert!(html.contains("poweredby__pagetop"));
|
||||||
|
|
||||||
// Y NO debe mostrar el bloque de copyright.
|
// Y NO debe mostrar el bloque de copyright.
|
||||||
assert!(!html.as_str().contains("poweredby__copyright"));
|
assert!(!html.contains("poweredby__copyright"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -27,23 +27,20 @@ async fn poweredby_new_includes_current_year_and_app_name() {
|
||||||
setup().await;
|
setup().await;
|
||||||
|
|
||||||
let mut p = PoweredBy::new();
|
let mut p = PoweredBy::new();
|
||||||
let html = p.render(&mut Context::default());
|
let html = p.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
let year = Utc::now().format("%Y").to_string();
|
let year = Utc::now().format("%Y").to_string();
|
||||||
assert!(
|
assert!(html.contains(&year), "HTML should include the current year");
|
||||||
html.as_str().contains(&year),
|
|
||||||
"HTML should include the current year"
|
|
||||||
);
|
|
||||||
|
|
||||||
// El nombre de la app proviene de `global::SETTINGS.app.name`.
|
// El nombre de la app proviene de `global::SETTINGS.app.name`.
|
||||||
let app_name = &global::SETTINGS.app.name;
|
let app_name = &global::SETTINGS.app.name;
|
||||||
assert!(
|
assert!(
|
||||||
html.as_str().contains(app_name),
|
html.contains(app_name),
|
||||||
"HTML should include the application name"
|
"HTML should include the application name"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Debe existir el span de copyright.
|
// Debe existir el span de copyright.
|
||||||
assert!(html.as_str().contains("poweredby__copyright"));
|
assert!(html.contains("poweredby__copyright"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -52,10 +49,10 @@ async fn poweredby_with_copyright_overrides_text() {
|
||||||
|
|
||||||
let custom = "2001 © FooBar Inc.";
|
let custom = "2001 © FooBar Inc.";
|
||||||
let mut p = PoweredBy::default().with_copyright(Some(custom));
|
let mut p = PoweredBy::default().with_copyright(Some(custom));
|
||||||
let html = p.render(&mut Context::default());
|
let html = p.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
assert!(html.as_str().contains(custom));
|
assert!(html.contains(custom));
|
||||||
assert!(html.as_str().contains("poweredby__copyright"));
|
assert!(html.contains("poweredby__copyright"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -63,11 +60,11 @@ async fn poweredby_with_copyright_none_hides_text() {
|
||||||
setup().await;
|
setup().await;
|
||||||
|
|
||||||
let mut p = PoweredBy::new().with_copyright(None::<String>);
|
let mut p = PoweredBy::new().with_copyright(None::<String>);
|
||||||
let html = p.render(&mut Context::default());
|
let html = p.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
assert!(!html.as_str().contains("poweredby__copyright"));
|
assert!(!html.contains("poweredby__copyright"));
|
||||||
// El reconocimiento a PageTop siempre debe aparecer.
|
// El reconocimiento a PageTop siempre debe aparecer.
|
||||||
assert!(html.as_str().contains("poweredby__pagetop"));
|
assert!(html.contains("poweredby__pagetop"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -75,10 +72,10 @@ async fn poweredby_link_points_to_crates_io() {
|
||||||
setup().await;
|
setup().await;
|
||||||
|
|
||||||
let mut p = PoweredBy::default();
|
let mut p = PoweredBy::default();
|
||||||
let html = p.render(&mut Context::default());
|
let html = p.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
html.as_str().contains("https://pagetop.cillero.es"),
|
html.contains("https://pagetop.cillero.es"),
|
||||||
"Link should point to pagetop.cillero.es"
|
"Link should point to pagetop.cillero.es"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ struct TestMarkupComponent {
|
||||||
markup: Markup,
|
markup: Markup,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Component for TestMarkupComponent {
|
impl Component for TestMarkupComponent {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
|
@ -17,7 +18,7 @@ impl Component for TestMarkupComponent {
|
||||||
cx.param_or::<bool>("renderable", true)
|
cx.param_or::<bool>("renderable", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(self.markup.clone())
|
Ok(self.markup.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +75,7 @@ async fn non_renderable_component_produces_empty_markup() {
|
||||||
let mut comp = TestMarkupComponent {
|
let mut comp = TestMarkupComponent {
|
||||||
markup: html! { p { "Should never be rendered" } },
|
markup: html! { p { "Should never be rendered" } },
|
||||||
};
|
};
|
||||||
assert_eq!(comp.render(&mut cx).into_string(), "");
|
assert_eq!(comp.render(&mut cx).await.into_string(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -93,7 +94,7 @@ async fn markup_from_component_equals_markup_reinjected_in_html_macro() {
|
||||||
let mut comp = TestMarkupComponent {
|
let mut comp = TestMarkupComponent {
|
||||||
markup: markup.clone(),
|
markup: markup.clone(),
|
||||||
};
|
};
|
||||||
comp.render(&mut cx).into_string()
|
comp.render(&mut cx).await.into_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Vía 2: reinyectamos el Markup en `html!` directamente.
|
// Vía 2: reinyectamos el Markup en `html!` directamente.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue