✨ Añade ComponentError con HTML alternativo
`prepare_component()` ahora devuelve `Result<Markup, ComponentError>` en lugar de `Markup`, para que los componentes señalen fallos durante el renderizado de forma explícita. `ComponentError` encapsula un mensaje de error y un marcado HTML alternativo opcional (`fallback`). Si se produce un error, el ciclo de renderizado registra la traza y muestra el `fallback` en lugar del componente fallido, sin interrumpir el resto de la página. Lo mismo aplica a los errores devueltos por la acción `PrepareRender` de los temas, que siguen el mismo mecanismo.
This commit is contained in:
parent
a0b14aec36
commit
34aeeab2d7
26 changed files with 232 additions and 100 deletions
|
|
@ -33,10 +33,10 @@ impl Component for Container {
|
||||||
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) -> Markup {
|
fn prepare_component(&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 html! {};
|
return Ok(html! {});
|
||||||
}
|
}
|
||||||
let style = match self.container_width() {
|
let style = match self.container_width() {
|
||||||
container::Width::FluidMax(w) if w.is_measurable() => {
|
container::Width::FluidMax(w) if w.is_measurable() => {
|
||||||
|
|
@ -44,7 +44,7 @@ impl Component for Container {
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
match self.container_kind() {
|
Ok(match self.container_kind() {
|
||||||
container::Kind::Default => html! {
|
container::Kind::Default => html! {
|
||||||
div id=[self.id()] class=[self.classes().get()] style=[style] {
|
div id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||||
(output)
|
(output)
|
||||||
|
|
@ -75,7 +75,7 @@ impl Component for Container {
|
||||||
(output)
|
(output)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,17 +63,17 @@ impl Component for Dropdown {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&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() {
|
||||||
return html! {};
|
return Ok(html! {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Título opcional para el menú desplegable.
|
// Título opcional para el menú desplegable.
|
||||||
let title = self.title().using(cx);
|
let title = self.title().using(cx);
|
||||||
|
|
||||||
html! {
|
Ok(html! {
|
||||||
div id=[self.id()] class=[self.classes().get()] {
|
div id=[self.id()] class=[self.classes().get()] {
|
||||||
@if !title.is_empty() {
|
@if !title.is_empty() {
|
||||||
@let mut btn_classes = Classes::new({
|
@let mut btn_classes = Classes::new({
|
||||||
|
|
@ -156,7 +156,7 @@ impl Component for Dropdown {
|
||||||
ul class="dropdown-menu" { (items) }
|
ul class="dropdown-menu" { (items) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ impl Component for Item {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
match self.item_kind() {
|
Ok(match self.item_kind() {
|
||||||
ItemKind::Void => html! {},
|
ItemKind::Void => html! {},
|
||||||
|
|
||||||
ItemKind::Label(label) => html! {
|
ItemKind::Label(label) => html! {
|
||||||
|
|
@ -151,7 +151,7 @@ impl Component for Item {
|
||||||
ItemKind::Divider => html! {
|
ItemKind::Divider => html! {
|
||||||
li id=[self.id()] class=[self.classes().get()] { hr class="dropdown-divider" {} }
|
li id=[self.id()] class=[self.classes().get()] { hr class="dropdown-divider" {} }
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,12 @@ impl Component for Form {
|
||||||
self.alter_classes(ClassesOp::Prepend, "form");
|
self.alter_classes(ClassesOp::Prepend, "form");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&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,
|
||||||
};
|
};
|
||||||
html! {
|
Ok(html! {
|
||||||
form
|
form
|
||||||
id=[self.id()]
|
id=[self.id()]
|
||||||
class=[self.classes().get()]
|
class=[self.classes().get()]
|
||||||
|
|
@ -67,7 +67,7 @@ impl Component for Form {
|
||||||
{
|
{
|
||||||
(self.children().render(cx))
|
(self.children().render(cx))
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,15 @@ impl Component for Fieldset {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
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) {
|
||||||
legend { (legend) }
|
legend { (legend) }
|
||||||
}
|
}
|
||||||
(self.children().render(cx))
|
(self.children().render(cx))
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ impl Component for Input {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&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));
|
||||||
html! {
|
Ok(html! {
|
||||||
div class=[self.classes().get()] {
|
div class=[self.classes().get()] {
|
||||||
@if let Some(label) = self.label().lookup(cx) {
|
@if let Some(label) = self.label().lookup(cx) {
|
||||||
label for=[&id] class="form-label" {
|
label for=[&id] class="form-label" {
|
||||||
|
|
@ -72,7 +72,7 @@ impl Component for Input {
|
||||||
div class="form-text" { (description) }
|
div class="form-text" { (description) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ impl Component for Icon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
match self.icon_kind() {
|
Ok(match self.icon_kind() {
|
||||||
IconKind::None => html! {},
|
IconKind::None => html! {},
|
||||||
IconKind::Font(_) => {
|
IconKind::Font(_) => {
|
||||||
let aria_label = self.aria_label().lookup(cx);
|
let aria_label = self.aria_label().lookup(cx);
|
||||||
|
|
@ -69,7 +69,7 @@ impl Component for Icon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,13 @@ impl Component for Image {
|
||||||
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) -> Markup {
|
fn prepare_component(&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();
|
||||||
let source = match self.source() {
|
let source = match self.source() {
|
||||||
image::Source::Logo(logo) => {
|
image::Source::Logo(logo) => {
|
||||||
return html! {
|
return Ok(html! {
|
||||||
span
|
span
|
||||||
id=[self.id()]
|
id=[self.id()]
|
||||||
class=[self.classes().get()]
|
class=[self.classes().get()]
|
||||||
|
|
@ -53,20 +53,20 @@ impl Component for Image {
|
||||||
{
|
{
|
||||||
(logo.render(cx))
|
(logo.render(cx))
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
image::Source::Responsive(source) => Some(source),
|
image::Source::Responsive(source) => Some(source),
|
||||||
image::Source::Thumbnail(source) => Some(source),
|
image::Source::Thumbnail(source) => Some(source),
|
||||||
image::Source::Plain(source) => Some(source),
|
image::Source::Plain(source) => Some(source),
|
||||||
};
|
};
|
||||||
html! {
|
Ok(html! {
|
||||||
img
|
img
|
||||||
src=[source]
|
src=[source]
|
||||||
alt=(alt_text)
|
alt=(alt_text)
|
||||||
id=[self.id()]
|
id=[self.id()]
|
||||||
class=[self.classes().get()]
|
class=[self.classes().get()]
|
||||||
style=[dimensions] {}
|
style=[dimensions] {}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,17 +42,17 @@ impl Component for Nav {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&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 html! {};
|
return Ok(html! {});
|
||||||
}
|
}
|
||||||
|
|
||||||
html! {
|
Ok(html! {
|
||||||
ul id=[self.id()] class=[self.classes().get()] {
|
ul id=[self.id()] class=[self.classes().get()] {
|
||||||
(items)
|
(items)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,8 @@ impl Component for Item {
|
||||||
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) -> Markup {
|
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
match self.item_kind() {
|
Ok(match self.item_kind() {
|
||||||
ItemKind::Void => html! {},
|
ItemKind::Void => html! {},
|
||||||
|
|
||||||
ItemKind::Label(label) => html! {
|
ItemKind::Label(label) => html! {
|
||||||
|
|
@ -162,7 +162,7 @@ impl Component for Item {
|
||||||
if let Some(dd) = menu.borrow() {
|
if let Some(dd) = menu.borrow() {
|
||||||
let items = dd.items().render(cx);
|
let items = dd.items().render(cx);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return html! {};
|
return Ok(html! {});
|
||||||
}
|
}
|
||||||
let title = dd.title().lookup(cx).unwrap_or_else(|| {
|
let title = dd.title().lookup(cx).unwrap_or_else(|| {
|
||||||
L10n::t("dropdown", &LOCALES_BOOTSIER)
|
L10n::t("dropdown", &LOCALES_BOOTSIER)
|
||||||
|
|
@ -189,7 +189,7 @@ impl Component for Item {
|
||||||
html! {}
|
html! {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,20 +36,20 @@ impl Component for Brand {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&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() {
|
||||||
return html! {};
|
return Ok(html! {});
|
||||||
}
|
}
|
||||||
let slogan = self.slogan().using(cx);
|
let slogan = self.slogan().using(cx);
|
||||||
html! {
|
Ok(html! {
|
||||||
@if let Some(route) = self.route() {
|
@if let Some(route) = self.route() {
|
||||||
a class="navbar-brand" href=(route(cx)) { (image) (title) (slogan) }
|
a class="navbar-brand" href=(route(cx)) { (image) (title) (slogan) }
|
||||||
} @else {
|
} @else {
|
||||||
span class="navbar-brand" { (image) (title) (slogan) }
|
span class="navbar-brand" { (image) (title) (slogan) }
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ impl Component for Navbar {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&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);
|
||||||
|
|
@ -75,13 +75,13 @@ impl Component for Navbar {
|
||||||
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
||||||
let items = self.items().render(cx);
|
let items = self.items().render(cx);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return html! {};
|
return Ok(html! {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas.
|
// Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas.
|
||||||
let id = cx.required_id::<Self>(self.id());
|
let id = cx.required_id::<Self>(self.id());
|
||||||
|
|
||||||
html! {
|
Ok(html! {
|
||||||
nav id=(id) class=[self.classes().get()] {
|
nav id=(id) class=[self.classes().get()] {
|
||||||
div class="container-fluid" {
|
div class="container-fluid" {
|
||||||
@match self.layout() {
|
@match self.layout() {
|
||||||
|
|
@ -162,7 +162,7 @@ impl Component for Navbar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,15 +46,15 @@ impl Component for Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
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.borrow() {
|
||||||
let items = nav.items().render(cx);
|
let items = nav.items().render(cx);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return html! {};
|
return Ok(html! {});
|
||||||
}
|
}
|
||||||
html! {
|
html! {
|
||||||
ul id=[nav.id()] class=[nav.classes().get()] {
|
ul id=[nav.id()] class=[nav.classes().get()] {
|
||||||
|
|
@ -70,7 +70,7 @@ impl Component for Item {
|
||||||
(text.using(cx))
|
(text.using(cx))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ impl Component for Offcanvas {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
self.render_offcanvas(cx, None)
|
Ok(self.render_offcanvas(cx, None))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::prelude::*;
|
||||||
/// los componentes.
|
/// los componentes.
|
||||||
///
|
///
|
||||||
/// Recibe una referencia al componente `component` y una referencia mutable al contexto `cx`.
|
/// Recibe una referencia al componente `component` y una referencia mutable al contexto `cx`.
|
||||||
pub type FnPrepareRender<C> = fn(component: &C, cx: &mut Context) -> Markup;
|
pub type FnPrepareRender<C> = fn(component: &C, cx: &mut Context) -> Result<Markup, ComponentError>;
|
||||||
|
|
||||||
/// Ejecuta [`FnPrepareRender`] para preparar el renderizado de un componente.
|
/// Ejecuta [`FnPrepareRender`] para preparar el renderizado de un componente.
|
||||||
///
|
///
|
||||||
|
|
@ -41,23 +41,25 @@ impl<C: Component> PrepareRender<C> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Despacha las acciones. Se detiene en cuanto una renderiza.
|
/// Despacha las acciones. Se detiene en cuanto una renderiza o produce un error.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn dispatch(component: &C, cx: &mut Context) -> Markup {
|
pub(crate) fn dispatch(component: &C, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let mut render_component = html! {};
|
let mut render_result: Result<Markup, ComponentError> = Ok(html! {});
|
||||||
dispatch_actions(
|
dispatch_actions_until(
|
||||||
&ActionKey::new(
|
&ActionKey::new(
|
||||||
UniqueId::of::<Self>(),
|
UniqueId::of::<Self>(),
|
||||||
Some(cx.theme().type_id()),
|
Some(cx.theme().type_id()),
|
||||||
Some(UniqueId::of::<C>()),
|
Some(UniqueId::of::<C>()),
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
|action: &Self| {
|
|action: &Self| match &render_result {
|
||||||
if render_component.is_empty() {
|
Ok(markup) if markup.is_empty() => {
|
||||||
render_component = (action.f)(component, cx);
|
render_result = (action.f)(component, cx);
|
||||||
|
std::ops::ControlFlow::Continue(())
|
||||||
}
|
}
|
||||||
|
_ => std::ops::ControlFlow::Break(()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
render_component
|
render_result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,23 +29,23 @@ impl Component for Block {
|
||||||
self.alter_classes(ClassesOp::Prepend, "block");
|
self.alter_classes(ClassesOp::Prepend, "block");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&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() {
|
||||||
return html! {};
|
return Ok(html! {});
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = cx.required_id::<Block>(self.id());
|
let id = cx.required_id::<Block>(self.id());
|
||||||
|
|
||||||
html! {
|
Ok(html! {
|
||||||
div id=(id) class=[self.classes().get()] {
|
div id=(id) class=[self.classes().get()] {
|
||||||
@if let Some(title) = self.title().lookup(cx) {
|
@if let Some(title) = self.title().lookup(cx) {
|
||||||
h2 class="block__title" { span { (title) } }
|
h2 class="block__title" { span { (title) } }
|
||||||
}
|
}
|
||||||
div class="block__body" { (block_body) }
|
div class="block__body" { (block_body) }
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ impl Component for Html {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
self.html(cx)
|
Ok(self.html(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ impl Component for Intro {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
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#"
|
||||||
|
|
@ -135,7 +135,7 @@ impl Component for Intro {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
html! {
|
Ok(html! {
|
||||||
div class="intro" {
|
div class="intro" {
|
||||||
div class="intro-header" {
|
div class="intro-header" {
|
||||||
section class="intro-header__body" {
|
section class="intro-header__body" {
|
||||||
|
|
@ -206,7 +206,7 @@ impl Component for Intro {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ impl Component for PoweredBy {
|
||||||
PoweredBy { copyright: Some(c) }
|
PoweredBy { copyright: Some(c) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> Markup {
|
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
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() {
|
||||||
span class="poweredby__copyright" { (c) "." } " "
|
span class="poweredby__copyright" { (c) "." } " "
|
||||||
|
|
@ -35,7 +35,7 @@ impl Component for PoweredBy {
|
||||||
(L10n::l("poweredby_pagetop").with_arg("pagetop_link", LINK).using(cx))
|
(L10n::l("poweredby_pagetop").with_arg("pagetop_link", LINK).using(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use list::ActionsList;
|
||||||
|
|
||||||
mod all;
|
mod all;
|
||||||
pub(crate) use all::add_action;
|
pub(crate) use all::add_action;
|
||||||
pub use all::dispatch_actions;
|
pub use all::{dispatch_actions, dispatch_actions_until};
|
||||||
|
|
||||||
/// Facilita la implementación del método [`actions()`](crate::core::extension::Extension::actions).
|
/// Facilita la implementación del método [`actions()`](crate::core::extension::Extension::actions).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -72,3 +72,18 @@ where
|
||||||
list.iter_map(f);
|
list.iter_map(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Despacha las funciones asociadas a una [`ActionKey`] con posible salida anticipada.
|
||||||
|
///
|
||||||
|
/// Funciona igual que [`dispatch_actions`], pero el *closure* puede devolver
|
||||||
|
/// [`std::ops::ControlFlow::Continue`] para continuar ejecutando la siguiente acción; o
|
||||||
|
/// [`std::ops::ControlFlow::Break`] para detener la iteración inmediatamente.
|
||||||
|
pub fn dispatch_actions_until<A, F>(key: &ActionKey, f: F)
|
||||||
|
where
|
||||||
|
A: ActionDispatcher,
|
||||||
|
F: FnMut(&A) -> std::ops::ControlFlow<()>,
|
||||||
|
{
|
||||||
|
if let Some(list) = ACTIONS.read().get(key) {
|
||||||
|
list.iter_try_map(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,21 @@ impl ActionsList {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn iter_try_map<A, F>(&self, mut f: F)
|
||||||
|
where
|
||||||
|
A: ActionDispatcher,
|
||||||
|
F: FnMut(&A) -> std::ops::ControlFlow<()>,
|
||||||
|
{
|
||||||
|
let list = self.0.read();
|
||||||
|
for a in list.iter().rev() {
|
||||||
|
if let Some(action) = (**a).downcast_ref::<A>() {
|
||||||
|
if f(action).is_break() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trace::error!("Failed to downcast action of type {}", (**a).type_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
use crate::html::RoutePath;
|
use crate::html::RoutePath;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
pub use error::ComponentError;
|
||||||
|
|
||||||
mod definition;
|
mod definition;
|
||||||
pub use definition::{Component, ComponentRender};
|
pub use definition::{Component, ComponentRender};
|
||||||
|
|
||||||
|
|
@ -38,8 +41,8 @@ pub use context::{AssetsOp, Context, ContextError, Contextual};
|
||||||
/// self.renderable.map_or(true, |f| f(cx))
|
/// self.renderable.map_or(true, |f| f(cx))
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn prepare_component(&self, _cx: &mut Context) -> Markup {
|
/// fn prepare_component(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
/// html! { "Visible component" }
|
/// Ok(html! { "Visible component" })
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
use crate::core::component::Context;
|
use crate::core::component::{ComponentError, Context, Contextual};
|
||||||
use crate::core::{AnyInfo, TypeInfo};
|
use crate::core::{AnyInfo, TypeInfo};
|
||||||
use crate::html::{html, Markup};
|
use crate::html::{html, Markup};
|
||||||
|
|
||||||
|
|
@ -14,11 +14,15 @@ pub trait ComponentRender {
|
||||||
|
|
||||||
/// Interfaz común que debe implementar un componente renderizable en PageTop.
|
/// Interfaz común que debe implementar un componente renderizable en PageTop.
|
||||||
///
|
///
|
||||||
/// Se recomienda que los componentes deriven [`AutoDefault`](crate::AutoDefault). También deben
|
/// Se recomienda que los componentes declaren sus campos como privados, que deriven
|
||||||
/// implementar explícitamente el método [`new()`](Self::new) y pueden sobrescribir los otros
|
/// [`AutoDefault`](crate::AutoDefault) o implementen [`Default`] para inicializarlos por defecto, y
|
||||||
/// métodos para personalizar su comportamiento.
|
/// [`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
|
||||||
|
/// comportamiento.
|
||||||
pub trait Component: AnyInfo + ComponentRender + Send + Sync {
|
pub trait Component: AnyInfo + ComponentRender + Send + Sync {
|
||||||
/// Crea una nueva instancia del componente.
|
/// Crea una nueva instancia del componente.
|
||||||
|
///
|
||||||
|
/// Por convención suele devolver `Self::default()`.
|
||||||
fn new() -> Self
|
fn new() -> Self
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
|
@ -51,9 +55,9 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
|
||||||
/// 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.
|
||||||
///
|
///
|
||||||
/// También puede usarse junto con un alias de función como
|
/// También puede asignarse una función [`FnIsRenderable`](super::FnIsRenderable) a un campo del
|
||||||
/// ([`FnIsRenderable`](crate::core::component::FnIsRenderable)) para permitir que instancias
|
/// componente para permitir que instancias concretas del mismo puedan decidir dinámicamente si
|
||||||
/// concretas del componente decidan 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: &mut Context) -> bool {
|
||||||
true
|
true
|
||||||
|
|
@ -62,25 +66,28 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
|
||||||
/// Configura el componente justo antes de preparar el renderizado.
|
/// Configura el componente justo antes de preparar el renderizado.
|
||||||
///
|
///
|
||||||
/// Este método puede sobrescribirse para modificar la estructura interna del componente o el
|
/// Este método puede sobrescribirse para modificar la estructura interna del componente o el
|
||||||
/// contexto antes de preparar la renderización del componente. Por defecto no hace nada.
|
/// contexto antes de renderizarlo. Por defecto no hace nada.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn setup_before_prepare(&mut self, cx: &mut Context) {}
|
fn setup_before_prepare(&mut self, cx: &mut Context) {}
|
||||||
|
|
||||||
/// Devuelve una representación renderizada del componente.
|
/// Devuelve el marcado HTML del componente usando el contexto proporcionado.
|
||||||
///
|
///
|
||||||
/// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente
|
/// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente
|
||||||
/// durante el proceso de construcción del documento. Puede sobrescribirse para generar
|
/// durante el proceso de construcción del documento. Cada componente lo implementa para generar
|
||||||
/// dinámicamente el contenido HTML con acceso al contexto de renderizado.
|
/// su propio contenido HTML. Los temas hijo pueden sobrescribir opcionalmente su resultado
|
||||||
|
/// mediante la acción [`PrepareRender`](crate::base::action::theme::PrepareRender).
|
||||||
///
|
///
|
||||||
/// Este método debe ser capaz de preparar el renderizado del componente con los métodos del
|
/// Se recomienda obtener los datos del componente a través de sus propios métodos para que los
|
||||||
/// propio componente y el contexto proporcionado, no debería hacerlo accediendo directamente a
|
/// temas hijo que implementen dicha acción puedan generar el nuevo HTML sin depender de los
|
||||||
/// los campos de la estructura del componente. Es una forma de garantizar que los programadores
|
/// detalles internos del componente.
|
||||||
/// podrán sobrescribir este método sin preocuparse por los detalles internos del componente.
|
|
||||||
///
|
///
|
||||||
/// Por defecto, devuelve un [`Markup`] vacío (`html! {}`).
|
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`).
|
||||||
|
///
|
||||||
|
/// 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) -> Markup {
|
fn prepare_component(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
html! {}
|
Ok(html! {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,11 +130,33 @@ impl<C: Component> ComponentRender for C {
|
||||||
action::component::BeforeRender::dispatch(self, cx);
|
action::component::BeforeRender::dispatch(self, cx);
|
||||||
|
|
||||||
// Prepara el renderizado del componente.
|
// Prepara el renderizado del componente.
|
||||||
let prepare = action::theme::PrepareRender::dispatch(self, cx);
|
let prepare = match action::theme::PrepareRender::dispatch(self, cx) {
|
||||||
let prepare = if prepare.is_empty() {
|
Ok(markup) if !markup.is_empty() => markup,
|
||||||
self.prepare_component(cx)
|
Ok(_) => match self.prepare_component(cx) {
|
||||||
} else {
|
Ok(markup) => markup,
|
||||||
prepare
|
Err(error) => {
|
||||||
|
crate::trace::error!(
|
||||||
|
path = cx.request().map(|r| r.path()).unwrap_or("<unknown>"),
|
||||||
|
component = self.name(),
|
||||||
|
id = self.id().as_deref().unwrap_or("<not set>"),
|
||||||
|
source = "prepare_component",
|
||||||
|
"render failed, using fallback: {}",
|
||||||
|
error.message()
|
||||||
|
);
|
||||||
|
error.into_fallback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
crate::trace::error!(
|
||||||
|
path = cx.request().map(|r| r.path()).unwrap_or("<unknown>"),
|
||||||
|
component = self.name(),
|
||||||
|
id = self.id().as_deref().unwrap_or("<not set>"),
|
||||||
|
source = "PrepareRender",
|
||||||
|
"render failed, using fallback: {}",
|
||||||
|
error.message()
|
||||||
|
);
|
||||||
|
error.into_fallback()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Acciones específicas del tema después de renderizar el componente.
|
// Acciones específicas del tema después de renderizar el componente.
|
||||||
|
|
|
||||||
66
src/core/component/error.rs
Normal file
66
src/core/component/error.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
use crate::html::{html, Markup};
|
||||||
|
use crate::{AutoDefault, Getters};
|
||||||
|
|
||||||
|
/// Error producido durante el renderizado de un componente.
|
||||||
|
///
|
||||||
|
/// Se usa en [`Component::prepare_component()`](super::Component::prepare_component) para devolver
|
||||||
|
/// un [`Err`]. Puede incluir un marcado HTML alternativo para renderizar el componente de manera
|
||||||
|
/// diferente en caso de error.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # struct MyComponent;
|
||||||
|
/// # impl Component for MyComponent {
|
||||||
|
/// # fn new() -> Self { MyComponent }
|
||||||
|
/// fn prepare_component(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
/// Err(ComponentError::new("Database connection failed")
|
||||||
|
/// .with_fallback(html! { p class="error" { "Content temporarily unavailable." } }))
|
||||||
|
/// }
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[derive(AutoDefault, Debug, Getters)]
|
||||||
|
pub struct ComponentError {
|
||||||
|
/// Mensaje descriptivo del error.
|
||||||
|
message: String,
|
||||||
|
/// Marcado HTML alternativo para mostrar si el componente falla.
|
||||||
|
fallback: Markup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentError {
|
||||||
|
/// Crea un nuevo error para un componente con un marcado alternativo vacío.
|
||||||
|
pub fn new(message: impl Into<String>) -> Self {
|
||||||
|
ComponentError {
|
||||||
|
message: message.into(),
|
||||||
|
fallback: html! {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< ComponentError BUILDER >*****************************************************************
|
||||||
|
|
||||||
|
/// Asigna el marcado HTML alternativo (*fallback*) que se mostrará si el componente falla.
|
||||||
|
///
|
||||||
|
/// Si no se proporciona, no se renderizará nada del componente.
|
||||||
|
pub fn with_fallback(mut self, fallback: Markup) -> Self {
|
||||||
|
self.fallback = fallback;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< ComponentError GETTERS >*****************************************************************
|
||||||
|
|
||||||
|
/// Consume el error y devuelve su marcado alternativo.
|
||||||
|
///
|
||||||
|
/// Se invoca internamente en [`ComponentRender`](crate::core::component::ComponentRender).
|
||||||
|
pub(crate) fn into_fallback(self) -> Markup {
|
||||||
|
self.fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ComponentError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ComponentError {}
|
||||||
|
|
@ -10,15 +10,15 @@ struct TestMarkupComponent {
|
||||||
|
|
||||||
impl Component for TestMarkupComponent {
|
impl Component for TestMarkupComponent {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
TestMarkupComponent::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_renderable(&self, cx: &mut Context) -> bool {
|
fn is_renderable(&self, cx: &mut Context) -> bool {
|
||||||
cx.param_or::<bool>("renderable", true)
|
cx.param_or::<bool>("renderable", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, _cx: &mut Context) -> Markup {
|
fn prepare_component(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
self.markup.clone()
|
Ok(self.markup.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue