Compare commits
4 commits
04e3d5b3c2
...
f1295c74df
| Author | SHA1 | Date | |
|---|---|---|---|
| f1295c74df | |||
| d10d546418 | |||
| cd9454a729 | |||
| 54f990b11c |
47 changed files with 1016 additions and 555 deletions
|
|
@ -14,7 +14,7 @@ async fn hello_name(
|
||||||
) -> ResultPage<Markup, ErrorPage> {
|
) -> ResultPage<Markup, ErrorPage> {
|
||||||
let name = path.into_inner();
|
let name = path.into_inner();
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.add_child(Html::with(move |_| {
|
.with_child(Html::with(move |_| {
|
||||||
html! {
|
html! {
|
||||||
h1 style="text-align: center;" { "Hello " (name) "!" }
|
h1 style="text-align: center;" { "Hello " (name) "!" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ impl Extension for HelloWorld {
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.add_child(Html::with(|_| {
|
.with_child(Html::with(|_| {
|
||||||
html! {
|
html! {
|
||||||
h1 style="text-align: center;" { "Hello World!" }
|
h1 style="text-align: center;" { "Hello World!" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,88 +12,88 @@ impl Extension for SuperMenu {
|
||||||
fn initialize(&self) {
|
fn initialize(&self) {
|
||||||
let navbar_menu = Navbar::brand_left(navbar::Brand::new())
|
let navbar_menu = Navbar::brand_left(navbar::Brand::new())
|
||||||
.with_expand(BreakPoint::LG)
|
.with_expand(BreakPoint::LG)
|
||||||
.add_item(navbar::Item::nav(
|
.with_item(navbar::Item::nav(
|
||||||
Nav::new()
|
Nav::new()
|
||||||
.add_item(nav::Item::link(L10n::l("sample_menus_item_link"), |cx| {
|
.with_item(nav::Item::link(L10n::l("sample_menus_item_link"), |cx| {
|
||||||
cx.route("/")
|
cx.route("/")
|
||||||
}))
|
}))
|
||||||
.add_item(nav::Item::link_blank(
|
.with_item(nav::Item::link_blank(
|
||||||
L10n::l("sample_menus_item_blank"),
|
L10n::l("sample_menus_item_blank"),
|
||||||
|_| "https://docs.rs/pagetop".into(),
|
|_| "https://docs.rs/pagetop".into(),
|
||||||
))
|
))
|
||||||
.add_item(nav::Item::dropdown(
|
.with_item(nav::Item::dropdown(
|
||||||
Dropdown::new()
|
Dropdown::new()
|
||||||
.with_title(L10n::l("sample_menus_test_title"))
|
.with_title(L10n::l("sample_menus_test_title"))
|
||||||
.add_item(dropdown::Item::header(L10n::l("sample_menus_dev_header")))
|
.with_item(dropdown::Item::header(L10n::l("sample_menus_dev_header")))
|
||||||
.add_item(dropdown::Item::link(
|
.with_item(dropdown::Item::link(
|
||||||
L10n::l("sample_menus_dev_getting_started"),
|
L10n::l("sample_menus_dev_getting_started"),
|
||||||
|cx| cx.route("/dev/getting-started"),
|
|cx| cx.route("/dev/getting-started"),
|
||||||
))
|
))
|
||||||
.add_item(dropdown::Item::link(
|
.with_item(dropdown::Item::link(
|
||||||
L10n::l("sample_menus_dev_guides"),
|
L10n::l("sample_menus_dev_guides"),
|
||||||
|cx| cx.route("/dev/guides"),
|
|cx| cx.route("/dev/guides"),
|
||||||
))
|
))
|
||||||
.add_item(dropdown::Item::link_blank(
|
.with_item(dropdown::Item::link_blank(
|
||||||
L10n::l("sample_menus_dev_forum"),
|
L10n::l("sample_menus_dev_forum"),
|
||||||
|_| "https://forum.example.dev".into(),
|
|_| "https://forum.example.dev".into(),
|
||||||
))
|
))
|
||||||
.add_item(dropdown::Item::divider())
|
.with_item(dropdown::Item::divider())
|
||||||
.add_item(dropdown::Item::header(L10n::l("sample_menus_sdk_header")))
|
.with_item(dropdown::Item::header(L10n::l("sample_menus_sdk_header")))
|
||||||
.add_item(dropdown::Item::link(
|
.with_item(dropdown::Item::link(
|
||||||
L10n::l("sample_menus_sdk_rust"),
|
L10n::l("sample_menus_sdk_rust"),
|
||||||
|cx| cx.route("/dev/sdks/rust"),
|
|cx| cx.route("/dev/sdks/rust"),
|
||||||
))
|
))
|
||||||
.add_item(dropdown::Item::link(L10n::l("sample_menus_sdk_js"), |cx| {
|
.with_item(dropdown::Item::link(L10n::l("sample_menus_sdk_js"), |cx| {
|
||||||
cx.route("/dev/sdks/js")
|
cx.route("/dev/sdks/js")
|
||||||
}))
|
}))
|
||||||
.add_item(dropdown::Item::link(
|
.with_item(dropdown::Item::link(
|
||||||
L10n::l("sample_menus_sdk_python"),
|
L10n::l("sample_menus_sdk_python"),
|
||||||
|cx| cx.route("/dev/sdks/python"),
|
|cx| cx.route("/dev/sdks/python"),
|
||||||
))
|
))
|
||||||
.add_item(dropdown::Item::divider())
|
.with_item(dropdown::Item::divider())
|
||||||
.add_item(dropdown::Item::header(L10n::l(
|
.with_item(dropdown::Item::header(L10n::l(
|
||||||
"sample_menus_plugin_header",
|
"sample_menus_plugin_header",
|
||||||
)))
|
)))
|
||||||
.add_item(dropdown::Item::link(
|
.with_item(dropdown::Item::link(
|
||||||
L10n::l("sample_menus_plugin_auth"),
|
L10n::l("sample_menus_plugin_auth"),
|
||||||
|cx| cx.route("/dev/sdks/rust/plugins/auth"),
|
|cx| cx.route("/dev/sdks/rust/plugins/auth"),
|
||||||
))
|
))
|
||||||
.add_item(dropdown::Item::link(
|
.with_item(dropdown::Item::link(
|
||||||
L10n::l("sample_menus_plugin_cache"),
|
L10n::l("sample_menus_plugin_cache"),
|
||||||
|cx| cx.route("/dev/sdks/rust/plugins/cache"),
|
|cx| cx.route("/dev/sdks/rust/plugins/cache"),
|
||||||
))
|
))
|
||||||
.add_item(dropdown::Item::divider())
|
.with_item(dropdown::Item::divider())
|
||||||
.add_item(dropdown::Item::label(L10n::l("sample_menus_item_label")))
|
.with_item(dropdown::Item::label(L10n::l("sample_menus_item_label")))
|
||||||
.add_item(dropdown::Item::link_disabled(
|
.with_item(dropdown::Item::link_disabled(
|
||||||
L10n::l("sample_menus_item_disabled"),
|
L10n::l("sample_menus_item_disabled"),
|
||||||
|cx| cx.route("#"),
|
|cx| cx.route("#"),
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
.add_item(nav::Item::link_disabled(
|
.with_item(nav::Item::link_disabled(
|
||||||
L10n::l("sample_menus_item_disabled"),
|
L10n::l("sample_menus_item_disabled"),
|
||||||
|cx| cx.route("#"),
|
|cx| cx.route("#"),
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
.add_item(navbar::Item::nav(
|
.with_item(navbar::Item::nav(
|
||||||
Nav::new()
|
Nav::new()
|
||||||
.with_classes(
|
.with_classes(
|
||||||
ClassesOp::Add,
|
ClassesOp::Add,
|
||||||
classes::Margin::with(Side::Start, ScaleSize::Auto).to_class(),
|
classes::Margin::with(Side::Start, ScaleSize::Auto).to_class(),
|
||||||
)
|
)
|
||||||
.add_item(nav::Item::link(
|
.with_item(nav::Item::link(
|
||||||
L10n::l("sample_menus_item_sign_up"),
|
L10n::l("sample_menus_item_sign_up"),
|
||||||
|cx| cx.route("/auth/sign-up"),
|
|cx| cx.route("/auth/sign-up"),
|
||||||
))
|
))
|
||||||
.add_item(nav::Item::link(L10n::l("sample_menus_item_login"), |cx| {
|
.with_item(nav::Item::link(L10n::l("sample_menus_item_login"), |cx| {
|
||||||
cx.route("/auth/login")
|
cx.route("/auth/login")
|
||||||
})),
|
})),
|
||||||
));
|
));
|
||||||
|
|
||||||
InRegion::Global(&DefaultRegion::Header).add(Child::with(
|
InRegion::Global(&DefaultRegion::Header).add(
|
||||||
Container::new()
|
Container::new()
|
||||||
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
||||||
.add_child(navbar_menu),
|
.with_child(navbar_menu),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ Y **selecciona el tema en la configuración** de la aplicación:
|
||||||
theme = "Aliner"
|
theme = "Aliner"
|
||||||
```
|
```
|
||||||
|
|
||||||
…o **fuerza el tema por código** en una página concreta:
|
o **fuerza el tema por código** en una página concreta:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ Y **selecciona el tema en la configuración** de la aplicación:
|
||||||
theme = "Aliner"
|
theme = "Aliner"
|
||||||
```
|
```
|
||||||
|
|
||||||
…o **fuerza el tema por código** en una página concreta:
|
o **fuerza el tema por código** en una página concreta:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
@ -69,10 +69,10 @@ use pagetop_aliner::Aliner;
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_theme(&Aliner)
|
.with_theme(&Aliner)
|
||||||
.add_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::l("sample_title"))
|
.with_title(L10n::l("sample_title"))
|
||||||
.add_child(Html::with(|cx| html! {
|
.with_child(Html::with(|cx| html! {
|
||||||
p { (L10n::l("sample_content").using(cx)) }
|
p { (L10n::l("sample_content").using(cx)) }
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
@ -122,7 +122,7 @@ impl Theme for Aliner {
|
||||||
))
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&DefaultRegion::Footer,
|
&DefaultRegion::Footer,
|
||||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ Y **selecciona el tema en la configuración** de la aplicación:
|
||||||
theme = "Bootsier"
|
theme = "Bootsier"
|
||||||
```
|
```
|
||||||
|
|
||||||
…o **fuerza el tema por código** en una página concreta:
|
o **fuerza el tema por código** en una página concreta:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ Y **selecciona el tema en la configuración** de la aplicación:
|
||||||
theme = "Bootsier"
|
theme = "Bootsier"
|
||||||
```
|
```
|
||||||
|
|
||||||
…o **fuerza el tema por código** en una página concreta:
|
o **fuerza el tema por código** en una página concreta:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
@ -69,10 +69,10 @@ use pagetop_bootsier::Bootsier;
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_theme(&Bootsier)
|
.with_theme(&Bootsier)
|
||||||
.add_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::l("sample_title"))
|
.with_title(L10n::l("sample_title"))
|
||||||
.add_child(Html::with(|cx| html! {
|
.with_child(Html::with(|cx| html! {
|
||||||
p { (L10n::l("sample_content").using(cx)) }
|
p { (L10n::l("sample_content").using(cx)) }
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
@ -118,7 +118,7 @@ impl Template for BootsierTemplate {
|
||||||
.with_width(theme::container::Width::FluidMax(
|
.with_width(theme::container::Width::FluidMax(
|
||||||
config::SETTINGS.bootsier.max_width,
|
config::SETTINGS.bootsier.max_width,
|
||||||
))
|
))
|
||||||
.add_child(Html::with(|cx| {
|
.with_child(Html::with(|cx| {
|
||||||
html! {
|
html! {
|
||||||
(DefaultRegion::Header.render(cx))
|
(DefaultRegion::Header.render(cx))
|
||||||
(DefaultRegion::Content.render(cx))
|
(DefaultRegion::Content.render(cx))
|
||||||
|
|
@ -163,7 +163,7 @@ impl Theme for Bootsier {
|
||||||
))
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&DefaultRegion::Footer,
|
&DefaultRegion::Footer,
|
||||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::prelude::*;
|
||||||
///
|
///
|
||||||
/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza
|
/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza
|
||||||
/// si existen componentes hijos (*children*).
|
/// si existen componentes hijos (*children*).
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -29,11 +29,11 @@ impl Component for Container {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(ClassesOp::Prepend, self.container_width().to_class());
|
self.alter_classes(ClassesOp::Prepend, self.container_width().to_class());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let output = self.children().render(cx);
|
let output = self.children().render(cx);
|
||||||
if output.is_empty() {
|
if output.is_empty() {
|
||||||
return Ok(html! {});
|
return Ok(html! {});
|
||||||
|
|
@ -150,17 +150,11 @@ impl Container {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un nuevo componente hijo al contenedor.
|
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
|
||||||
#[inline]
|
/// una operación [`ChildOp`].
|
||||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
|
||||||
self.children.add(Child::with(component));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.children.alter_child(op);
|
self.children.alter_child(op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
//! .with_button_color(ButtonColor::Background(Color::Secondary))
|
//! .with_button_color(ButtonColor::Background(Color::Secondary))
|
||||||
//! .with_auto_close(dropdown::AutoClose::ClickableInside)
|
//! .with_auto_close(dropdown::AutoClose::ClickableInside)
|
||||||
//! .with_direction(dropdown::Direction::Dropend)
|
//! .with_direction(dropdown::Direction::Dropend)
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into()))
|
//! .with_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||||
//! .add_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://google.es".into()))
|
//! .with_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
||||||
//! .add_item(dropdown::Item::divider())
|
//! .with_item(dropdown::Item::divider())
|
||||||
//! .add_item(dropdown::Item::header(L10n::n("User session")))
|
//! .with_item(dropdown::Item::header(L10n::n("User session")))
|
||||||
//! .add_item(dropdown::Item::button(L10n::n("Sign out")));
|
//! .with_item(dropdown::Item::button(L10n::n("Sign out")));
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
mod props;
|
mod props;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ use crate::LOCALES_BOOTSIER;
|
||||||
///
|
///
|
||||||
/// Ver ejemplo en el módulo [`dropdown`].
|
/// Ver ejemplo en el módulo [`dropdown`].
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Dropdown {
|
pub struct Dropdown {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -56,14 +56,14 @@ impl Component for Dropdown {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(
|
self.alter_classes(
|
||||||
ClassesOp::Prepend,
|
ClassesOp::Prepend,
|
||||||
self.direction().class_with(*self.button_grouped()),
|
self.direction().class_with(*self.button_grouped()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
// Si no hay elementos en el menú, no se prepara.
|
// Si no hay elementos en el menú, no se prepara.
|
||||||
let items = self.items().render(cx);
|
let items = self.items().render(cx);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
|
|
@ -240,17 +240,22 @@ impl Dropdown {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un nuevo elemento hijo al menú.
|
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una operación
|
||||||
#[inline]
|
/// [`ChildOp`].
|
||||||
pub fn add_item(mut self, item: dropdown::Item) -> Self {
|
///
|
||||||
self.items.add(Child::with(item));
|
/// # Ejemplo
|
||||||
self
|
///
|
||||||
}
|
/// ```rust,ignore
|
||||||
|
/// dropdown.with_item(dropdown::Item::link("Opción", "/ruta"));
|
||||||
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
/// dropdown.with_item(ChildOp::AddMany(vec![
|
||||||
|
/// dropdown::Item::link(...).into(),
|
||||||
|
/// dropdown::Item::divider().into(),
|
||||||
|
/// dropdown::Item::link(...).into(),
|
||||||
|
/// ]));
|
||||||
|
/// ```
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
|
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.items.alter_typed(op);
|
self.items.alter_child(op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||||
/// con él.
|
/// con él.
|
||||||
#[derive(AutoDefault, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
pub enum ItemKind {
|
pub enum ItemKind {
|
||||||
/// Elemento vacío, no produce salida.
|
/// Elemento vacío, no produce salida.
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -43,7 +43,7 @@ pub enum ItemKind {
|
||||||
///
|
///
|
||||||
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -62,7 +62,7 @@ impl Component for Item {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(match self.item_kind() {
|
Ok(match self.item_kind() {
|
||||||
ItemKind::Void => html! {},
|
ItemKind::Void => html! {},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ use crate::theme::form;
|
||||||
/// .with_action("/search")
|
/// .with_action("/search")
|
||||||
/// .with_method(form::Method::Get)
|
/// .with_method(form::Method::Get)
|
||||||
/// .with_classes(ClassesOp::Add, "mb-3")
|
/// .with_classes(ClassesOp::Add, "mb-3")
|
||||||
/// .add_child(Input::new().with_name("q"));
|
/// .with_child(Input::new().with_name("q"));
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Form {
|
pub struct Form {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -48,11 +48,11 @@ impl Component for Form {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(ClassesOp::Prepend, "form");
|
self.alter_classes(ClassesOp::Prepend, "form");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
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,
|
||||||
|
|
@ -114,17 +114,11 @@ impl Form {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un nuevo componente hijo al formulario.
|
/// Añade un nuevo componente al formulario o modifica la lista de de componentes (`children`)
|
||||||
#[inline]
|
/// con una operación [`ChildOp`].
|
||||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
|
||||||
self.children.add(Child::with(component));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.children.alter_child(op);
|
self.children.alter_child(op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use pagetop::prelude::*;
|
||||||
/// Agrupa controles relacionados de un formulario (`<fieldset>`).
|
/// Agrupa controles relacionados de un formulario (`<fieldset>`).
|
||||||
///
|
///
|
||||||
/// Se usa para mejorar la accesibilidad cuando se acompaña de una leyenda que encabeza el grupo.
|
/// Se usa para mejorar la accesibilidad cuando se acompaña de una leyenda que encabeza el grupo.
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Fieldset {
|
pub struct Fieldset {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -22,7 +22,7 @@ impl Component for Fieldset {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] {
|
fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] {
|
||||||
@if let Some(legend) = self.legend().lookup(cx) {
|
@if let Some(legend) = self.legend().lookup(cx) {
|
||||||
|
|
@ -65,17 +65,11 @@ impl Fieldset {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un nuevo componente hijo al `fieldset`.
|
/// Añade un nuevo componente al `fieldset` o modifica la lista de de componentes (`children`)
|
||||||
#[inline]
|
/// con una operación [`ChildOp`].
|
||||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
|
||||||
self.children.add(Child::with(component));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.children.alter_child(op);
|
self.children.alter_child(op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use pagetop::prelude::*;
|
||||||
use crate::theme::form;
|
use crate::theme::form;
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Input {
|
pub struct Input {
|
||||||
classes: Classes,
|
classes: Classes,
|
||||||
input_type: form::InputType,
|
input_type: form::InputType,
|
||||||
|
|
@ -29,14 +29,14 @@ impl Component for Input {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(
|
self.alter_classes(
|
||||||
ClassesOp::Prepend,
|
ClassesOp::Prepend,
|
||||||
util::join!("form-item form-type-", self.input_type().to_string()),
|
util::join!("form-item form-type-", self.input_type().to_string()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let id = self.name().get().map(|name| util::join!("edit-", name));
|
let id = self.name().get().map(|name| util::join!("edit-", name));
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
div class=[self.classes().get()] {
|
div class=[self.classes().get()] {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::prelude::*;
|
||||||
|
|
||||||
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
||||||
|
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault, Clone)]
|
||||||
pub enum IconKind {
|
pub enum IconKind {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
|
|
@ -13,7 +13,7 @@ pub enum IconKind {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Icon {
|
pub struct Icon {
|
||||||
/// Devuelve las clases CSS asociadas al icono.
|
/// Devuelve las clases CSS asociadas al icono.
|
||||||
classes: Classes,
|
classes: Classes,
|
||||||
|
|
@ -26,7 +26,7 @@ impl Component for Icon {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
if !matches!(self.icon_kind(), IconKind::None) {
|
if !matches!(self.icon_kind(), IconKind::None) {
|
||||||
self.alter_classes(ClassesOp::Prepend, "icon");
|
self.alter_classes(ClassesOp::Prepend, "icon");
|
||||||
}
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ impl Component for Icon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(match self.icon_kind() {
|
Ok(match self.icon_kind() {
|
||||||
IconKind::None => html! {},
|
IconKind::None => html! {},
|
||||||
IconKind::Font(_) => {
|
IconKind::Font(_) => {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use crate::prelude::*;
|
||||||
/// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas**
|
/// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas**
|
||||||
/// ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
/// ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
||||||
/// - Resuelve el texto alternativo `alt` con **localización** mediante [`L10n`].
|
/// - Resuelve el texto alternativo `alt` con **localización** mediante [`L10n`].
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -32,11 +32,11 @@ impl Component for Image {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(ClassesOp::Prepend, self.source().to_class());
|
self.alter_classes(ClassesOp::Prepend, self.source().to_class());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let dimensions = self.size().to_style();
|
let dimensions = self.size().to_style();
|
||||||
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
|
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
|
||||||
let is_decorative = alt_text.is_empty();
|
let is_decorative = alt_text.is_empty();
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,17 @@
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
//! # use pagetop_bootsier::prelude::*;
|
||||||
//! let nav = Nav::tabs()
|
//! let nav = Nav::tabs()
|
||||||
//! .with_layout(nav::Layout::End)
|
//! .with_layout(nav::Layout::End)
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||||
//! .add_item(nav::Item::link_blank(L10n::n("External"), |_| "https://google.es".into()))
|
//! .with_item(nav::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
||||||
//! .add_item(nav::Item::dropdown(
|
//! .with_item(nav::Item::dropdown(
|
||||||
//! Dropdown::new()
|
//! Dropdown::new()
|
||||||
//! .with_title(L10n::n("Options"))
|
//! .with_title(L10n::n("Options"))
|
||||||
//! .with_items(TypedOp::AddMany(vec![
|
//! .with_item(ChildOp::AddMany(vec![
|
||||||
//! Typed::with(dropdown::Item::link(L10n::n("Action"), |_| "/action".into())),
|
//! dropdown::Item::link(L10n::n("Action"), |_| "/action".into()).into(),
|
||||||
//! Typed::with(dropdown::Item::link(L10n::n("Another"), |_| "/another".into())),
|
//! dropdown::Item::link(L10n::n("Another"), |_| "/another".into()).into(),
|
||||||
//! ])),
|
//! ])),
|
||||||
//! ))
|
//! ))
|
||||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()));
|
//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()));
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
mod props;
|
mod props;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::prelude::*;
|
||||||
///
|
///
|
||||||
/// Ver ejemplo en el módulo [`nav`].
|
/// Ver ejemplo en el módulo [`nav`].
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Nav {
|
pub struct Nav {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -33,7 +33,7 @@ impl Component for Nav {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(ClassesOp::Prepend, {
|
self.alter_classes(ClassesOp::Prepend, {
|
||||||
let mut classes = "nav".to_string();
|
let mut classes = "nav".to_string();
|
||||||
self.nav_kind().push_class(&mut classes);
|
self.nav_kind().push_class(&mut classes);
|
||||||
|
|
@ -42,7 +42,7 @@ impl Component for Nav {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let items = self.items().render(cx);
|
let items = self.items().render(cx);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return Ok(html! {});
|
return Ok(html! {});
|
||||||
|
|
@ -102,16 +102,21 @@ impl Nav {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un nuevo elemento hijo al menú.
|
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una operación
|
||||||
pub fn add_item(mut self, item: nav::Item) -> Self {
|
/// [`ChildOp`].
|
||||||
self.items.add(Child::with(item));
|
///
|
||||||
self
|
/// # Ejemplo
|
||||||
}
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
/// nav.with_item(nav::Item::link("Inicio", "/"));
|
||||||
|
/// nav.with_item(ChildOp::AddMany(vec![
|
||||||
|
/// nav::Item::link(...).into(),
|
||||||
|
/// nav::Item::link_disabled(...).into(),
|
||||||
|
/// ]));
|
||||||
|
/// ```
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_items(mut self, op: TypedOp<nav::Item>) -> Self {
|
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.items.alter_typed(op);
|
self.items.alter_child(op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::LOCALES_BOOTSIER;
|
||||||
///
|
///
|
||||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||||
/// con él.
|
/// con él.
|
||||||
#[derive(AutoDefault, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
pub enum ItemKind {
|
pub enum ItemKind {
|
||||||
/// Elemento vacío, no produce salida.
|
/// Elemento vacío, no produce salida.
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -28,9 +28,9 @@ pub enum ItemKind {
|
||||||
},
|
},
|
||||||
/// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del
|
/// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del
|
||||||
/// menú, sin añadir ningún comportamiento de navegación adicional.
|
/// menú, sin añadir ningún comportamiento de navegación adicional.
|
||||||
Html(Typed<Html>),
|
Html(Embed<Html>),
|
||||||
/// Elemento que despliega un menú [`Dropdown`].
|
/// Elemento que despliega un menú [`Dropdown`].
|
||||||
Dropdown(Typed<Dropdown>),
|
Dropdown(Embed<Dropdown>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ItemKind {
|
impl ItemKind {
|
||||||
|
|
@ -76,7 +76,7 @@ impl ItemKind {
|
||||||
///
|
///
|
||||||
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -95,11 +95,11 @@ impl Component for Item {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class());
|
self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(match self.item_kind() {
|
Ok(match self.item_kind() {
|
||||||
ItemKind::Void => html! {},
|
ItemKind::Void => html! {},
|
||||||
|
|
||||||
|
|
@ -159,7 +159,7 @@ impl Component for Item {
|
||||||
},
|
},
|
||||||
|
|
||||||
ItemKind::Dropdown(menu) => {
|
ItemKind::Dropdown(menu) => {
|
||||||
if let Some(dd) = menu.borrow() {
|
if let Some(dd) = menu.get() {
|
||||||
let items = dd.items().render(cx);
|
let items = dd.items().render(cx);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return Ok(html! {});
|
return Ok(html! {});
|
||||||
|
|
@ -264,7 +264,7 @@ impl Item {
|
||||||
/// con las clases de navegación asociadas a [`Item`].
|
/// con las clases de navegación asociadas a [`Item`].
|
||||||
pub fn html(html: Html) -> Self {
|
pub fn html(html: Html) -> Self {
|
||||||
Self {
|
Self {
|
||||||
item_kind: ItemKind::Html(Typed::with(html)),
|
item_kind: ItemKind::Html(Embed::with(html)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +276,7 @@ impl Item {
|
||||||
/// a su representación en [`Nav`].
|
/// a su representación en [`Nav`].
|
||||||
pub fn dropdown(menu: Dropdown) -> Self {
|
pub fn dropdown(menu: Dropdown) -> Self {
|
||||||
Self {
|
Self {
|
||||||
item_kind: ItemKind::Dropdown(Typed::with(menu)),
|
item_kind: ItemKind::Dropdown(Embed::with(menu)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@
|
||||||
//! # use pagetop::prelude::*;
|
//! # use pagetop::prelude::*;
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
//! # use pagetop_bootsier::prelude::*;
|
||||||
//! let navbar = Navbar::simple()
|
//! let navbar = Navbar::simple()
|
||||||
//! .add_item(navbar::Item::nav(
|
//! .with_item(navbar::Item::nav(
|
||||||
//! Nav::new()
|
//! Nav::new()
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||||
//! .add_item(nav::Item::link(L10n::n("About"), |_| "/about".into()))
|
//! .with_item(nav::Item::link(L10n::n("About"), |_| "/about".into()))
|
||||||
//! .add_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into()))
|
//! .with_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into()))
|
||||||
//! ));
|
//! ));
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
|
@ -30,11 +30,11 @@
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
//! # use pagetop_bootsier::prelude::*;
|
||||||
//! let navbar = Navbar::simple_toggle()
|
//! let navbar = Navbar::simple_toggle()
|
||||||
//! .with_expand(BreakPoint::MD)
|
//! .with_expand(BreakPoint::MD)
|
||||||
//! .add_item(navbar::Item::nav(
|
//! .with_item(navbar::Item::nav(
|
||||||
//! Nav::new()
|
//! Nav::new()
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||||
//! .add_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://sample.com".into()))
|
//! .with_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
||||||
//! .add_item(nav::Item::link(L10n::n("Support"), |_| "/support".into()))
|
//! .with_item(nav::Item::link(L10n::n("Support"), |_| "/support".into()))
|
||||||
//! ));
|
//! ));
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
|
@ -48,20 +48,20 @@
|
||||||
//! .with_route(Some(|cx| cx.route("/")));
|
//! .with_route(Some(|cx| cx.route("/")));
|
||||||
//!
|
//!
|
||||||
//! let navbar = Navbar::brand_left(brand)
|
//! let navbar = Navbar::brand_left(brand)
|
||||||
//! .add_item(navbar::Item::nav(
|
//! .with_item(navbar::Item::nav(
|
||||||
//! Nav::new()
|
//! Nav::new()
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||||
//! .add_item(nav::Item::dropdown(
|
//! .with_item(nav::Item::dropdown(
|
||||||
//! Dropdown::new()
|
//! Dropdown::new()
|
||||||
//! .with_title(L10n::n("Tools"))
|
//! .with_title(L10n::n("Tools"))
|
||||||
//! .add_item(dropdown::Item::link(
|
//! .with_item(dropdown::Item::link(
|
||||||
//! L10n::n("Generator"), |_| "/tools/gen".into())
|
//! L10n::n("Generator"), |_| "/tools/gen".into())
|
||||||
//! )
|
//! )
|
||||||
//! .add_item(dropdown::Item::link(
|
//! .with_item(dropdown::Item::link(
|
||||||
//! L10n::n("Reports"), |_| "/tools/reports".into())
|
//! L10n::n("Reports"), |_| "/tools/reports".into())
|
||||||
//! )
|
//! )
|
||||||
//! ))
|
//! ))
|
||||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()))
|
//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()))
|
||||||
//! ));
|
//! ));
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
|
@ -76,10 +76,10 @@
|
||||||
//!
|
//!
|
||||||
//! let navbar = Navbar::brand_right(brand)
|
//! let navbar = Navbar::brand_right(brand)
|
||||||
//! .with_expand(BreakPoint::LG)
|
//! .with_expand(BreakPoint::LG)
|
||||||
//! .add_item(navbar::Item::nav(
|
//! .with_item(navbar::Item::nav(
|
||||||
//! Nav::pills()
|
//! Nav::pills()
|
||||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into()))
|
//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into()))
|
||||||
//! .add_item(nav::Item::link(L10n::n("Users"), |_| "/users".into()))
|
//! .with_item(nav::Item::link(L10n::n("Users"), |_| "/users".into()))
|
||||||
//! ));
|
//! ));
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
|
@ -95,15 +95,15 @@
|
||||||
//! .with_backdrop(offcanvas::Backdrop::Enabled);
|
//! .with_backdrop(offcanvas::Backdrop::Enabled);
|
||||||
//!
|
//!
|
||||||
//! let navbar = Navbar::offcanvas(oc)
|
//! let navbar = Navbar::offcanvas(oc)
|
||||||
//! .add_item(navbar::Item::nav(
|
//! .with_item(navbar::Item::nav(
|
||||||
//! Nav::new()
|
//! Nav::new()
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||||
//! .add_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into()))
|
//! .with_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into()))
|
||||||
//! .add_item(nav::Item::dropdown(
|
//! .with_item(nav::Item::dropdown(
|
||||||
//! Dropdown::new()
|
//! Dropdown::new()
|
||||||
//! .with_title(L10n::n("More"))
|
//! .with_title(L10n::n("More"))
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into()))
|
//! .with_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into()))
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into()))
|
//! .with_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into()))
|
||||||
//! ))
|
//! ))
|
||||||
//! ));
|
//! ));
|
||||||
//! ```
|
//! ```
|
||||||
|
|
@ -119,11 +119,11 @@
|
||||||
//!
|
//!
|
||||||
//! let navbar = Navbar::brand_left(brand)
|
//! let navbar = Navbar::brand_left(brand)
|
||||||
//! .with_position(navbar::Position::FixedTop)
|
//! .with_position(navbar::Position::FixedTop)
|
||||||
//! .add_item(navbar::Item::nav(
|
//! .with_item(navbar::Item::nav(
|
||||||
//! Nav::new()
|
//! Nav::new()
|
||||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into()))
|
//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into()))
|
||||||
//! .add_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into()))
|
//! .with_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into()))
|
||||||
//! .add_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into()))
|
//! .with_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into()))
|
||||||
//! ));
|
//! ));
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ use crate::prelude::*;
|
||||||
/// - Si no hay imagen ([`with_image()`](Self::with_image)) ni título
|
/// - Si no hay imagen ([`with_image()`](Self::with_image)) ni título
|
||||||
/// ([`with_title()`](Self::with_title)), la marca de identidad no se renderiza.
|
/// ([`with_title()`](Self::with_title)), la marca de identidad no se renderiza.
|
||||||
/// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido.
|
/// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido.
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Brand {
|
pub struct Brand {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
/// Devuelve la imagen de marca (si la hay).
|
/// Devuelve la imagen de marca (si la hay).
|
||||||
image: Typed<Image>,
|
image: Embed<Image>,
|
||||||
/// Devuelve el título de la identidad de marca.
|
/// Devuelve el título de la identidad de marca.
|
||||||
#[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
|
#[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
|
||||||
title: L10n,
|
title: L10n,
|
||||||
|
|
@ -36,7 +36,7 @@ impl Component for Brand {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let image = self.image().render(cx);
|
let image = self.image().render(cx);
|
||||||
let title = self.title().using(cx);
|
let title = self.title().using(cx);
|
||||||
if title.is_empty() && image.is_empty() {
|
if title.is_empty() && image.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
||||||
///
|
///
|
||||||
/// Ver ejemplos en el módulo [`navbar`].
|
/// Ver ejemplos en el módulo [`navbar`].
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Navbar {
|
pub struct Navbar {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -39,7 +39,7 @@ impl Component for Navbar {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(ClassesOp::Prepend, {
|
self.alter_classes(ClassesOp::Prepend, {
|
||||||
let mut classes = "navbar".to_string();
|
let mut classes = "navbar".to_string();
|
||||||
self.expand().push_class(&mut classes, "navbar-expand", "");
|
self.expand().push_class(&mut classes, "navbar-expand", "");
|
||||||
|
|
@ -48,7 +48,7 @@ impl Component for Navbar {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
// Botón de despliegue (colapso u offcanvas) para la barra.
|
// Botón de despliegue (colapso u offcanvas) para la barra.
|
||||||
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
|
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
|
||||||
let id_content_target = util::join!("#", id_content);
|
let id_content_target = util::join!("#", id_content);
|
||||||
|
|
@ -133,7 +133,7 @@ impl Component for Navbar {
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
@let id_content = offcanvas.id().unwrap_or_default();
|
||||||
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||||
@if let Some(oc) = offcanvas.borrow() {
|
@if let Some(oc) = offcanvas.get() {
|
||||||
(oc.render_offcanvas(cx, Some(self.items())))
|
(oc.render_offcanvas(cx, Some(self.items())))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -144,7 +144,7 @@ impl Component for Navbar {
|
||||||
|
|
||||||
(brand.render(cx))
|
(brand.render(cx))
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||||
@if let Some(oc) = offcanvas.borrow() {
|
@if let Some(oc) = offcanvas.get() {
|
||||||
(oc.render_offcanvas(cx, Some(self.items())))
|
(oc.render_offcanvas(cx, Some(self.items())))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -155,7 +155,7 @@ impl Component for Navbar {
|
||||||
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||||
(brand.render(cx))
|
(brand.render(cx))
|
||||||
@if let Some(oc) = offcanvas.borrow() {
|
@if let Some(oc) = offcanvas.get() {
|
||||||
(oc.render_offcanvas(cx, Some(self.items())))
|
(oc.render_offcanvas(cx, Some(self.items())))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -179,37 +179,37 @@ impl Navbar {
|
||||||
|
|
||||||
/// Crea una barra de navegación **con marca a la izquierda**, siempre visible.
|
/// Crea una barra de navegación **con marca a la izquierda**, siempre visible.
|
||||||
pub fn simple_brand_left(brand: navbar::Brand) -> Self {
|
pub fn simple_brand_left(brand: navbar::Brand) -> Self {
|
||||||
Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Typed::with(brand)))
|
Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Embed::with(brand)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**.
|
/// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**.
|
||||||
pub fn brand_left(brand: navbar::Brand) -> Self {
|
pub fn brand_left(brand: navbar::Brand) -> Self {
|
||||||
Self::default().with_layout(navbar::Layout::BrandLeft(Typed::with(brand)))
|
Self::default().with_layout(navbar::Layout::BrandLeft(Embed::with(brand)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**.
|
/// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**.
|
||||||
pub fn brand_right(brand: navbar::Brand) -> Self {
|
pub fn brand_right(brand: navbar::Brand) -> Self {
|
||||||
Self::default().with_layout(navbar::Layout::BrandRight(Typed::with(brand)))
|
Self::default().with_layout(navbar::Layout::BrandRight(Embed::with(brand)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
|
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
|
||||||
pub fn offcanvas(oc: Offcanvas) -> Self {
|
pub fn offcanvas(oc: Offcanvas) -> Self {
|
||||||
Self::default().with_layout(navbar::Layout::Offcanvas(Typed::with(oc)))
|
Self::default().with_layout(navbar::Layout::Offcanvas(Embed::with(oc)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**.
|
/// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**.
|
||||||
pub fn offcanvas_brand_left(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
pub fn offcanvas_brand_left(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
||||||
Self::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
|
Self::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
|
||||||
Typed::with(brand),
|
Embed::with(brand),
|
||||||
Typed::with(oc),
|
Embed::with(oc),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**.
|
/// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**.
|
||||||
pub fn offcanvas_brand_right(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
pub fn offcanvas_brand_right(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
||||||
Self::default().with_layout(navbar::Layout::OffcanvasBrandRight(
|
Self::default().with_layout(navbar::Layout::OffcanvasBrandRight(
|
||||||
Typed::with(brand),
|
Embed::with(brand),
|
||||||
Typed::with(oc),
|
Embed::with(oc),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,17 +255,21 @@ impl Navbar {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un nuevo contenido hijo.
|
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
||||||
#[inline]
|
/// barra con una operación [`ChildOp`].
|
||||||
pub fn add_item(mut self, item: navbar::Item) -> Self {
|
///
|
||||||
self.items.add(Child::with(item));
|
/// # Ejemplo
|
||||||
self
|
///
|
||||||
}
|
/// ```rust,ignore
|
||||||
|
/// navbar.with_item(navbar::Item::nav(...));
|
||||||
/// Modifica la lista de contenidos (`children`) aplicando una operación [`TypedOp`].
|
/// navbar.with_item(ChildOp::AddMany(vec![
|
||||||
|
/// navbar::Item::nav(...).into(),
|
||||||
|
/// navbar::Item::text(...).into(),
|
||||||
|
/// ]));
|
||||||
|
/// ```
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
|
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.items.alter_typed(op);
|
self.items.alter_child(op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use crate::prelude::*;
|
||||||
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
|
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
|
||||||
/// contenido** de la barra (la parte colapsable, el *offcanvas* o el bloque simple), por lo que son
|
/// contenido** de la barra (la parte colapsable, el *offcanvas* o el bloque simple), por lo que son
|
||||||
/// independientes de la marca o del botón que ya pueda definir el propio [`navbar::Layout`].
|
/// independientes de la marca o del botón que ya pueda definir el propio [`navbar::Layout`].
|
||||||
#[derive(AutoDefault, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
pub enum Item {
|
pub enum Item {
|
||||||
/// Sin contenido, no produce salida.
|
/// Sin contenido, no produce salida.
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -17,9 +17,9 @@ pub enum Item {
|
||||||
/// Útil cuando el [`navbar::Layout`] no incluye marca, y se quiere incluir dentro del área
|
/// Útil cuando el [`navbar::Layout`] no incluye marca, y se quiere incluir dentro del área
|
||||||
/// colapsable/*offcanvas*. Si el *layout* ya muestra una marca, esta variante no la sustituye,
|
/// colapsable/*offcanvas*. Si el *layout* ya muestra una marca, esta variante no la sustituye,
|
||||||
/// sólo añade otra dentro del bloque de contenidos.
|
/// sólo añade otra dentro del bloque de contenidos.
|
||||||
Brand(Typed<navbar::Brand>),
|
Brand(Embed<navbar::Brand>),
|
||||||
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
|
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
|
||||||
Nav(Typed<Nav>),
|
Nav(Embed<Nav>),
|
||||||
/// Representa un *texto localizado* libre.
|
/// Representa un *texto localizado* libre.
|
||||||
Text(L10n),
|
Text(L10n),
|
||||||
}
|
}
|
||||||
|
|
@ -38,20 +38,20 @@ impl Component for Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
if let Self::Nav(nav) = self {
|
if let Self::Nav(nav) = self {
|
||||||
if let Some(mut nav) = nav.borrow_mut() {
|
if let Some(mut nav) = nav.get() {
|
||||||
nav.alter_classes(ClassesOp::Prepend, "navbar-nav");
|
nav.alter_classes(ClassesOp::Prepend, "navbar-nav");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
Self::Void => html! {},
|
Self::Void => html! {},
|
||||||
Self::Brand(brand) => html! { (brand.render(cx)) },
|
Self::Brand(brand) => html! { (brand.render(cx)) },
|
||||||
Self::Nav(nav) => {
|
Self::Nav(nav) => {
|
||||||
if let Some(nav) = nav.borrow() {
|
if let Some(nav) = nav.get() {
|
||||||
let items = nav.items().render(cx);
|
let items = nav.items().render(cx);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return Ok(html! {});
|
return Ok(html! {});
|
||||||
|
|
@ -80,12 +80,12 @@ impl Item {
|
||||||
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
|
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
|
||||||
/// desplegable.
|
/// desplegable.
|
||||||
pub fn brand(brand: navbar::Brand) -> Self {
|
pub fn brand(brand: navbar::Brand) -> Self {
|
||||||
Self::Brand(Typed::with(brand))
|
Self::Brand(Embed::with(brand))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`].
|
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`].
|
||||||
pub fn nav(item: Nav) -> Self {
|
pub fn nav(item: Nav) -> Self {
|
||||||
Self::Nav(Typed::with(item))
|
Self::Nav(Embed::with(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
|
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use crate::prelude::*;
|
||||||
// **< Layout >*************************************************************************************
|
// **< Layout >*************************************************************************************
|
||||||
|
|
||||||
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
|
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
|
||||||
#[derive(AutoDefault, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
pub enum Layout {
|
pub enum Layout {
|
||||||
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
||||||
///
|
///
|
||||||
|
|
@ -19,24 +19,24 @@ pub enum Layout {
|
||||||
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
|
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
|
||||||
///
|
///
|
||||||
/// La barra de navegación no se colapsa.
|
/// La barra de navegación no se colapsa.
|
||||||
SimpleBrandLeft(Typed<navbar::Brand>),
|
SimpleBrandLeft(Embed<navbar::Brand>),
|
||||||
|
|
||||||
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
|
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
|
||||||
BrandLeft(Typed<navbar::Brand>),
|
BrandLeft(Embed<navbar::Brand>),
|
||||||
|
|
||||||
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
|
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
|
||||||
BrandRight(Typed<navbar::Brand>),
|
BrandRight(Embed<navbar::Brand>),
|
||||||
|
|
||||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y sin marca de identidad.
|
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y sin marca de identidad.
|
||||||
Offcanvas(Typed<Offcanvas>),
|
Offcanvas(Embed<Offcanvas>),
|
||||||
|
|
||||||
/// Contenido en [`Offcanvas`], con marca de identidad a la izquierda y botón de despliegue a la
|
/// Contenido en [`Offcanvas`], con marca de identidad a la izquierda y botón de despliegue a la
|
||||||
/// derecha.
|
/// derecha.
|
||||||
OffcanvasBrandLeft(Typed<navbar::Brand>, Typed<Offcanvas>),
|
OffcanvasBrandLeft(Embed<navbar::Brand>, Embed<Offcanvas>),
|
||||||
|
|
||||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y marca de identidad a la
|
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y marca de identidad a la
|
||||||
/// derecha.
|
/// derecha.
|
||||||
OffcanvasBrandRight(Typed<navbar::Brand>, Typed<Offcanvas>),
|
OffcanvasBrandRight(Embed<navbar::Brand>, Embed<Offcanvas>),
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Position >***********************************************************************************
|
// **< Position >***********************************************************************************
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@
|
||||||
//! .with_backdrop(offcanvas::Backdrop::Enabled)
|
//! .with_backdrop(offcanvas::Backdrop::Enabled)
|
||||||
//! .with_body_scroll(offcanvas::BodyScroll::Enabled)
|
//! .with_body_scroll(offcanvas::BodyScroll::Enabled)
|
||||||
//! .with_visibility(offcanvas::Visibility::Default)
|
//! .with_visibility(offcanvas::Visibility::Default)
|
||||||
//! .add_child(Dropdown::new()
|
//! .with_child(Dropdown::new()
|
||||||
//! .with_title(L10n::n("Menu"))
|
//! .with_title(L10n::n("Menu"))
|
||||||
//! .add_item(dropdown::Item::label(L10n::n("Label")))
|
//! .with_item(dropdown::Item::label(L10n::n("Label")))
|
||||||
//! .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://google.es".into()))
|
//! .with_item(dropdown::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into()))
|
//! .with_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into()))
|
||||||
//! );
|
//! );
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use crate::LOCALES_BOOTSIER;
|
||||||
///
|
///
|
||||||
/// Ver ejemplo en el módulo [`offcanvas`].
|
/// Ver ejemplo en el módulo [`offcanvas`].
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Offcanvas {
|
pub struct Offcanvas {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -52,7 +52,7 @@ impl Component for Offcanvas {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(ClassesOp::Prepend, {
|
self.alter_classes(ClassesOp::Prepend, {
|
||||||
let mut classes = "offcanvas".to_string();
|
let mut classes = "offcanvas".to_string();
|
||||||
self.breakpoint().push_class(&mut classes, "offcanvas", "");
|
self.breakpoint().push_class(&mut classes, "offcanvas", "");
|
||||||
|
|
@ -62,7 +62,7 @@ impl Component for Offcanvas {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(self.render_offcanvas(cx, None))
|
Ok(self.render_offcanvas(cx, None))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,17 +135,11 @@ impl Offcanvas {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un nuevo componente hijo al panel.
|
/// Añade un nuevo componente al panel o modifica la lista de componentes (`children`) con una
|
||||||
#[inline]
|
/// operación [`ChildOp`].
|
||||||
pub fn add_child(mut self, child: impl Component) -> Self {
|
|
||||||
self.children.add(Child::with(child));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_children(mut self, op: ChildOp) -> Self {
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.children.alter_child(op);
|
self.children.alter_child(op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ use pagetop::prelude::*;
|
||||||
/// Tipo de función para manipular componentes y su contexto de renderizado.
|
/// Tipo de función para manipular componentes y su contexto de renderizado.
|
||||||
///
|
///
|
||||||
/// Se usa en [`action::component::BeforeRender`] y [`action::component::AfterRender`] para alterar
|
/// Se usa en [`action::component::BeforeRender`] y [`action::component::AfterRender`] para alterar
|
||||||
/// el comportamiento predefinido de los componentes.
|
/// el comportamiento predefinido de los componentes antes y después de renderizarlos.
|
||||||
///
|
///
|
||||||
/// Recibe referencias mutables (`&mut`) del componente `component` y del contexto `cx`.
|
/// Recibe referencias mutables del componente `component` y del contexto `cx`.
|
||||||
pub type FnActionWithComponent<C> = fn(component: &mut C, cx: &mut Context);
|
pub type FnActionWithComponent<C> = fn(component: &mut C, cx: &mut Context);
|
||||||
|
|
||||||
/// Tipo de función para alterar el [`Markup`] generado por un componente.
|
/// Tipo de función para alterar el [`Markup`] generado por un componente.
|
||||||
|
|
@ -18,10 +18,12 @@ pub type FnActionWithComponent<C> = fn(component: &mut C, cx: &mut Context);
|
||||||
/// sustituciones, concatenaciones y cualquier otra primitiva de trabajo con cadenas.
|
/// sustituciones, concatenaciones y cualquier otra primitiva de trabajo con cadenas.
|
||||||
///
|
///
|
||||||
/// La función recibe una referencia inmutable al componente `component` (el renderizado ya ha
|
/// La función recibe una referencia inmutable al componente `component` (el renderizado ya ha
|
||||||
/// concluido, solo se necesita leer su estado), una referencia mutable al contexto `cx`, y toma
|
/// concluido, solo se necesita leer su estado), y al contexto `cx` (solo para consulta), y toma
|
||||||
/// posesión del `markup` producido hasta ese momento. Devuelve el nuevo [`Markup`] transformado,
|
/// posesión del `markup` producido hasta ese momento.
|
||||||
/// que se encadena como entrada para la siguiente acción registrada, si la hay.
|
///
|
||||||
pub type FnActionTransformMarkup<C> = fn(component: &C, cx: &mut Context, markup: Markup) -> Markup;
|
/// Devuelve el nuevo [`Markup`] transformado, que se encadena como entrada para la siguiente acción
|
||||||
|
/// registrada, si la hay.
|
||||||
|
pub type FnActionTransformMarkup<C> = fn(component: &C, cx: &Context, markup: Markup) -> Markup;
|
||||||
|
|
||||||
mod before_render_component;
|
mod before_render_component;
|
||||||
pub use before_render_component::*;
|
pub use before_render_component::*;
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ impl<C: Component> TransformMarkup<C> {
|
||||||
|
|
||||||
/// Despacha las acciones encadenando el [`Markup`] entre cada una.
|
/// Despacha las acciones encadenando el [`Markup`] entre cada una.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn dispatch(component: &C, cx: &mut Context, markup: Markup) -> Markup {
|
pub(crate) fn dispatch(component: &C, cx: &Context, markup: Markup) -> Markup {
|
||||||
let mut output = markup;
|
let mut output = markup;
|
||||||
|
|
||||||
// Primero despacha las acciones para el tipo de componente.
|
// Primero despacha las acciones para el tipo de componente.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use crate::prelude::*;
|
||||||
///
|
///
|
||||||
/// Los bloques se utilizan como contenedores de otros componentes o contenidos, con un título
|
/// Los bloques se utilizan como contenedores de otros componentes o contenidos, con un título
|
||||||
/// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*).
|
/// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*).
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Block {
|
pub struct Block {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
id: AttrId,
|
id: AttrId,
|
||||||
|
|
@ -25,11 +25,11 @@ impl Component for Block {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
self.alter_classes(ClassesOp::Prepend, "block");
|
self.alter_classes(ClassesOp::Prepend, "block");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let block_body = self.children().render(cx);
|
let block_body = self.children().render(cx);
|
||||||
|
|
||||||
if block_body.is_empty() {
|
if block_body.is_empty() {
|
||||||
|
|
@ -73,17 +73,11 @@ impl Block {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un nuevo componente hijo al bloque.
|
/// Añade un nuevo componente al bloque o modifica la lista de componentes (`children`) con una
|
||||||
#[inline]
|
/// operación [`ChildOp`].
|
||||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
|
||||||
self.children.add(Child::with(component));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.children.alter_child(op);
|
self.children.alter_child(op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Componente básico que renderiza dinámicamente código HTML según el contexto.
|
/// Componente básico que renderiza dinámicamente código HTML según el contexto.
|
||||||
///
|
///
|
||||||
|
|
@ -31,7 +32,8 @@ use std::fmt;
|
||||||
/// }
|
/// }
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Html(Box<dyn Fn(&mut Context) -> Markup + Send + Sync>);
|
#[derive(Clone)]
|
||||||
|
pub struct Html(Arc<dyn Fn(&mut Context) -> Markup + Send + Sync>);
|
||||||
|
|
||||||
impl fmt::Debug for Html {
|
impl fmt::Debug for Html {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
|
@ -52,7 +54,7 @@ impl Component for Html {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(self.html(cx))
|
Ok(self.html(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,26 +64,26 @@ impl Html {
|
||||||
|
|
||||||
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
|
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
|
||||||
///
|
///
|
||||||
/// El método [`Self::prepare_component()`] delega el renderizado a la función que aquí se
|
/// El método [`Self::prepare()`] delega el renderizado a la función que aquí se proporciona,
|
||||||
/// proporciona, que recibe una referencia mutable al [`Context`].
|
/// con una llamada que requiere una referencia mutable al [`Context`].
|
||||||
pub fn with<F>(f: F) -> Self
|
pub fn with<F>(f: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
|
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
Html(Box::new(f))
|
Html(Arc::new(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sustituye la función que genera el `Markup`.
|
/// Sustituye la función que genera el `Markup`.
|
||||||
///
|
///
|
||||||
/// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando
|
/// Permite a otras extensiones modificar la función de renderizado que se ejecutará cuando
|
||||||
/// [`Self::prepare_component()`] invoque esta instancia. La nueva función también recibe una
|
/// [`Self::prepare()`] invoque esta instancia. La nueva función también recibe una referencia
|
||||||
/// referencia al [`Context`].
|
/// mutable al [`Context`].
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_fn<F>(mut self, f: F) -> Self
|
pub fn with_fn<F>(mut self, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
|
F: Fn(&mut Context) -> Markup + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
self.0 = Box::new(f);
|
self.0 = Arc::new(f);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,8 +93,8 @@ impl Html {
|
||||||
///
|
///
|
||||||
/// Normalmente no se invoca manualmente, ya que el proceso de renderizado de los componentes lo
|
/// Normalmente no se invoca manualmente, ya que el proceso de renderizado de los componentes lo
|
||||||
/// invoca automáticamente durante la construcción de la página. Puede usarse, no obstante, para
|
/// invoca automáticamente durante la construcción de la página. Puede usarse, no obstante, para
|
||||||
/// sobrescribir [`prepare_component()`](crate::core::component::Component::prepare_component)
|
/// sobrescribir [`prepare()`](crate::core::component::Component::prepare) y alterar el
|
||||||
/// y alterar el comportamiento del componente.
|
/// comportamiento del componente.
|
||||||
pub fn html(&self, cx: &mut Context) -> Markup {
|
pub fn html(&self, cx: &mut Context) -> Markup {
|
||||||
(self.0)(cx)
|
(self.0)(cx)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,10 @@ pub enum IntroOpening {
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// let intro = Intro::default()
|
/// let intro = Intro::default()
|
||||||
/// .add_child(
|
/// .with_child(
|
||||||
/// Block::new()
|
/// Block::new()
|
||||||
/// .with_title(L10n::l("intro_custom_block_title"))
|
/// .with_title(L10n::l("intro_custom_block_title"))
|
||||||
/// .add_child(Html::with(move |cx| {
|
/// .with_child(Html::with(move |cx| {
|
||||||
/// html! {
|
/// html! {
|
||||||
/// p { (L10n::l("intro_custom_paragraph_1").using(cx)) }
|
/// p { (L10n::l("intro_custom_paragraph_1").using(cx)) }
|
||||||
/// p { (L10n::l("intro_custom_paragraph_2").using(cx)) }
|
/// p { (L10n::l("intro_custom_paragraph_2").using(cx)) }
|
||||||
|
|
@ -76,7 +76,7 @@ pub enum IntroOpening {
|
||||||
/// })),
|
/// })),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Getters)]
|
#[derive(Clone, Debug, Getters)]
|
||||||
pub struct Intro {
|
pub struct Intro {
|
||||||
/// Devuelve el título de entrada.
|
/// Devuelve el título de entrada.
|
||||||
title: L10n,
|
title: L10n,
|
||||||
|
|
@ -109,13 +109,10 @@ impl Component for Intro {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, cx: &mut Context) {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
cx.alter_assets(AssetsOp::AddStyleSheet(
|
cx.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION),
|
StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION),
|
||||||
));
|
));
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
if *self.opening() == IntroOpening::PageTop {
|
if *self.opening() == IntroOpening::PageTop {
|
||||||
cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
||||||
util::indoc!(r#"
|
util::indoc!(r#"
|
||||||
|
|
@ -280,19 +277,13 @@ impl Intro {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un nuevo componente hijo a la intro.
|
/// Añade un nuevo componente a la intro o modifica la lista de componentes (`children`) con una
|
||||||
|
/// operación [`ChildOp`].
|
||||||
///
|
///
|
||||||
/// Si es un bloque ([`Block`]) aplica estilos específicos para destacarlo.
|
/// Si se añade un bloque ([`Block`]) se aplicarán estilos específicos para destacarlo.
|
||||||
#[inline]
|
|
||||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
|
||||||
self.children.add(Child::with(component));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.children.alter_child(op);
|
self.children.alter_child(op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const LINK: &str = "<a href=\"https://pagetop.cillero.es\" rel=\"noopener norefe
|
||||||
/// Por defecto, usando [`default()`](Self::default) sólo se muestra un reconocimiento a PageTop.
|
/// Por defecto, usando [`default()`](Self::default) sólo se muestra un reconocimiento a PageTop.
|
||||||
/// Sin embargo, se puede usar [`new()`](Self::new) para crear una instancia con un texto de
|
/// Sin embargo, se puede usar [`new()`](Self::new) para crear una instancia con un texto de
|
||||||
/// copyright predeterminado.
|
/// copyright predeterminado.
|
||||||
#[derive(AutoDefault, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct PoweredBy {
|
pub struct PoweredBy {
|
||||||
/// Devuelve el texto de copyright actual, si existe.
|
/// Devuelve el texto de copyright actual, si existe.
|
||||||
copyright: Option<String>,
|
copyright: Option<String>,
|
||||||
|
|
@ -25,7 +25,7 @@ impl Component for PoweredBy {
|
||||||
PoweredBy { copyright: Some(c) }
|
PoweredBy { copyright: Some(c) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
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() {
|
||||||
|
|
|
||||||
|
|
@ -30,22 +30,22 @@ async fn home(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
|
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_title(L10n::l("welcome_title"))
|
.with_title(L10n::l("welcome_title"))
|
||||||
.add_child(
|
.with_child(
|
||||||
Intro::new()
|
Intro::new()
|
||||||
.add_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::l("welcome_status_title"))
|
.with_title(L10n::l("welcome_status_title"))
|
||||||
.add_child(Html::with(move |cx| {
|
.with_child(Html::with(move |cx| {
|
||||||
html! {
|
html! {
|
||||||
p { (L10n::l("welcome_status_1").using(cx)) }
|
p { (L10n::l("welcome_status_1").using(cx)) }
|
||||||
p { (L10n::l("welcome_status_2").using(cx)) }
|
p { (L10n::l("welcome_status_2").using(cx)) }
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.add_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::l("welcome_support_title"))
|
.with_title(L10n::l("welcome_support_title"))
|
||||||
.add_child(Html::with(move |cx| {
|
.with_child(Html::with(move |cx| {
|
||||||
html! {
|
html! {
|
||||||
p { (L10n::l("welcome_support_1").using(cx)) }
|
p { (L10n::l("welcome_support_1").using(cx)) }
|
||||||
p { (L10n::l("welcome_support_2").with_arg("app", app).using(cx)) }
|
p { (L10n::l("welcome_support_2").with_arg("app", app).using(cx)) }
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ impl Theme for Basic {
|
||||||
))
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&DefaultRegion::Footer,
|
&DefaultRegion::Footer,
|
||||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ mod error;
|
||||||
pub use error::ComponentError;
|
pub use error::ComponentError;
|
||||||
|
|
||||||
mod definition;
|
mod definition;
|
||||||
pub use definition::{Component, ComponentRender};
|
pub use definition::{Component, ComponentClone, ComponentRender};
|
||||||
|
|
||||||
mod children;
|
mod children;
|
||||||
pub use children::Children;
|
pub use children::Children;
|
||||||
pub use children::{Child, ChildOp};
|
pub use children::ComponentGuard;
|
||||||
pub use children::{Typed, TypedOp};
|
pub use children::{Child, ChildOp, Embed};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
pub use message::{MessageLevel, StatusMessage};
|
pub use message::{MessageLevel, StatusMessage};
|
||||||
|
|
@ -29,7 +29,7 @@ pub use context::{AssetsOp, Context, ContextError, Contextual};
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// #[derive(AutoDefault)]
|
/// #[derive(AutoDefault, Clone)]
|
||||||
/// struct SampleComponent {
|
/// struct SampleComponent {
|
||||||
/// renderable: Option<FnIsRenderable>,
|
/// renderable: Option<FnIsRenderable>,
|
||||||
/// }
|
/// }
|
||||||
|
|
@ -39,12 +39,12 @@ pub use context::{AssetsOp, Context, ContextError, Contextual};
|
||||||
/// Self::default()
|
/// Self::default()
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn is_renderable(&self, cx: &mut Context) -> bool {
|
/// fn is_renderable(&self, cx: &Context) -> bool {
|
||||||
/// // Si hay callback, se usa; en caso contrario, se renderiza por defecto.
|
/// // Si hay callback, se usa; en caso contrario, se renderiza por defecto.
|
||||||
/// self.renderable.map_or(true, |f| f(cx))
|
/// self.renderable.map_or(true, |f| f(cx))
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn prepare_component(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
/// fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
/// Ok(html! { "Visible component" })
|
/// Ok(html! { "Visible component" })
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,28 @@ use crate::core::component::{Component, Context};
|
||||||
use crate::html::{html, Markup};
|
use crate::html::{html, Markup};
|
||||||
use crate::{builder_fn, AutoDefault, UniqueId};
|
use crate::{builder_fn, AutoDefault, UniqueId};
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
pub use parking_lot::RwLockReadGuard as ComponentReadGuard;
|
pub use parking_lot::MutexGuard as ComponentGuard;
|
||||||
pub use parking_lot::RwLockWriteGuard as ComponentWriteGuard;
|
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::vec::IntoIter;
|
use std::vec::IntoIter;
|
||||||
|
|
||||||
/// Representa un componente encapsulado de forma segura y compartida.
|
/// Representa un componente hijo encapsulado para su uso en una lista [`Children`].
|
||||||
///
|
#[derive(AutoDefault)]
|
||||||
/// Esta estructura permite manipular y renderizar un componente que implemente [`Component`], y
|
pub struct Child(Option<Mutex<Box<dyn Component>>>);
|
||||||
/// habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
|
|
||||||
#[derive(AutoDefault, Clone)]
|
impl Clone for Child {
|
||||||
pub struct Child(Option<Arc<RwLock<dyn Component>>>);
|
fn clone(&self) -> Self {
|
||||||
|
Child(self.0.as_ref().map(|m| Mutex::new(m.lock().clone_box())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Child {
|
impl fmt::Debug for Child {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match &self.0 {
|
match &self.0 {
|
||||||
None => write!(f, "Child(None)"),
|
None => write!(f, "Child(None)"),
|
||||||
Some(c) => write!(f, "Child({})", c.read().name()),
|
Some(c) => write!(f, "Child({})", c.lock().name()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +31,7 @@ impl fmt::Debug for Child {
|
||||||
impl Child {
|
impl Child {
|
||||||
/// Crea un nuevo `Child` a partir de un componente.
|
/// Crea un nuevo `Child` a partir de un componente.
|
||||||
pub fn with(component: impl Component) -> Self {
|
pub fn with(component: impl Component) -> Self {
|
||||||
Child(Some(Arc::new(RwLock::new(component))))
|
Child(Some(Mutex::new(Box::new(component))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Child BUILDER >**************************************************************************
|
// **< Child BUILDER >**************************************************************************
|
||||||
|
|
@ -40,11 +41,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 {
|
||||||
if let Some(c) = component {
|
self.0 = component.map(|c| Mutex::new(Box::new(c) as Box<dyn Component>));
|
||||||
self.0 = Some(Arc::new(RwLock::new(c)));
|
|
||||||
} else {
|
|
||||||
self.0 = None;
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,14 +50,14 @@ impl Child {
|
||||||
/// Devuelve el identificador del componente, si existe y está definido.
|
/// Devuelve el identificador del componente, si existe y está definido.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn id(&self) -> Option<String> {
|
pub fn id(&self) -> Option<String> {
|
||||||
self.0.as_ref().and_then(|c| c.read().id())
|
self.0.as_ref().and_then(|c| c.lock().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 fn render(&self, cx: &mut Context) -> Markup {
|
||||||
self.0.as_ref().map_or(html! {}, |c| c.write().render(cx))
|
self.0.as_ref().map_or(html! {}, |c| c.lock().render(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Child HELPERS >**************************************************************************
|
// **< Child HELPERS >**************************************************************************
|
||||||
|
|
@ -68,121 +65,144 @@ impl Child {
|
||||||
/// Devuelve el [`UniqueId`] del tipo del componente, si existe.
|
/// Devuelve el [`UniqueId`] del tipo del componente, si existe.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn type_id(&self) -> Option<UniqueId> {
|
fn type_id(&self) -> Option<UniqueId> {
|
||||||
self.0.as_ref().map(|c| c.read().type_id())
|
self.0.as_ref().map(|c| c.lock().type_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component + 'static> From<Embed<C>> for Child {
|
||||||
|
/// Convierte un [`Embed<C>`] en un [`Child`], consumiendo el componente tipado.
|
||||||
|
///
|
||||||
|
/// Útil cuando se tiene un [`Embed`] y se necesita añadirlo a una lista [`Children`]:
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// children.with_child(Child::from(my_embed));
|
||||||
|
/// // o equivalentemente:
|
||||||
|
/// children.with_child(my_embed.into());
|
||||||
|
/// ```
|
||||||
|
fn from(embed: Embed<C>) -> Self {
|
||||||
|
if let Some(m) = embed.0 {
|
||||||
|
Child(Some(Mutex::new(
|
||||||
|
Box::new(m.into_inner()) as Box<dyn Component>
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Child(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Component + 'static> From<T> for Child {
|
||||||
|
#[inline]
|
||||||
|
fn from(component: T) -> Self {
|
||||||
|
Child::with(component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Component + 'static> From<T> for ChildOp {
|
||||||
|
/// Convierte un componente en [`ChildOp::Add`], permitiendo pasar componentes directamente a
|
||||||
|
/// métodos como [`Children::with_child`] sin envolverlos explícitamente.
|
||||||
|
#[inline]
|
||||||
|
fn from(component: T) -> Self {
|
||||||
|
ChildOp::Add(Child::with(component))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Child> for ChildOp {
|
||||||
|
/// Convierte un [`Child`] en [`ChildOp::Add`].
|
||||||
|
#[inline]
|
||||||
|
fn from(child: Child) -> Self {
|
||||||
|
ChildOp::Add(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// *************************************************************************************************
|
// *************************************************************************************************
|
||||||
|
|
||||||
/// Variante tipada de [`Child`] para evitar conversiones de tipo durante el uso.
|
/// Contenedor tipado para un *único* componente de un tipo concreto conocido.
|
||||||
///
|
///
|
||||||
/// Esta estructura permite manipular y renderizar un componente concreto que implemente
|
/// A diferencia de [`Child`], que encapsula cualquier componente como `dyn Component`, `Embed`
|
||||||
/// [`Component`], y habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
|
/// mantiene el tipo concreto `C` y permite acceder directamente a sus métodos específicos a través
|
||||||
#[derive(AutoDefault, Clone)]
|
/// de [`get()`](Embed::get).
|
||||||
pub struct Typed<C: Component>(Option<Arc<RwLock<C>>>);
|
///
|
||||||
|
/// 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.
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Embed<C: Component>(Option<Mutex<C>>);
|
||||||
|
|
||||||
impl<C: Component> fmt::Debug for Typed<C> {
|
impl<C: Component + Clone> Clone for Embed<C> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Embed(self.0.as_ref().map(|m| Mutex::new(m.lock().clone())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "Typed(None)"),
|
None => write!(f, "Embed(None)"),
|
||||||
Some(c) => write!(f, "Typed({})", c.read().name()),
|
Some(c) => write!(f, "Embed({})", c.lock().name()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Component> Typed<C> {
|
impl<C: Component> Embed<C> {
|
||||||
/// Crea un nuevo `Typed` 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 {
|
||||||
Typed(Some(Arc::new(RwLock::new(component))))
|
Embed(Some(Mutex::new(component)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Typed BUILDER >**************************************************************************
|
// **< Embed BUILDER >**************************************************************************
|
||||||
|
|
||||||
/// Establece un componente nuevo, o lo vacía.
|
/// Establece un componente nuevo, o lo vacía.
|
||||||
///
|
///
|
||||||
/// Si se proporciona `Some(component)`, se encapsula como [`Typed`]; 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(|c| Arc::new(RwLock::new(c)));
|
self.0 = component.map(Mutex::new);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Typed GETTERS >**************************************************************************
|
// **< Embed GETTERS >**************************************************************************
|
||||||
|
|
||||||
/// Devuelve el identificador del componente, si existe y está definido.
|
/// Devuelve el identificador del componente, si existe y está definido.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn id(&self) -> Option<String> {
|
pub fn id(&self) -> Option<String> {
|
||||||
self.0.as_ref().and_then(|c| c.read().id())
|
self.0.as_ref().and_then(|c| c.lock().id())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve una **referencia inmutable** al componente interno.
|
/// Devuelve un acceso al componente incrustado.
|
||||||
///
|
///
|
||||||
/// - Devuelve `Some(ComponentReadGuard<C>)` si existe el componente, o `None` si está vacío.
|
/// - Devuelve `Some(ComponentGuard<C>)` si existe el componente, o `None` si está vacío.
|
||||||
/// - Permite realizar **múltiples lecturas concurrentes**.
|
/// - El acceso es **exclusivo**: mientras el *guard* esté activo, no habrá otros accesos.
|
||||||
/// - Mientras el *guard* esté activo, no se pueden realizar escrituras concurrentes (ver
|
|
||||||
/// [`borrow_mut`](Self::borrow_mut)).
|
|
||||||
/// - Se recomienda mantener el *guard* **el menor tiempo posible** para evitar bloqueos
|
/// - Se recomienda mantener el *guard* **el menor tiempo posible** para evitar bloqueos
|
||||||
/// innecesarios.
|
/// innecesarios.
|
||||||
|
/// - Para modificar el componente, declara el *guard* como `mut`:
|
||||||
|
/// `if let Some(mut c) = embed.get() { c.alter_title(...); }`.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// Lectura del nombre del componente:
|
|
||||||
///
|
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// let typed = Typed::with(Html::with(|_| html! { "Prueba" }));
|
/// let embed = Embed::with(Html::with(|_| html! { "Prueba" }));
|
||||||
/// {
|
/// {
|
||||||
/// if let Some(component) = typed.borrow() {
|
/// 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 `typed`.
|
/// }; // El *guard* se libera aquí, antes del *drop* de `embed`.
|
||||||
/// ```
|
|
||||||
pub fn borrow(&self) -> Option<ComponentReadGuard<'_, C>> {
|
|
||||||
self.0.as_ref().map(|a| a.read())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtiene una **referencia mutable exclusiva** al componente interno.
|
|
||||||
///
|
///
|
||||||
/// - Devuelve `Some(ComponentWriteGuard<C>)` si existe el componente, o `None` si está vacío.
|
/// let embed = Embed::with(Block::new().with_title(L10n::n("Título")));
|
||||||
/// - **Exclusivo**: mientras el *guard* esté activo, no habrá otros lectores ni escritores.
|
|
||||||
/// - Usar sólo para operaciones que **modifican** el estado interno.
|
|
||||||
/// - Igual que con [`borrow`](Self::borrow), se recomienda mantener el *guard* en un **ámbito
|
|
||||||
/// reducido**.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// Acceso mutable (ámbito corto):
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// let typed = Typed::with(Block::new().with_title(L10n::n("Título")));
|
|
||||||
/// {
|
/// {
|
||||||
/// if let Some(mut component) = typed.borrow_mut() {
|
/// if let Some(mut component) = embed.get() {
|
||||||
/// component.alter_title(L10n::n("Nuevo título"));
|
/// component.alter_title(L10n::n("Nuevo título"));
|
||||||
/// }
|
/// }
|
||||||
/// }; // El *guard* se libera aquí, antes del *drop* de `typed`.
|
/// }; // El *guard* se libera aquí, antes del *drop* de `embed`.
|
||||||
/// ```
|
/// ```
|
||||||
pub fn borrow_mut(&self) -> Option<ComponentWriteGuard<'_, C>> {
|
pub fn get(&self) -> Option<ComponentGuard<'_, C>> {
|
||||||
self.0.as_ref().map(|a| a.write())
|
self.0.as_ref().map(|m| m.lock())
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Typed 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 fn render(&self, cx: &mut Context) -> Markup {
|
||||||
self.0.as_ref().map_or(html! {}, |c| c.write().render(cx))
|
self.0.as_ref().map_or(html! {}, |c| c.lock().render(cx))
|
||||||
}
|
|
||||||
|
|
||||||
// **< Typed HELPERS >**************************************************************************
|
|
||||||
|
|
||||||
/// Método interno para convertir un componente tipado en un [`Child`].
|
|
||||||
#[inline]
|
|
||||||
fn into(self) -> Child {
|
|
||||||
if let Some(c) = &self.0 {
|
|
||||||
Child(Some(c.clone()))
|
|
||||||
} else {
|
|
||||||
Child(None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,37 +210,60 @@ impl<C: Component> Typed<C> {
|
||||||
|
|
||||||
/// 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.
|
||||||
Add(Child),
|
Add(Child),
|
||||||
|
/// Añade un hijo solo si la lista está vacía.
|
||||||
AddIfEmpty(Child),
|
AddIfEmpty(Child),
|
||||||
|
/// Añade varios hijos al final de la lista, en el orden recibido.
|
||||||
AddMany(Vec<Child>),
|
AddMany(Vec<Child>),
|
||||||
|
/// Inserta un hijo justo después del componente con el `id` dado, o al final si no existe.
|
||||||
InsertAfterId(&'static str, Child),
|
InsertAfterId(&'static str, Child),
|
||||||
|
/// Inserta un hijo justo antes del componente con el `id` dado, o al principio si no existe.
|
||||||
InsertBeforeId(&'static str, Child),
|
InsertBeforeId(&'static str, Child),
|
||||||
|
/// Inserta un hijo al principio de la lista.
|
||||||
Prepend(Child),
|
Prepend(Child),
|
||||||
|
/// Inserta varios hijos al principio de la lista, manteniendo el orden recibido.
|
||||||
PrependMany(Vec<Child>),
|
PrependMany(Vec<Child>),
|
||||||
|
/// Elimina el primer hijo con el `id` dado.
|
||||||
RemoveById(&'static str),
|
RemoveById(&'static str),
|
||||||
|
/// Sustituye el primer hijo con el `id` dado por otro componente.
|
||||||
ReplaceById(&'static str, Child),
|
ReplaceById(&'static str, Child),
|
||||||
Reset,
|
/// Vacía la lista eliminando todos los hijos.
|
||||||
}
|
|
||||||
|
|
||||||
/// Operaciones con un componente hijo tipado [`Typed<C>`] en una lista [`Children`].
|
|
||||||
pub enum TypedOp<C: Component> {
|
|
||||||
Add(Typed<C>),
|
|
||||||
AddIfEmpty(Typed<C>),
|
|
||||||
AddMany(Vec<Typed<C>>),
|
|
||||||
InsertAfterId(&'static str, Typed<C>),
|
|
||||||
InsertBeforeId(&'static str, Typed<C>),
|
|
||||||
Prepend(Typed<C>),
|
|
||||||
PrependMany(Vec<Typed<C>>),
|
|
||||||
RemoveById(&'static str),
|
|
||||||
ReplaceById(&'static str, Typed<C>),
|
|
||||||
Reset,
|
Reset,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lista ordenada de componentes hijo ([`Child`]) mantenida por un componente padre.
|
/// Lista ordenada de componentes hijo ([`Child`]) mantenida por un componente padre.
|
||||||
///
|
///
|
||||||
/// Esta lista permite añadir, modificar, renderizar y consultar componentes hijo en orden de
|
/// Permite añadir, modificar, renderizar y consultar componentes hijo en orden de inserción, con
|
||||||
/// inserción, soportando operaciones avanzadas como inserción relativa o reemplazo por
|
/// soporte para operaciones avanzadas como inserción relativa o reemplazo por identificador a
|
||||||
/// identificador.
|
/// través de [`ChildOp`].
|
||||||
|
///
|
||||||
|
/// Los tipos que completan este sistema son:
|
||||||
|
///
|
||||||
|
/// - [`Child`]: representa un componente hijo encapsulado dentro de la lista. Almacena cualquier
|
||||||
|
/// componente sin necesidad de conocer su tipo concreto.
|
||||||
|
/// - [`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
|
||||||
|
/// de `C`.
|
||||||
|
/// - [`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).
|
||||||
|
/// - [`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
|
||||||
|
///
|
||||||
|
/// Cualquier componente implementa `Into<ChildOp>` (equivalente a `ChildOp::Add`) e `Into<Child>`.
|
||||||
|
/// Gracias a esto, [`with_child`](Self::with_child) acepta un componente directamente o cualquier
|
||||||
|
/// variante de [`ChildOp`]:
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// // Añadir al final de la lista (implícito):
|
||||||
|
/// children.with_child(MiComponente::new());
|
||||||
|
///
|
||||||
|
/// // Operación explícita:
|
||||||
|
/// children.with_child(ChildOp::Prepend(MiComponente::new().into()));
|
||||||
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
pub struct Children(Vec<Child>);
|
pub struct Children(Vec<Child>);
|
||||||
|
|
||||||
|
|
@ -232,24 +275,15 @@ impl Children {
|
||||||
|
|
||||||
/// Crea una lista con un componente hijo inicial.
|
/// Crea una lista con un componente hijo inicial.
|
||||||
pub fn with(child: Child) -> Self {
|
pub fn with(child: Child) -> Self {
|
||||||
Self::default().with_child(ChildOp::Add(child))
|
Self::default().with_child(child)
|
||||||
}
|
|
||||||
|
|
||||||
/// Fusiona varias listas de `Children` en una sola.
|
|
||||||
pub(crate) fn merge(mixes: &[Option<&Children>]) -> Self {
|
|
||||||
let mut opt = Children::default();
|
|
||||||
for m in mixes.iter().flatten() {
|
|
||||||
opt.0.extend(m.0.iter().cloned());
|
|
||||||
}
|
|
||||||
opt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Children BUILDER >***********************************************************************
|
// **< Children BUILDER >***********************************************************************
|
||||||
|
|
||||||
/// Ejecuta una operación con [`ChildOp`] en la lista.
|
/// Añade un componente hijo o aplica una operación [`ChildOp`] sobre la lista.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
match op {
|
match op.into() {
|
||||||
ChildOp::Add(any) => self.add(any),
|
ChildOp::Add(any) => self.add(any),
|
||||||
ChildOp::AddIfEmpty(any) => self.add_if_empty(any),
|
ChildOp::AddIfEmpty(any) => self.add_if_empty(any),
|
||||||
ChildOp::AddMany(many) => self.add_many(many),
|
ChildOp::AddMany(many) => self.add_many(many),
|
||||||
|
|
@ -263,35 +297,16 @@ impl Children {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ejecuta una operación con [`TypedOp`] en la lista.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_typed<C: Component>(mut self, op: TypedOp<C>) -> Self {
|
|
||||||
match op {
|
|
||||||
TypedOp::Add(typed) => self.add(typed.into()),
|
|
||||||
TypedOp::AddIfEmpty(typed) => self.add_if_empty(typed.into()),
|
|
||||||
TypedOp::AddMany(many) => self.add_many(many.into_iter().map(Typed::<C>::into)),
|
|
||||||
TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.into()),
|
|
||||||
TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.into()),
|
|
||||||
TypedOp::Prepend(typed) => self.prepend(typed.into()),
|
|
||||||
TypedOp::PrependMany(many) => self.prepend_many(many.into_iter().map(Typed::<C>::into)),
|
|
||||||
TypedOp::RemoveById(id) => self.remove_by_id(id),
|
|
||||||
TypedOp::ReplaceById(id, typed) => self.replace_by_id(id, typed.into()),
|
|
||||||
TypedOp::Reset => self.reset(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un componente hijo al final de la lista.
|
/// Añade un componente hijo al final de la lista.
|
||||||
///
|
|
||||||
/// Es un atajo para `children.alter_child(ChildOp::Add(child))`.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub 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 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() {
|
||||||
self.0.push(child);
|
self.0.push(child);
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +387,7 @@ impl Children {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserta un hijo al principio de la colección.
|
/// 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);
|
||||||
|
|
@ -427,7 +442,7 @@ impl IntoIterator for Children {
|
||||||
|
|
||||||
/// Consume la estructura `Children`, devolviendo un iterador que consume los elementos.
|
/// Consume la estructura `Children`, devolviendo un iterador que consume los elementos.
|
||||||
///
|
///
|
||||||
/// # Ejemplo de uso:
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// let children = Children::new().with(child1).with(child2);
|
/// let children = Children::new().with(child1).with(child2);
|
||||||
|
|
@ -446,7 +461,7 @@ impl<'a> IntoIterator for &'a Children {
|
||||||
|
|
||||||
/// Itera sobre una referencia inmutable de `Children`, devolviendo un iterador de referencia.
|
/// Itera sobre una referencia inmutable de `Children`, devolviendo un iterador de referencia.
|
||||||
///
|
///
|
||||||
/// # Ejemplo de uso:
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// let children = Children::new().with(child1).with(child2);
|
/// let children = Children::new().with(child1).with(child2);
|
||||||
|
|
@ -465,7 +480,7 @@ impl<'a> IntoIterator for &'a mut Children {
|
||||||
|
|
||||||
/// Itera sobre una referencia mutable de `Children`, devolviendo un iterador mutable.
|
/// Itera sobre una referencia mutable de `Children`, devolviendo un iterador mutable.
|
||||||
///
|
///
|
||||||
/// # Ejemplo de uso:
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// let mut children = Children::new().with(child1).with(child2);
|
/// let mut children = Children::new().with(child1).with(child2);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::core::component::{ChildOp, MessageLevel, StatusMessage};
|
use crate::core::component::{ChildOp, MessageLevel, StatusMessage};
|
||||||
use crate::core::theme::all::DEFAULT_THEME;
|
use crate::core::theme::all::DEFAULT_THEME;
|
||||||
use crate::core::theme::{ChildrenInRegions, RegionRef, TemplateRef, ThemeRef};
|
use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef};
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
use crate::html::{html, Markup, RoutePath};
|
use crate::html::{html, Markup, RoutePath};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
|
|
@ -10,6 +10,7 @@ use crate::service::HttpRequest;
|
||||||
use crate::{builder_fn, util, CowStr};
|
use crate::{builder_fn, util, CowStr};
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
use std::cell::Cell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
|
@ -136,9 +137,15 @@ pub trait Contextual: LangId {
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_assets(self, op: AssetsOp) -> Self;
|
fn with_assets(self, op: AssetsOp) -> Self;
|
||||||
|
|
||||||
/// Opera con [`ChildOp`] en una región del documento.
|
/// Añade un componente o aplica una operación [`ChildOp`] en la región por defecto del
|
||||||
|
/// documento.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_child_in(self, region_ref: RegionRef, op: ChildOp) -> Self;
|
fn with_child(self, op: impl Into<ChildOp>) -> Self;
|
||||||
|
|
||||||
|
/// Añade un componente o aplica una operación [`ChildOp`] en una región específica del
|
||||||
|
/// documento.
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_child_in(self, region_ref: RegionRef, op: impl Into<ChildOp>) -> Self;
|
||||||
|
|
||||||
// **< Contextual GETTERS >*********************************************************************
|
// **< Contextual GETTERS >*********************************************************************
|
||||||
|
|
||||||
|
|
@ -214,7 +221,7 @@ pub trait Contextual: LangId {
|
||||||
/// nombre corto del tipo en minúsculas y `<n>` un contador incremental interno del contexto. Es
|
/// nombre corto del tipo en minúsculas y `<n>` un contador incremental interno del contexto. Es
|
||||||
/// útil para asignar identificadores HTML predecibles cuando el componente no recibe uno
|
/// útil para asignar identificadores HTML predecibles cuando el componente no recibe uno
|
||||||
/// explícito.
|
/// explícito.
|
||||||
fn required_id<T>(&mut self, id: Option<String>) -> String;
|
fn required_id<T>(&self, id: Option<String>) -> String;
|
||||||
|
|
||||||
/// Acumula un [`StatusMessage`] en el contexto para notificar al visitante.
|
/// Acumula un [`StatusMessage`] en el contexto para notificar al visitante.
|
||||||
///
|
///
|
||||||
|
|
@ -291,7 +298,7 @@ pub struct Context {
|
||||||
javascripts: Assets<JavaScript>, // Scripts JavaScript.
|
javascripts: Assets<JavaScript>, // Scripts JavaScript.
|
||||||
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>, &'static str)>, // Parámetros en ejecución.
|
||||||
id_counter : usize, // Contador para generar identificadores únicos.
|
id_counter : Cell<usize>, // Cell permite incrementarlo desde &self en required_id().
|
||||||
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
|
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -319,7 +326,7 @@ impl Context {
|
||||||
javascripts: Assets::<JavaScript>::new(),
|
javascripts: Assets::<JavaScript>::new(),
|
||||||
regions : ChildrenInRegions::default(),
|
regions : ChildrenInRegions::default(),
|
||||||
params : HashMap::default(),
|
params : HashMap::default(),
|
||||||
id_counter : 0,
|
id_counter : Cell::new(0),
|
||||||
messages : Vec::new(),
|
messages : Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -556,8 +563,15 @@ impl Contextual for Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.regions.alter_child_in(region_ref, op);
|
self.regions
|
||||||
|
.alter_child_in(&DefaultRegion::Content, op.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_child_in(mut self, region_ref: RegionRef, op: impl Into<ChildOp>) -> Self {
|
||||||
|
self.regions.alter_child_in(region_ref, op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -593,7 +607,7 @@ impl Contextual for Context {
|
||||||
|
|
||||||
// **< Contextual HELPERS >*********************************************************************
|
// **< Contextual HELPERS >*********************************************************************
|
||||||
|
|
||||||
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
fn required_id<T>(&self, id: Option<String>) -> String {
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
id
|
id
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -607,8 +621,8 @@ impl Contextual for Context {
|
||||||
} else {
|
} else {
|
||||||
prefix
|
prefix
|
||||||
};
|
};
|
||||||
self.id_counter += 1;
|
self.id_counter.set(self.id_counter.get() + 1);
|
||||||
util::join!(prefix, "-", self.id_counter.to_string())
|
util::join!(prefix, "-", self.id_counter.get().to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,16 @@ use crate::core::theme::ThemeRef;
|
||||||
use crate::core::{AnyInfo, TypeInfo};
|
use crate::core::{AnyInfo, TypeInfo};
|
||||||
use crate::html::{html, Markup};
|
use crate::html::{html, Markup};
|
||||||
|
|
||||||
|
/// Permite clonar un componente.
|
||||||
|
///
|
||||||
|
/// 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
|
||||||
|
/// permite clonar componentes sin conocer su tipo concreto en tiempo de compilación.
|
||||||
|
pub trait ComponentClone {
|
||||||
|
/// Devuelve un clon del componente encapsulado en un [`Box<dyn Component>`].
|
||||||
|
fn clone_box(&self) -> Box<dyn Component>;
|
||||||
|
}
|
||||||
|
|
||||||
/// Define la función de renderizado para todos los componentes.
|
/// Define la función de renderizado para todos los componentes.
|
||||||
///
|
///
|
||||||
/// Este *trait* se implementa automáticamente en cualquier tipo (componente) que implemente
|
/// Este *trait* se implementa automáticamente en cualquier tipo (componente) que implemente
|
||||||
|
|
@ -20,7 +30,16 @@ pub trait ComponentRender {
|
||||||
/// [`Getters`](crate::Getters) para acceder a sus datos. Deberán implementar explícitamente el
|
/// [`Getters`](crate::Getters) para acceder a sus datos. Deberán implementar explícitamente el
|
||||||
/// método [`new()`](Self::new) y podrán sobrescribir los demás métodos para personalizar su
|
/// método [`new()`](Self::new) y podrán sobrescribir los demás métodos para personalizar su
|
||||||
/// comportamiento.
|
/// comportamiento.
|
||||||
pub trait Component: AnyInfo + ComponentRender + Send + Sync {
|
///
|
||||||
|
/// # Requisito: derivar `Clone`
|
||||||
|
///
|
||||||
|
/// 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`—,
|
||||||
|
/// [`ComponentClone`] se implementa automáticamente mediante una *impl* blanket solo para los
|
||||||
|
/// tipos 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).
|
||||||
|
pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
|
||||||
/// Crea una nueva instancia del componente.
|
/// Crea una nueva instancia del componente.
|
||||||
///
|
///
|
||||||
/// Por convención suele devolver `Self::default()`.
|
/// Por convención suele devolver `Self::default()`.
|
||||||
|
|
@ -54,61 +73,76 @@ pub trait Component: AnyInfo + 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.
|
/// renderizan o no en función del contexto de renderizado. Recibe solo una referencia
|
||||||
|
/// 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 del
|
||||||
/// componente para permitir que instancias concretas del mismo puedan decidir dinámicamente si
|
/// componente para permitir que instancias concretas del mismo puedan decidir dinámicamente si
|
||||||
/// se renderizan o no.
|
/// se renderizan o no.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn is_renderable(&self, cx: &mut Context) -> bool {
|
fn is_renderable(&self, cx: &Context) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configura el componente justo antes de preparar el renderizado.
|
/// Configura el estado interno del componente antes de generar el marcado.
|
||||||
///
|
///
|
||||||
/// Este método puede sobrescribirse para modificar la estructura interna del componente o el
|
/// Segundo paso del [ciclo de renderizado](ComponentRender): se ejecuta tras comprobar
|
||||||
/// contexto antes de renderizarlo. Por defecto no hace nada.
|
/// [`is_renderable()`](Self::is_renderable) y antes de la acción
|
||||||
|
/// [`BeforeRender`](crate::base::action::component::BeforeRender) y de
|
||||||
|
/// [`prepare()`](Self::prepare). Recibe solo una referencia compartida al contexto porque su
|
||||||
|
/// propósito es mutar el propio componente, no el contexto. Por defecto no hace nada.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn setup_before_prepare(&mut self, cx: &mut Context) {}
|
fn setup(&mut self, cx: &Context) {}
|
||||||
|
|
||||||
/// Versión del componente para preparar su propio renderizado.
|
/// Genera el marcado HTML del componente cuando ningún tema lo sobrescribe.
|
||||||
///
|
///
|
||||||
/// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente
|
/// Cuarto paso del [ciclo de renderizado](ComponentRender): se invoca tras
|
||||||
/// durante el proceso de construcción del documento cuando ningún tema sobrescribe el
|
/// [`setup()`](Self::setup) y la acción
|
||||||
/// renderizado mediante [`Theme::handle_component()`](crate::core::theme::Theme::handle_component).
|
/// [`BeforeRender`](crate::base::action::component::BeforeRender), pero solo si ningún tema
|
||||||
|
/// en la cadena devuelve `Some` en
|
||||||
|
/// [`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
|
||||||
/// temas puedan implementar [`Theme::handle_component()`](crate::core::theme::Theme::handle_component)
|
/// temas puedan implementar `handle_component()` sin depender de los detalles internos.
|
||||||
/// sin depender de los detalles internos del componente.
|
|
||||||
///
|
///
|
||||||
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`).
|
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`). En caso de error, devuelve un
|
||||||
///
|
/// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*).
|
||||||
/// En caso de error, devuelve un [`ComponentError`] que puede incluir un marcado alternativo
|
|
||||||
/// (*fallback*) para sustituir al componente fallido.
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(html! {})
|
Ok(html! {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// *************************************************************************************************
|
||||||
|
|
||||||
|
impl<T: Component + Clone + 'static> ComponentClone for T {
|
||||||
|
fn clone_box(&self) -> Box<dyn Component> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************************************************************************************
|
||||||
|
|
||||||
/// Implementa [`render()`](ComponentRender::render) para todos los componentes.
|
/// Implementa [`render()`](ComponentRender::render) para todos los componentes.
|
||||||
///
|
///
|
||||||
/// El proceso de renderizado de cada componente sigue esta secuencia:
|
/// El proceso de renderizado de cada componente sigue esta secuencia:
|
||||||
///
|
///
|
||||||
/// 1. Ejecuta [`is_renderable()`](Component::is_renderable) para ver si puede renderizarse en el
|
/// 1. Ejecuta [`is_renderable()`](Component::is_renderable) para ver si puede renderizarse en el
|
||||||
/// contexto actual. Si no es así, devuelve un [`Markup`] vacío.
|
/// contexto actual. Si no es así, devuelve un [`Markup`] vacío.
|
||||||
/// 2. Ejecuta [`setup_before_prepare()`](Component::setup_before_prepare) para que el componente
|
/// 2. Ejecuta [`setup()`](Component::setup) para que el componente
|
||||||
/// pueda ajustar su estructura interna o modificar el contexto.
|
/// pueda ajustar su estructura interna.
|
||||||
/// 3. Despacha [`action::component::BeforeRender<C>`](crate::base::action::component::BeforeRender)
|
/// 3. Despacha [`action::component::BeforeRender<C>`](crate::base::action::component::BeforeRender)
|
||||||
/// para que las extensiones puedan hacer ajustes previos.
|
/// para que las extensiones puedan hacer ajustes previos.
|
||||||
/// 4. **Prepara el renderizado del componente** recorriendo la cadena de temas (hijo → padre →
|
/// 4. Prepara el renderizado del componente, recorre la cadena de temas (hijo > padre > abuelo...)
|
||||||
/// abuelo…) llamando a [`Theme::handle_component()`](crate::core::theme::Theme::handle_component)
|
/// llamando a [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) en cada
|
||||||
/// en cada nivel hasta que uno devuelva `Some`. Si ninguno lo sobrescribe, llama a
|
/// nivel hasta que uno devuelva `Some`. Si ninguno lo sobrescribe, llama al
|
||||||
/// [`Component::prepare_component()`](Component::prepare_component) del propio componente.
|
/// [`Component::prepare()`](Component::prepare) del propio componente.
|
||||||
/// 5. Despacha [`action::component::AfterRender<C>`](crate::base::action::component::AfterRender)
|
/// 5. Despacha [`action::component::AfterRender<C>`](crate::base::action::component::AfterRender)
|
||||||
/// para que las extensiones puedan reaccionar con sus últimos ajustes.
|
/// para que las extensiones puedan reaccionar con sus últimos ajustes.
|
||||||
/// 6. Despacha [`action::component::TransformMarkup<C>`](crate::base::action::component::TransformMarkup)
|
/// 6. Finalmente despacha
|
||||||
/// para que las extensiones puedan modificar el HTML final antes de devolverlo.
|
/// [`action::component::TransformMarkup<C>`](crate::base::action::component::TransformMarkup)
|
||||||
|
/// para que las extensiones puedan trabajar sobre el HTML final para modificarlo antes de
|
||||||
|
/// devolverlo.
|
||||||
/// 7. Devuelve el [`Markup`] resultante.
|
/// 7. Devuelve el [`Markup`] resultante.
|
||||||
impl<C: Component> ComponentRender for C {
|
impl<C: Component> ComponentRender for C {
|
||||||
fn render(&mut self, cx: &mut Context) -> Markup {
|
fn render(&mut self, cx: &mut Context) -> Markup {
|
||||||
|
|
@ -118,7 +152,7 @@ impl<C: Component> ComponentRender for C {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configura el componente antes de preparar.
|
// Configura el componente antes de preparar.
|
||||||
self.setup_before_prepare(cx);
|
self.setup(cx);
|
||||||
|
|
||||||
// Acciones de las extensiones antes de renderizar el componente.
|
// Acciones de las extensiones antes de renderizar el componente.
|
||||||
action::component::BeforeRender::dispatch(self, cx);
|
action::component::BeforeRender::dispatch(self, cx);
|
||||||
|
|
@ -132,7 +166,7 @@ impl<C: Component> ComponentRender for C {
|
||||||
}
|
}
|
||||||
t = theme.parent();
|
t = theme.parent();
|
||||||
}
|
}
|
||||||
self.prepare_component(cx)
|
self.prepare(cx)
|
||||||
} {
|
} {
|
||||||
Ok(markup) => markup,
|
Ok(markup) => markup,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::{AutoDefault, Getters};
|
||||||
|
|
||||||
/// Error producido durante el renderizado de un componente.
|
/// Error producido durante el renderizado de un componente.
|
||||||
///
|
///
|
||||||
/// Se usa en [`Component::prepare_component()`](super::Component::prepare_component) para devolver
|
/// Se usa en [`Component::prepare()`](super::Component::prepare) para devolver
|
||||||
/// un [`Err`]. Puede incluir un marcado HTML alternativo para renderizar el componente de manera
|
/// un [`Err`]. Puede incluir un marcado HTML alternativo para renderizar el componente de manera
|
||||||
/// diferente en caso de error.
|
/// diferente en caso de error.
|
||||||
///
|
///
|
||||||
|
|
@ -11,10 +11,11 @@ use crate::{AutoDefault, Getters};
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # #[derive(Clone)]
|
||||||
/// # struct MyComponent;
|
/// # struct MyComponent;
|
||||||
/// # impl Component for MyComponent {
|
/// # impl Component for MyComponent {
|
||||||
/// # fn new() -> Self { MyComponent }
|
/// # fn new() -> Self { MyComponent }
|
||||||
/// fn prepare_component(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
/// 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." } }))
|
||||||
/// }
|
/// }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::base::component::{Html, Intro, IntroOpening};
|
use crate::base::component::{Html, Intro, IntroOpening};
|
||||||
use crate::core::component::{Child, ChildOp, Component, ComponentError, Context, Contextual};
|
use crate::core::component::{ChildOp, Component, ComponentError, Context, Contextual};
|
||||||
use crate::core::extension::Extension;
|
use crate::core::extension::Extension;
|
||||||
use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef};
|
use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef};
|
||||||
use crate::global;
|
use crate::global;
|
||||||
|
|
@ -195,7 +195,7 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
///
|
///
|
||||||
/// - `None` si este tema no sobrescribe el renderizado. Es la implementación por defecto. El
|
/// - `None` si este tema no sobrescribe el renderizado. Es la implementación por defecto. El
|
||||||
/// sistema continúa con el siguiente tema de la cadena y, si ninguno lo sobrescribe, usa
|
/// sistema continúa con el siguiente tema de la cadena y, si ninguno lo sobrescribe, usa
|
||||||
/// [`Component::prepare_component()`](crate::core::component::Component::prepare_component).
|
/// [`Component::prepare()`](crate::core::component::Component::prepare).
|
||||||
/// El tema puede mutar el componente antes de devolver `None`, dejando que otro nivel de la
|
/// El tema puede mutar el componente antes de devolver `None`, dejando que otro nivel de la
|
||||||
/// cadena se encargue del renderizado.
|
/// cadena se encargue del renderizado.
|
||||||
/// - `Some(Ok(markup))` con el HTML generado por el tema para el componente.
|
/// - `Some(Ok(markup))` con el HTML generado por el tema para el componente.
|
||||||
|
|
@ -247,14 +247,17 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
.alter_template(&DefaultTemplate::Error)
|
.alter_template(&DefaultTemplate::Error)
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&DefaultRegion::Content,
|
&DefaultRegion::Content,
|
||||||
ChildOp::Prepend(Child::with(Html::with(move |cx| {
|
ChildOp::Prepend(
|
||||||
html! {
|
Html::with(move |cx| {
|
||||||
div {
|
html! {
|
||||||
h1 { (L10n::l("error403_alert").using(cx)) }
|
div {
|
||||||
p { (L10n::l("error403_help").using(cx)) }
|
h1 { (L10n::l("error403_alert").using(cx)) }
|
||||||
|
p { (L10n::l("error403_help").using(cx)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}))),
|
.into(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,14 +273,17 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
.alter_template(&DefaultTemplate::Error)
|
.alter_template(&DefaultTemplate::Error)
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&DefaultRegion::Content,
|
&DefaultRegion::Content,
|
||||||
ChildOp::Prepend(Child::with(Html::with(move |cx| {
|
ChildOp::Prepend(
|
||||||
html! {
|
Html::with(move |cx| {
|
||||||
div {
|
html! {
|
||||||
h1 { (L10n::l("error404_alert").using(cx)) }
|
div {
|
||||||
p { (L10n::l("error404_help").using(cx)) }
|
h1 { (L10n::l("error404_alert").using(cx)) }
|
||||||
|
p { (L10n::l("error404_help").using(cx)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}))),
|
.into(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,19 +306,20 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
.alter_template(&DefaultTemplate::Error)
|
.alter_template(&DefaultTemplate::Error)
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&DefaultRegion::Content,
|
&DefaultRegion::Content,
|
||||||
ChildOp::Prepend(Child::with(
|
ChildOp::Prepend(
|
||||||
Intro::new()
|
Intro::new()
|
||||||
.with_title(L10n::l("error_code").with_arg("code", code.to_string()))
|
.with_title(L10n::l("error_code").with_arg("code", code.to_string()))
|
||||||
.with_slogan(L10n::n(code.to_string()))
|
.with_slogan(L10n::n(code.to_string()))
|
||||||
.with_button(None)
|
.with_button(None)
|
||||||
.with_opening(IntroOpening::Custom)
|
.with_opening(IntroOpening::Custom)
|
||||||
.add_child(Html::with(move |cx| {
|
.with_child(Html::with(move |cx| {
|
||||||
html! {
|
html! {
|
||||||
h1 { (alert.using(cx)) }
|
h1 { (alert.using(cx)) }
|
||||||
p { (help.using(cx)) }
|
p { (help.using(cx)) }
|
||||||
}
|
}
|
||||||
})),
|
}))
|
||||||
)),
|
.into(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,42 @@
|
||||||
use crate::core::component::{Child, ChildOp, Children};
|
use crate::core::component::{Child, ChildOp, Children, Component};
|
||||||
use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef};
|
use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef};
|
||||||
use crate::{builder_fn, AutoDefault, UniqueId};
|
use crate::{builder_fn, AutoDefault, UniqueId};
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::LazyLock;
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
// Conjunto de regiones globales asociadas a un tema específico.
|
// Permite almacenar un componente como prototipo en regiones globales.
|
||||||
static THEME_REGIONS: LazyLock<RwLock<HashMap<UniqueId, ChildrenInRegions>>> =
|
//
|
||||||
|
// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. En cada
|
||||||
|
// llamada a [`as_child`](Self::as_child) produce un clon fresco del estado original, de modo que
|
||||||
|
// cada página renderiza el componente desde su estado inicial sin acumular mutaciones de peticiones
|
||||||
|
// anteriores.
|
||||||
|
trait ComponentGlobal: Send + Sync {
|
||||||
|
// Devuelve un nuevo [`Child`] con una copia independiente del componente original.
|
||||||
|
fn as_child(&self) -> Child;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Component + Clone + 'static> ComponentGlobal for T {
|
||||||
|
#[inline]
|
||||||
|
fn as_child(&self) -> Child {
|
||||||
|
Child::with(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapa de nombre de región a lista de prototipos de componentes.
|
||||||
|
type RegionComponents = HashMap<String, Vec<Arc<dyn ComponentGlobal>>>;
|
||||||
|
|
||||||
|
// Regiones globales con prototipos asociados a un tema específico.
|
||||||
|
static THEME_REGIONS: LazyLock<RwLock<HashMap<UniqueId, RegionComponents>>> =
|
||||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
// Conjunto de regiones globales comunes a todos los temas.
|
// Regiones globales con prototipos comunes a todos los temas.
|
||||||
static COMMON_REGIONS: LazyLock<RwLock<ChildrenInRegions>> =
|
static COMMON_REGIONS: LazyLock<RwLock<RegionComponents>> =
|
||||||
LazyLock::new(|| RwLock::new(ChildrenInRegions::default()));
|
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
|
// *************************************************************************************************
|
||||||
|
|
||||||
// Contenedor interno de componentes agrupados por región.
|
// Contenedor interno de componentes agrupados por región.
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
|
|
@ -21,39 +44,84 @@ pub(crate) struct ChildrenInRegions(HashMap<String, Children>);
|
||||||
|
|
||||||
impl ChildrenInRegions {
|
impl ChildrenInRegions {
|
||||||
pub fn with(region_ref: RegionRef, child: Child) -> Self {
|
pub fn with(region_ref: RegionRef, child: Child) -> Self {
|
||||||
Self::default().with_child_in(region_ref, ChildOp::Add(child))
|
Self::default().with_child_in(region_ref, child)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
pub fn with_child_in(mut self, region_ref: RegionRef, op: impl Into<ChildOp>) -> Self {
|
||||||
|
let child = op.into();
|
||||||
if let Some(region) = self.0.get_mut(region_ref.name()) {
|
if let Some(region) = self.0.get_mut(region_ref.name()) {
|
||||||
region.alter_child(op);
|
region.alter_child(child);
|
||||||
} else {
|
} else {
|
||||||
self.0
|
self.0.insert(
|
||||||
.insert(region_ref.name().to_owned(), Children::new().with_child(op));
|
region_ref.name().to_owned(),
|
||||||
|
Children::new().with_child(child),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn children_for(&self, theme_ref: ThemeRef, region_ref: RegionRef) -> Children {
|
/// Construye una lista de componentes frescos para la región indicada.
|
||||||
|
///
|
||||||
|
/// El orden es: prototipos globales comunes → children propios de la página →
|
||||||
|
/// prototipos específicos del tema activo.
|
||||||
|
///
|
||||||
|
/// Los prototipos globales se clonan en cada llamada (clon profundo gracias a
|
||||||
|
/// [`ComponentClone`]), garantizando que `setup()` siempre parte del estado
|
||||||
|
/// inicial. Los children propios de la página se mueven (son por petición y no necesitan
|
||||||
|
/// clonarse).
|
||||||
|
///
|
||||||
|
/// [`ComponentClone`]: crate::core::component::ComponentClone
|
||||||
|
pub fn children_for(&mut self, theme_ref: ThemeRef, region_ref: RegionRef) -> Children {
|
||||||
let name = region_ref.name();
|
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();
|
||||||
|
|
||||||
if let Some(r) = themed.get(&theme_ref.type_id()) {
|
let mut result = Children::new();
|
||||||
Children::merge(&[common.0.get(name), self.0.get(name), r.0.get(name)])
|
|
||||||
} else {
|
// 1. Prototipos globales comunes — clon fresco por cada página.
|
||||||
Children::merge(&[common.0.get(name), self.0.get(name)])
|
if let Some(protos) = common.get(name) {
|
||||||
|
for proto in protos {
|
||||||
|
result.add(proto.as_child());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 2. Children propios de la página — se mueven (son por petición, no requieren clonado).
|
||||||
|
if let Some(page_children) = self.0.remove(name) {
|
||||||
|
for child in page_children {
|
||||||
|
result.add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. Prototipos del tema activo — clon fresco por cada página.
|
||||||
|
if let Some(theme_map) = themed.get(&theme_ref.type_id()) {
|
||||||
|
if let Some(protos) = theme_map.get(name) {
|
||||||
|
for proto in protos {
|
||||||
|
result.add(proto.as_child());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// *************************************************************************************************
|
||||||
|
|
||||||
/// Añade componentes a regiones globales o específicas de un tema.
|
/// Añade componentes a regiones globales o específicas de un tema.
|
||||||
///
|
///
|
||||||
/// Cada variante indica la región en la que se añade el componente usando [`Self::add()`]. Los
|
/// Los componentes se almacenan como **prototipos**: cada página recibe un clon fresco en el
|
||||||
/// componentes añadidos se mantienen durante toda la ejecución y se inyectan automáticamente al
|
/// momento del renderizado, de modo que `setup()` se ejecuta siempre sobre un
|
||||||
/// renderizar los documentos HTML que utilizan esas regiones, como las páginas de contenido
|
/// estado inicial limpio sin acumular mutaciones de peticiones anteriores.
|
||||||
/// ([`Page`](crate::response::page::Page)).
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// // Banner global en la región de contenido.
|
||||||
|
/// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" }));
|
||||||
|
///
|
||||||
|
/// // Texto en la cabecera, visible en todos los temas.
|
||||||
|
/// InRegion::Global(&DefaultRegion::Header).add(Html::with(|_| html! { "Publicidad" }));
|
||||||
|
/// ```
|
||||||
pub enum InRegion {
|
pub enum InRegion {
|
||||||
/// Región principal de **contenido** por defecto.
|
/// Región principal de **contenido** por defecto.
|
||||||
///
|
///
|
||||||
|
|
@ -81,50 +149,55 @@ pub enum InRegion {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InRegion {
|
impl InRegion {
|
||||||
/// Añade un componente a la región indicada por la variante.
|
/// Añade un componente como prototipo en la región indicada por la variante.
|
||||||
|
///
|
||||||
|
/// El componente se almacena internamente como prototipo. Cada vez que se renderiza una página,
|
||||||
|
/// se genera un clon fresco del estado original, garantizando que `setup()` no
|
||||||
|
/// acumula estado entre peticiones.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// // Banner global en la región por defecto.
|
/// // Banner global en la región por defecto.
|
||||||
/// InRegion::Content.add(Child::with(Html::with(|_| {
|
/// InRegion::Content.add(Html::with(|_| {
|
||||||
/// html! { "🎉 ¡Bienvenido!" }
|
/// html! { "🎉 ¡Bienvenido!" }
|
||||||
/// })));
|
/// }));
|
||||||
///
|
///
|
||||||
/// // Texto en la cabecera.
|
/// // Texto en la cabecera.
|
||||||
/// InRegion::Global(&DefaultRegion::Header).add(Child::with(Html::with(|_| {
|
/// InRegion::Global(&DefaultRegion::Header).add(Html::with(|_| {
|
||||||
/// html! { "Publicidad" }
|
/// html! { "Publicidad" }
|
||||||
/// })));
|
/// }));
|
||||||
///
|
///
|
||||||
/// // Contenido sólo para la región del pie de página en un tema concreto.
|
/// // Contenido sólo para la región del pie de página en un tema concreto.
|
||||||
/// InRegion::ForTheme(&theme::Basic, &DefaultRegion::Footer).add(Child::with(Html::with(|_| {
|
/// InRegion::ForTheme(&theme::Basic, &DefaultRegion::Footer).add(Html::with(|_| {
|
||||||
/// html! { "Aviso legal" }
|
/// html! { "Aviso legal" }
|
||||||
/// })));
|
/// }));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn add(&self, child: Child) -> &Self {
|
pub fn add(&self, component: impl Component + Clone + 'static) -> &Self {
|
||||||
|
let proto: Arc<dyn ComponentGlobal> = Arc::new(component);
|
||||||
match self {
|
match self {
|
||||||
InRegion::Content => Self::add_to_common(&DefaultRegion::Content, child),
|
InRegion::Content => Self::add_to_common(&DefaultRegion::Content, proto),
|
||||||
InRegion::Global(region_ref) => Self::add_to_common(*region_ref, child),
|
InRegion::Global(region_ref) => Self::add_to_common(*region_ref, proto),
|
||||||
InRegion::ForTheme(theme_ref, region_ref) => {
|
InRegion::ForTheme(theme_ref, region_ref) => {
|
||||||
let mut regions = THEME_REGIONS.write();
|
THEME_REGIONS
|
||||||
if let Some(r) = regions.get_mut(&theme_ref.type_id()) {
|
.write()
|
||||||
r.alter_child_in(*region_ref, ChildOp::Add(child));
|
.entry(theme_ref.type_id())
|
||||||
} else {
|
.or_default()
|
||||||
regions.insert(
|
.entry((*region_ref).name().to_owned())
|
||||||
theme_ref.type_id(),
|
.or_default()
|
||||||
ChildrenInRegions::with(*region_ref, child),
|
.push(proto);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn add_to_common(region_ref: RegionRef, child: Child) {
|
fn add_to_common(region_ref: RegionRef, proto: Arc<dyn ComponentGlobal>) {
|
||||||
COMMON_REGIONS
|
COMMON_REGIONS
|
||||||
.write()
|
.write()
|
||||||
.alter_child_in(region_ref, ChildOp::Add(child));
|
.entry(region_ref.name().to_owned())
|
||||||
|
.or_default()
|
||||||
|
.push(proto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ use std::str::FromStr;
|
||||||
///
|
///
|
||||||
/// - Las absolutas **no aceptan** decimales (p. ej., `"1.5px"` sería erróneo).
|
/// - Las absolutas **no aceptan** decimales (p. ej., `"1.5px"` sería erróneo).
|
||||||
/// - Se aceptan signos `+`/`-` en todas las unidades (p. ej., `"-12px"`, `"+0.5em"`).
|
/// - Se aceptan signos `+`/`-` en todas las unidades (p. ej., `"-12px"`, `"+0.5em"`).
|
||||||
/// - La comparación de unidad es *case-insensitive* al interpretar el texto (`"PX"`, `"Px"`, …).
|
/// - La comparación de unidad es *case-insensitive* al interpretar el texto (`"PX"`, `"Px"`, ...).
|
||||||
/// - **Sobre píxeles**: Los píxeles (px) son relativos al dispositivo de visualización. En
|
/// - **Sobre píxeles**: Los píxeles (px) son relativos al dispositivo de visualización. En
|
||||||
/// dispositivos con baja densidad de píxeles (dpi), 1px equivale a un píxel (punto) del
|
/// dispositivos con baja densidad de píxeles (dpi), 1px equivale a un píxel (punto) del
|
||||||
/// dispositivo. En impresoras y pantallas de alta resolución, 1px implica múltiples píxeles del
|
/// dispositivo. En impresoras y pantallas de alta resolución, 1px implica múltiples píxeles del
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ impl Extension for HelloWorld {
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
.with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||||
.render()
|
.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ pub use error::ErrorPage;
|
||||||
pub use actix_web::Result as ResultPage;
|
pub use actix_web::Result as ResultPage;
|
||||||
|
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
use crate::core::component::{AssetsOp, Context, Contextual};
|
use crate::core::component::{AssetsOp, ChildOp, Context, Contextual};
|
||||||
use crate::core::component::{Child, ChildOp, Component};
|
|
||||||
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
|
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
|
||||||
use crate::html::{html, Markup, DOCTYPE};
|
use crate::html::{html, Markup, DOCTYPE};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
|
|
@ -160,22 +159,6 @@ impl Page {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un componente hijo a la región de contenido por defecto.
|
|
||||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
|
||||||
self.context.alter_child_in(
|
|
||||||
&DefaultRegion::Content,
|
|
||||||
ChildOp::Add(Child::with(component)),
|
|
||||||
);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un componente hijo en la región `region_name` de la página.
|
|
||||||
pub fn add_child_in(mut self, region_ref: RegionRef, component: impl Component) -> Self {
|
|
||||||
self.context
|
|
||||||
.alter_child_in(region_ref, ChildOp::Add(Child::with(component)));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Page GETTERS >***************************************************************************
|
// **< Page GETTERS >***************************************************************************
|
||||||
|
|
||||||
/// Devuelve el título traducido para el idioma de la página, si existe.
|
/// Devuelve el título traducido para el idioma de la página, si existe.
|
||||||
|
|
@ -340,8 +323,15 @@ impl Contextual for Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.context.alter_child_in(region_ref, op);
|
self.context
|
||||||
|
.alter_child_in(&DefaultRegion::Content, op.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_child_in(mut self, region_ref: RegionRef, op: impl Into<ChildOp>) -> Self {
|
||||||
|
self.context.alter_child_in(region_ref, op.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -377,7 +367,7 @@ impl Contextual for Page {
|
||||||
|
|
||||||
// **< Contextual HELPERS >*********************************************************************
|
// **< Contextual HELPERS >*********************************************************************
|
||||||
|
|
||||||
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
fn required_id<T>(&self, id: Option<String>) -> String {
|
||||||
self.context.required_id::<T>(id)
|
self.context.required_id::<T>(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
348
tests/component_children.rs
Normal file
348
tests/component_children.rs
Normal file
|
|
@ -0,0 +1,348 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
// **< TestComp — componente mínimo para los tests >************************************************
|
||||||
|
//
|
||||||
|
// Componente con id configurable y texto fijo de salida. El id permite probar las operaciones de
|
||||||
|
// `Children` basadas en identificador (`InsertAfterId`, `RemoveById`, etc.).
|
||||||
|
|
||||||
|
#[derive(AutoDefault, Clone)]
|
||||||
|
struct TestComp {
|
||||||
|
id: AttrId,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for TestComp {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
Ok(html! { (self.text) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestComp {
|
||||||
|
/// Crea un componente con id y texto de salida fijos.
|
||||||
|
fn tagged(id: &str, text: &str) -> Self {
|
||||||
|
let mut c = Self::default();
|
||||||
|
c.id.alter_id(id);
|
||||||
|
c.text = text.to_string();
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un componente sin id, con texto de salida fijo.
|
||||||
|
fn text(text: &str) -> Self {
|
||||||
|
let mut c = Self::default();
|
||||||
|
c.text = text.to_string();
|
||||||
|
c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Child >***************************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn child_default_is_empty() {
|
||||||
|
let child = Child::default();
|
||||||
|
assert!(child.id().is_none());
|
||||||
|
assert!(child.render(&mut Context::default()).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn child_with_stores_component_and_renders_it() {
|
||||||
|
let child = Child::with(TestComp::text("hola"));
|
||||||
|
assert_eq!(child.render(&mut Context::default()).into_string(), "hola");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn child_id_returns_component_id() {
|
||||||
|
let child = Child::with(TestComp::tagged("my-id", "texto"));
|
||||||
|
assert_eq!(child.id(), Some("my-id".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn child_from_component_is_equivalent_to_with() {
|
||||||
|
let child: Child = TestComp::text("desde from").into();
|
||||||
|
assert_eq!(
|
||||||
|
child.render(&mut Context::default()).into_string(),
|
||||||
|
"desde from"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn child_clone_is_deep() {
|
||||||
|
// Modificar el clon no debe afectar al original.
|
||||||
|
let original = Child::with(TestComp::text("original"));
|
||||||
|
let clone = original.clone();
|
||||||
|
assert_eq!(
|
||||||
|
original.render(&mut Context::default()).into_string(),
|
||||||
|
"original"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
clone.render(&mut Context::default()).into_string(),
|
||||||
|
"original"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Children + ChildOp >*************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_new_is_empty() {
|
||||||
|
let c = Children::new();
|
||||||
|
assert!(c.is_empty());
|
||||||
|
assert_eq!(c.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_add_appends_in_order() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::text("a"))
|
||||||
|
.with_child(TestComp::text("b"))
|
||||||
|
.with_child(TestComp::text("c"));
|
||||||
|
assert_eq!(c.len(), 3);
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_add_if_empty_only_adds_when_list_is_empty() {
|
||||||
|
let mut cx = Context::default();
|
||||||
|
|
||||||
|
// Se añade porque la lista está vacía.
|
||||||
|
let c = Children::new().with_child(ChildOp::AddIfEmpty(TestComp::text("primero").into()));
|
||||||
|
assert_eq!(c.len(), 1);
|
||||||
|
|
||||||
|
// No se añade porque ya hay un elemento.
|
||||||
|
let c = c.with_child(ChildOp::AddIfEmpty(TestComp::text("segundo").into()));
|
||||||
|
assert_eq!(c.len(), 1);
|
||||||
|
assert_eq!(c.render(&mut cx).into_string(), "primero");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_add_many_appends_all_in_order() {
|
||||||
|
let c = Children::new().with_child(ChildOp::AddMany(vec![
|
||||||
|
TestComp::text("x").into(),
|
||||||
|
TestComp::text("y").into(),
|
||||||
|
TestComp::text("z").into(),
|
||||||
|
]));
|
||||||
|
assert_eq!(c.len(), 3);
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "xyz");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_prepend_inserts_at_start() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::text("b"))
|
||||||
|
.with_child(ChildOp::Prepend(TestComp::text("a").into()));
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "ab");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_prepend_many_inserts_all_at_start_maintaining_order() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::text("c"))
|
||||||
|
.with_child(ChildOp::PrependMany(vec![
|
||||||
|
TestComp::text("a").into(),
|
||||||
|
TestComp::text("b").into(),
|
||||||
|
]));
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_insert_after_id_inserts_after_matching_element() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::tagged("first", "a"))
|
||||||
|
.with_child(TestComp::text("c"))
|
||||||
|
.with_child(ChildOp::InsertAfterId("first", TestComp::text("b").into()));
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_insert_after_id_appends_when_id_not_found() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::text("a"))
|
||||||
|
.with_child(ChildOp::InsertAfterId(
|
||||||
|
"no-existe",
|
||||||
|
TestComp::text("b").into(),
|
||||||
|
));
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "ab");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_insert_before_id_inserts_before_matching_element() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::text("a"))
|
||||||
|
.with_child(TestComp::tagged("last", "c"))
|
||||||
|
.with_child(ChildOp::InsertBeforeId("last", TestComp::text("b").into()));
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_insert_before_id_prepends_when_id_not_found() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::text("b"))
|
||||||
|
.with_child(ChildOp::InsertBeforeId(
|
||||||
|
"no-existe",
|
||||||
|
TestComp::text("a").into(),
|
||||||
|
));
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "ab");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_remove_by_id_removes_first_matching_element() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::tagged("keep", "a"))
|
||||||
|
.with_child(TestComp::tagged("drop", "b"))
|
||||||
|
.with_child(TestComp::text("c"))
|
||||||
|
.with_child(ChildOp::RemoveById("drop"));
|
||||||
|
assert_eq!(c.len(), 2);
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "ac");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_remove_by_id_does_nothing_when_id_not_found() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::text("a"))
|
||||||
|
.with_child(ChildOp::RemoveById("no-existe"));
|
||||||
|
assert_eq!(c.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_replace_by_id_replaces_first_matching_element() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::tagged("target", "viejo"))
|
||||||
|
.with_child(TestComp::text("b"))
|
||||||
|
.with_child(ChildOp::ReplaceById(
|
||||||
|
"target",
|
||||||
|
TestComp::text("nuevo").into(),
|
||||||
|
));
|
||||||
|
assert_eq!(c.len(), 2);
|
||||||
|
assert_eq!(c.render(&mut Context::default()).into_string(), "nuevob");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_reset_clears_all_elements() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::text("a"))
|
||||||
|
.with_child(TestComp::text("b"))
|
||||||
|
.with_child(ChildOp::Reset);
|
||||||
|
assert!(c.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_get_by_id_returns_first_matching_child() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::tagged("uno", "a"))
|
||||||
|
.with_child(TestComp::tagged("dos", "b"));
|
||||||
|
assert!(c.get_by_id("uno").is_some());
|
||||||
|
assert!(c.get_by_id("dos").is_some());
|
||||||
|
assert!(c.get_by_id("tres").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_iter_by_id_yields_all_matching_children() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::tagged("rep", "a"))
|
||||||
|
.with_child(TestComp::tagged("rep", "b"))
|
||||||
|
.with_child(TestComp::tagged("otro", "c"));
|
||||||
|
assert_eq!(c.iter_by_id("rep").count(), 2);
|
||||||
|
assert_eq!(c.iter_by_id("otro").count(), 1);
|
||||||
|
assert_eq!(c.iter_by_id("ninguno").count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn children_render_concatenates_all_outputs_in_order() {
|
||||||
|
let c = Children::new()
|
||||||
|
.with_child(TestComp::text("uno "))
|
||||||
|
.with_child(TestComp::text("dos "))
|
||||||
|
.with_child(TestComp::text("tres"));
|
||||||
|
assert_eq!(
|
||||||
|
c.render(&mut Context::default()).into_string(),
|
||||||
|
"uno dos tres"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Embed >**************************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn embed_default_is_empty() {
|
||||||
|
let embed: Embed<TestComp> = Embed::default();
|
||||||
|
assert!(embed.id().is_none());
|
||||||
|
assert!(embed.render(&mut Context::default()).is_empty());
|
||||||
|
assert!(embed.get().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn embed_with_stores_component() {
|
||||||
|
let embed = Embed::with(TestComp::text("contenido"));
|
||||||
|
assert!(embed.get().is_some());
|
||||||
|
assert_eq!(
|
||||||
|
embed.render(&mut Context::default()).into_string(),
|
||||||
|
"contenido"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn embed_id_returns_component_id() {
|
||||||
|
let embed = Embed::with(TestComp::tagged("embed-id", "texto"));
|
||||||
|
assert_eq!(embed.id(), Some("embed-id".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn embed_get_is_some_when_component_present() {
|
||||||
|
let embed = Embed::with(TestComp::tagged("abc", "hola"));
|
||||||
|
// `get()` devuelve Some; la lectura del id verifica que accede al componente correctamente.
|
||||||
|
assert!(embed.get().is_some());
|
||||||
|
assert_eq!(embed.id(), Some("abc".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn embed_get_allows_mutating_component() {
|
||||||
|
let embed = Embed::with(TestComp::tagged("orig", "texto"));
|
||||||
|
// El `;` final convierte el `if let` en sentencia y libera el guard antes que `embed`.
|
||||||
|
if let Some(mut comp) = embed.get() {
|
||||||
|
comp.id.alter_id("modificado");
|
||||||
|
};
|
||||||
|
assert_eq!(embed.id(), Some("modificado".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn embed_with_component_replaces_content() {
|
||||||
|
let embed =
|
||||||
|
Embed::with(TestComp::text("primero")).with_component(Some(TestComp::text("segundo")));
|
||||||
|
assert_eq!(
|
||||||
|
embed.render(&mut Context::default()).into_string(),
|
||||||
|
"segundo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn embed_with_component_none_empties_embed() {
|
||||||
|
let embed = Embed::with(TestComp::text("algo")).with_component(None);
|
||||||
|
assert!(embed.get().is_none());
|
||||||
|
assert!(embed.render(&mut Context::default()).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn embed_clone_is_deep() {
|
||||||
|
let original = Embed::with(TestComp::tagged("orig", "texto"));
|
||||||
|
let clone = original.clone();
|
||||||
|
// Mutar el clon no debe afectar al original.
|
||||||
|
if let Some(mut comp) = clone.get() {
|
||||||
|
comp.id.alter_id("clone-id");
|
||||||
|
}
|
||||||
|
assert_eq!(original.id(), Some("orig".to_string()));
|
||||||
|
assert_eq!(clone.id(), Some("clone-id".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn embed_converts_into_child() {
|
||||||
|
let embed = Embed::with(TestComp::text("desde embed"));
|
||||||
|
let child = Child::from(embed);
|
||||||
|
assert_eq!(
|
||||||
|
child.render(&mut Context::default()).into_string(),
|
||||||
|
"desde embed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ use pagetop::prelude::*;
|
||||||
/// Componente mínimo para probar `Markup` pasando por el ciclo real de renderizado de componentes
|
/// Componente mínimo para probar `Markup` pasando por el ciclo real de renderizado de componentes
|
||||||
/// (`ComponentRender`). El parámetro de contexto `"renderable"` se usará para controlar si el
|
/// (`ComponentRender`). El parámetro de contexto `"renderable"` se usará para controlar si el
|
||||||
/// componente se renderiza (`true` por defecto).
|
/// componente se renderiza (`true` por defecto).
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault, Clone)]
|
||||||
struct TestMarkupComponent {
|
struct TestMarkupComponent {
|
||||||
markup: Markup,
|
markup: Markup,
|
||||||
}
|
}
|
||||||
|
|
@ -13,11 +13,11 @@ impl Component for TestMarkupComponent {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_renderable(&self, cx: &mut Context) -> bool {
|
fn is_renderable(&self, cx: &Context) -> bool {
|
||||||
cx.param_or::<bool>("renderable", true)
|
cx.param_or::<bool>("renderable", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
Ok(self.markup.clone())
|
Ok(self.markup.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ if [[ "$EXECUTE" != "--execute" ]]; then
|
||||||
echo "Running dry-run (default mode). Add --execute to publish"
|
echo "Running dry-run (default mode). Add --execute to publish"
|
||||||
cargo release --config "$CONFIG" --package "$CRATE" "$LEVEL"
|
cargo release --config "$CONFIG" --package "$CRATE" "$LEVEL"
|
||||||
else
|
else
|
||||||
echo "Releasing $CRATE ($LEVEL)…"
|
echo "Releasing $CRATE ($LEVEL)..."
|
||||||
cargo release --config "$CONFIG" --package "$CRATE" "$LEVEL" --execute
|
cargo release --config "$CONFIG" --package "$CRATE" "$LEVEL" --execute
|
||||||
echo "Release completed."
|
echo "Release completed."
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue