diff --git a/assets/css/intro.css b/assets/css/intro.css index 00fe0d21..cc60b7b8 100644 --- a/assets/css/intro.css +++ b/assets/css/intro.css @@ -389,10 +389,10 @@ body { .intro-text-body .block { position: relative; } -.intro-text-body .block__title { +.intro-text-body .block-title { margin: 1em 0 .8em; } -.intro-text-body .block__title span { +.intro-text-body .block-title span { display: inline-block; padding: 10px 30px 14px; margin: 30px 20px 0; @@ -403,7 +403,7 @@ body { border-color: orangered; transform: rotate(-3deg) translateY(-25%); } -.intro-text-body .block__title:before { +.intro-text-body .block-title:before { content: ""; height: 5px; position: absolute; @@ -416,7 +416,7 @@ body { transform: rotate(2deg) translateY(-50%); transform-origin: top left; } -.intro-text-body .block__title:after { +.intro-text-body .block-title:after { content: ""; height: 120%; position: absolute; @@ -427,22 +427,22 @@ body { background: var(--intro-bg-block-1); transform: rotate(2deg); } -.intro-text-body .block:nth-of-type(6n+1) .block__title:after { +.intro-text-body .block:nth-of-type(6n+1) .block-title:after { background: var(--intro-bg-block-1); } -.intro-text-body .block:nth-of-type(6n+2) .block__title:after { +.intro-text-body .block:nth-of-type(6n+2) .block-title:after { background: var(--intro-bg-block-2); } -.intro-text-body .block:nth-of-type(6n+3) .block__title:after { +.intro-text-body .block:nth-of-type(6n+3) .block-title:after { background: var(--intro-bg-block-3); } -.intro-text-body .block:nth-of-type(6n+4) .block__title:after { +.intro-text-body .block:nth-of-type(6n+4) .block-title:after { background: var(--intro-bg-block-4); } -.intro-text-body .block:nth-of-type(6n+5) .block__title:after { +.intro-text-body .block:nth-of-type(6n+5) .block-title:after { background: var(--intro-bg-block-5); } -.intro-text-body .block:nth-of-type(6n+6) .block__title:after { +.intro-text-body .block:nth-of-type(6n+6) .block-title:after { background: var(--intro-bg-block-6); } diff --git a/extensions/pagetop-bootsier/assets/_bootsier-custom.scss b/extensions/pagetop-bootsier/assets/_bootsier-custom.scss index 1f8421ec..9a4c7e53 100644 --- a/extensions/pagetop-bootsier/assets/_bootsier-custom.scss +++ b/extensions/pagetop-bootsier/assets/_bootsier-custom.scss @@ -18,6 +18,11 @@ font-display: swap; } +// Font-relative top offset to keep the skip-link hidden at any font size. +.skip-link { + top: -3em; +} + // Required field indicator in forms. .form-required { color: var(--bs-danger); diff --git a/extensions/pagetop-bootsier/src/theme/button.rs b/extensions/pagetop-bootsier/src/theme/button.rs index fbb8617f..17fb7cf6 100644 --- a/extensions/pagetop-bootsier/src/theme/button.rs +++ b/extensions/pagetop-bootsier/src/theme/button.rs @@ -45,9 +45,7 @@ use crate::theme::{ButtonAction, ButtonColor, ButtonSize}; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Button { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del botón. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el comportamiento del botón al activarse. kind: ButtonAction, @@ -73,7 +71,7 @@ impl Component for Button { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { @@ -86,7 +84,6 @@ impl Component for Button { fn prepare(&self, cx: &mut Context) -> Result { Ok(html! { button - id=[self.id()] type=(self.kind()) (self.props()) name=[self.name().get()] @@ -140,14 +137,14 @@ impl Button { // **< Button BUILDER >************************************************************************* - /// Establece el identificador único (`id`) del botón. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del botón. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/container/component.rs b/extensions/pagetop-bootsier/src/theme/container/component.rs index bac350d0..07a835a7 100644 --- a/extensions/pagetop-bootsier/src/theme/container/component.rs +++ b/extensions/pagetop-bootsier/src/theme/container/component.rs @@ -20,9 +20,7 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Container { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del contenedor. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el tipo semántico del contenedor. container_kind: container::Kind, @@ -38,7 +36,7 @@ impl Component for Container { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { @@ -58,32 +56,32 @@ impl Component for Container { }; Ok(match self.container_kind() { container::Kind::Default => html! { - div id=[self.id()] (self.props()) style=[style] { + div (self.props()) style=[style] { (output) } }, container::Kind::Main => html! { - main id=[self.id()] (self.props()) style=[style] { + main (self.props()) style=[style] { (output) } }, container::Kind::Header => html! { - header id=[self.id()] (self.props()) style=[style] { + header (self.props()) style=[style] { (output) } }, container::Kind::Footer => html! { - footer id=[self.id()] (self.props()) style=[style] { + footer (self.props()) style=[style] { (output) } }, container::Kind::Section => html! { - section id=[self.id()] (self.props()) style=[style] { + section (self.props()) style=[style] { (output) } }, container::Kind::Article => html! { - article id=[self.id()] (self.props()) style=[style] { + article (self.props()) style=[style] { (output) } }, @@ -134,14 +132,14 @@ impl Container { // **< Container BUILDER >********************************************************************** - /// Establece el identificador único (`id`) del contenedor. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del contenedor. + /// Modifica identificador, clases CSS o atributos HTML del componente. /// /// También acepta clases predefinidas para: /// diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs index 779d9021..88e3dcf9 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs @@ -38,9 +38,7 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Dropdown { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del menú desplegable. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el título del menú desplegable. title: L10n, @@ -70,7 +68,7 @@ impl Component for Dropdown { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { @@ -90,7 +88,7 @@ impl Component for Dropdown { let title = self.title().using(cx); Ok(html! { - div id=[self.id()] (self.props()) { + div (self.props()) { @if !title.is_empty() { @let btn_base = { let mut classes = "btn".to_string(); @@ -178,14 +176,14 @@ impl Component for Dropdown { impl Dropdown { // **< Dropdown BUILDER >*********************************************************************** - /// Establece el identificador único (`id`) del menú desplegable. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del menú desplegable. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs index 53aec2e4..5eaecc00 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs @@ -45,9 +45,7 @@ pub enum ItemKind { /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Item { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del elemento. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el tipo de elemento representado. item_kind: ItemKind, @@ -59,7 +57,7 @@ impl Component for Item { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn prepare(&self, cx: &mut Context) -> Result { @@ -67,7 +65,7 @@ impl Component for Item { ItemKind::Void => html! {}, ItemKind::Label(label) => html! { - li id=[self.id()] (self.props()) { + li (self.props()) { span class="dropdown-item-text" { (label.using(cx)) } @@ -101,7 +99,7 @@ impl Component for Item { let tabindex = disabled.then_some("-1"); html! { - li id=[self.id()] (self.props()) { + li (self.props()) { a class=(classes) href=[href] @@ -127,7 +125,7 @@ impl Component for Item { let disabled_attr = disabled.then_some("disabled"); html! { - li id=[self.id()] (self.props()) { + li (self.props()) { button class=(classes) type="button" @@ -141,7 +139,7 @@ impl Component for Item { } ItemKind::Header(label) => html! { - li id=[self.id()] (self.props()) { + li (self.props()) { h6 class="dropdown-header" { (label.using(cx)) } @@ -149,7 +147,7 @@ impl Component for Item { }, ItemKind::Divider => html! { - li id=[self.id()] (self.props()) { hr class="dropdown-divider" {} } + li (self.props()) { hr class="dropdown-divider" {} } }, }) } @@ -260,14 +258,14 @@ impl Item { // **< Item BUILDER >*************************************************************************** - /// Establece el identificador único (`id`) del elemento. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del elemento. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/check.rs b/extensions/pagetop-bootsier/src/theme/form/check.rs index 7396dd2f..b692cefd 100644 --- a/extensions/pagetop-bootsier/src/theme/form/check.rs +++ b/extensions/pagetop-bootsier/src/theme/form/check.rs @@ -108,9 +108,7 @@ impl Item { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del contenedor del grupo. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el nombre base compartido por todas las casillas del grupo. name: AttrName, @@ -132,21 +130,31 @@ impl Component for Field { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { - self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes")); - } - - fn prepare(&self, cx: &mut Context) -> Result { + fn setup(&mut self, cx: &Context) { + // Asegura `name` e `id`. + // Si falta uno se deriva del otro; si faltan ambos se genera un valor único. let name = self .name() .get() .unwrap_or_else(|| cx.required_id::(self.id(), 3)); + self.alter_name(&name); let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); + self.alter_prop(PropsOp::ensure_id(container_id)); + + // Clases CSS del contenedor del grupo de casillas. + self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes")); + } + + fn prepare(&self, cx: &mut Context) -> Result { + // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. + let name = self.name().get().unwrap(); + let container_id = self.id().unwrap(); + Ok(html! { - div id=(&container_id) (self.props()) { + div (self.props()) { @if let Some(label) = self.label().lookup(cx) { label class="form-label" { (label) } } @@ -188,14 +196,14 @@ impl Component for Field { impl Field { // **< Field BUILDER >************************************************************************** - /// Establece el identificador único (`id`) del grupo de casillas. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del contenedor del grupo de casillas. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs index 06851900..74c3a1ff 100644 --- a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs +++ b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs @@ -43,9 +43,7 @@ use crate::theme::form; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Checkbox { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del contenedor del control. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve la variante visual del control. checkbox_kind: form::CheckboxKind, @@ -73,10 +71,21 @@ impl Component for Checkbox { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { + fn setup(&mut self, cx: &Context) { + // Asegura `name` e `id`. + // Si falta uno se deriva del otro; si faltan ambos se genera un valor único. + let name = self + .name() + .get() + .unwrap_or_else(|| cx.required_id::(self.id(), 1)); + self.alter_name(&name); + let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); + self.alter_prop(PropsOp::ensure_id(container_id)); + + // Clases CSS del contenedor de la casilla de verificación. let mut classes = "form-field form-check".to_string(); if *self.checkbox_kind() == form::CheckboxKind::Switch { classes.push_str(" form-switch"); @@ -91,15 +100,15 @@ impl Component for Checkbox { } fn prepare(&self, cx: &mut Context) -> Result { - let name = self - .name() - .get() - .unwrap_or_else(|| cx.required_id::(self.id(), 1)); - let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); + // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. + let name = self.name().get().unwrap(); + let container_id = self.id().unwrap(); + let checkbox_id = util::join!(&container_id, "-checkbox"); let is_switch = *self.checkbox_kind() == form::CheckboxKind::Switch; + Ok(html! { - div id=(&container_id) (self.props()) { + div (self.props()) { input type="checkbox" role=[is_switch.then_some("switch")] @@ -145,14 +154,14 @@ impl Checkbox { // **< Checkbox BUILDER >*********************************************************************** - /// Establece el identificador único (`id`) del control. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del contenedor del control. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/component.rs b/extensions/pagetop-bootsier/src/theme/form/component.rs index 9d6d2fad..999d0dd0 100644 --- a/extensions/pagetop-bootsier/src/theme/form/component.rs +++ b/extensions/pagetop-bootsier/src/theme/form/component.rs @@ -47,9 +47,7 @@ use crate::theme::form; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Form { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del formulario. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve la URL/ruta de destino del formulario. action: AttrValue, @@ -68,7 +66,7 @@ impl Component for Form { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { @@ -82,7 +80,6 @@ impl Component for Form { }; Ok(html! { form - id=[self.id()] (self.props()) action=[self.action().get()] method=[method] @@ -97,14 +94,14 @@ impl Component for Form { impl Form { // **< Form BUILDER >*************************************************************************** - /// Establece el identificador único (`id`) del formulario. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del formulario. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs index 2e6b7493..5a880376 100644 --- a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs +++ b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs @@ -24,9 +24,7 @@ use pagetop::prelude::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Fieldset { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del `fieldset`. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve la leyenda del `fieldset`. legend: Attr, @@ -44,7 +42,7 @@ impl Component for Fieldset { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn prepare(&self, cx: &mut Context) -> Result { @@ -55,7 +53,7 @@ impl Component for Fieldset { } Ok(html! { - fieldset id=[self.id()] (self.props()) disabled[*self.disabled()] { + fieldset (self.props()) disabled[*self.disabled()] { @if let Some(legend) = self.legend().lookup(cx) { legend { (legend) } } @@ -71,14 +69,14 @@ impl Component for Fieldset { impl Fieldset { // **< Fieldset BUILDER >*********************************************************************** - /// Establece el identificador único (`id`) del `fieldset` (grupo de controles). + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del `fieldset`. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/input.rs b/extensions/pagetop-bootsier/src/theme/form/input.rs index a15f5e95..d920a68c 100644 --- a/extensions/pagetop-bootsier/src/theme/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/form/input.rs @@ -126,9 +126,7 @@ impl fmt::Display for Mode { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del contenedor del campo. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el tipo de campo. kind: Kind, @@ -170,10 +168,18 @@ impl Component for Field { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { + if let Some(container_id) = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) + { + self.alter_prop(PropsOp::ensure_id(container_id)); + } + + // Clases CSS del contenedor del campo de texto. if *self.floating_label() { self.alter_prop(PropsOp::prepend_classes("form-floating")); } @@ -184,9 +190,7 @@ impl Component for Field { } fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); + let container_id = self.id(); let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); let input_class = if *self.plaintext() { "form-control-plaintext" @@ -217,7 +221,7 @@ impl Component for Field { None => html! {}, }; Ok(html! { - div id=[container_id.as_deref()] (self.props()) { + div (self.props()) { @if !*self.floating_label() { (label) } @@ -313,14 +317,14 @@ impl Field { // **< Field BUILDER >************************************************************************** - /// Establece el identificador único (`id`) del contenedor del campo. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del contenedor del campo. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/radio.rs b/extensions/pagetop-bootsier/src/theme/form/radio.rs index 101fa2fd..76b2dc32 100644 --- a/extensions/pagetop-bootsier/src/theme/form/radio.rs +++ b/extensions/pagetop-bootsier/src/theme/form/radio.rs @@ -96,9 +96,7 @@ impl Item { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del contenedor del grupo. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el nombre compartido por todos los botones de opción del grupo. name: AttrName, @@ -122,21 +120,31 @@ impl Component for Field { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { - self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios")); - } - - fn prepare(&self, cx: &mut Context) -> Result { + fn setup(&mut self, cx: &Context) { + // Asegura `name` e `id`. + // Si falta uno se deriva del otro; si faltan ambos se genera un valor único. let name = self .name() .get() .unwrap_or_else(|| cx.required_id::(self.id(), 3)); + self.alter_name(&name); let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); + self.alter_prop(PropsOp::ensure_id(container_id)); + + // Clases CSS del contenedor del grupo de opciones. + self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios")); + } + + fn prepare(&self, cx: &mut Context) -> Result { + // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. + let name = self.name().get().unwrap(); + let container_id = self.id().unwrap(); + Ok(html! { - div id=(&container_id) (self.props()) { + div (self.props()) { @if let Some(label) = self.label().lookup(cx) { label class="form-label" { (label) @@ -190,14 +198,14 @@ impl Component for Field { impl Field { // **< Field BUILDER >************************************************************************** - /// Establece el identificador único (`id`) del grupo de opciones. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del contenedor del grupo de opciones. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/range.rs b/extensions/pagetop-bootsier/src/theme/form/range.rs index bac5a0f7..37dfa68f 100644 --- a/extensions/pagetop-bootsier/src/theme/form/range.rs +++ b/extensions/pagetop-bootsier/src/theme/form/range.rs @@ -31,9 +31,7 @@ use pagetop::prelude::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Range { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del contenedor del control deslizante. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el nombre del campo. name: AttrName, @@ -61,20 +59,26 @@ impl Component for Range { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { + if let Some(container_id) = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) + { + self.alter_prop(PropsOp::ensure_id(container_id)); + }; + + // Clases CSS del contenedor del control deslizante. self.alter_prop(PropsOp::prepend_classes("form-field form-field-range")); } fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); + let container_id = self.id(); let range_id = container_id.as_deref().map(|id| util::join!(id, "-range")); Ok(html! { - div id=[container_id.as_deref()] (self.props()) { + div (self.props()) { @if let Some(label) = self.label().lookup(cx) { label for=[range_id.as_deref()] class="form-label" { (label) } } @@ -100,14 +104,14 @@ impl Component for Range { impl Range { // **< Range BUILDER >************************************************************************** - /// Establece el identificador único (`id`) del contenedor del control deslizante. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del contenedor del control deslizante. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/select.rs b/extensions/pagetop-bootsier/src/theme/form/select.rs index 48dc7127..47b411aa 100644 --- a/extensions/pagetop-bootsier/src/theme/form/select.rs +++ b/extensions/pagetop-bootsier/src/theme/form/select.rs @@ -191,9 +191,7 @@ pub enum Entry { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del contenedor de la lista de selección. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el nombre del campo. name: AttrName, @@ -225,10 +223,18 @@ impl Component for Field { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { + if let Some(container_id) = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) + { + self.alter_prop(PropsOp::ensure_id(container_id)); + }; + + // Clases CSS del contenedor de la lista de selección. if *self.floating_label() { self.alter_multiple(false); self.alter_rows(None::); @@ -238,9 +244,7 @@ impl Component for Field { } fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); + let container_id = self.id(); let select_id = container_id.as_deref().map(|id| util::join!(id, "-select")); let label = match self.label().lookup(cx) { Some(text) => html! { @@ -259,7 +263,7 @@ impl Component for Field { None => html! {}, }; Ok(html! { - div id=[container_id.as_deref()] (self.props()) { + div (self.props()) { @if !*self.floating_label() { (label) } @@ -318,14 +322,14 @@ impl Component for Field { impl Field { // **< Field BUILDER >*************************************************************************** - /// Establece el identificador único (`id`) del contenedor del campo. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del contenedor de la lista de selección. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/form/textarea.rs index 4a9f9c9c..f47bc139 100644 --- a/extensions/pagetop-bootsier/src/theme/form/textarea.rs +++ b/extensions/pagetop-bootsier/src/theme/form/textarea.rs @@ -34,9 +34,7 @@ use crate::theme::form; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Textarea { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del contenedor del área de texto. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el nombre del campo. name: AttrName, @@ -74,10 +72,18 @@ impl Component for Textarea { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { + if let Some(container_id) = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) + { + self.alter_prop(PropsOp::ensure_id(container_id)); + }; + + // Clases CSS del contenedor del área de texto. if *self.floating_label() { self.alter_rows(None::); self.alter_prop(PropsOp::prepend_classes("form-floating")); @@ -86,9 +92,7 @@ impl Component for Textarea { } fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); + let container_id = self.id(); let textarea_id = container_id .as_deref() .map(|id| util::join!(id, "-textarea")); @@ -116,7 +120,7 @@ impl Component for Textarea { None => html! {}, }; Ok(html! { - div id=[container_id.as_deref()] (self.props()) { + div (self.props()) { @if !*self.floating_label() { (label) } @@ -152,14 +156,14 @@ impl Component for Textarea { impl Textarea { // **< Textarea BUILDER >*********************************************************************** - /// Establece el identificador único (`id`) del contenedor del campo. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del contenedor del campo. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/icon.rs b/extensions/pagetop-bootsier/src/theme/icon.rs index 5b3f64d3..970a6cd4 100644 --- a/extensions/pagetop-bootsier/src/theme/icon.rs +++ b/extensions/pagetop-bootsier/src/theme/icon.rs @@ -15,7 +15,7 @@ pub enum IconKind { #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Icon { - /// Devuelve los atributos HTML y clases CSS del icono. + /// Devuelve los atributos HTML y clases CSS del componente. props: Props, icon_kind: IconKind, aria_label: AttrL10n, @@ -26,6 +26,10 @@ impl Component for Icon { Self::default() } + fn id(&self) -> Option { + self.props.get_id() + } + fn setup(&mut self, _cx: &Context) { if !matches!(self.icon_kind(), IconKind::None) { self.alter_prop(PropsOp::prepend_classes("icon")); @@ -98,7 +102,14 @@ impl Icon { // **< Icon BUILDER >*************************************************************************** - /// Modifica los atributos HTML o las clases CSS del icono. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); + self + } + + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/image/component.rs b/extensions/pagetop-bootsier/src/theme/image/component.rs index 72a07ae3..8ab379e7 100644 --- a/extensions/pagetop-bootsier/src/theme/image/component.rs +++ b/extensions/pagetop-bootsier/src/theme/image/component.rs @@ -13,9 +13,7 @@ use crate::theme::*; /// - Aplicar el texto alternativo `alt` con **localización** mediante [`L10n`]. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Image { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS de la imagen. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve las dimensiones de la imagen. size: image::Size, @@ -31,10 +29,11 @@ impl Component for Image { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { + // Clases CSS por defecto para la imagen, según el origen seleccionado. self.alter_prop(PropsOp::prepend_classes(self.source().to_class())); } @@ -46,7 +45,6 @@ impl Component for Image { image::Source::Logo(logo) => { return Ok(html! { span - id=[self.id()] (self.props()) style=[dimensions] role=[(!is_decorative).then_some("img")] @@ -65,7 +63,6 @@ impl Component for Image { img src=[source] alt=(alt_text) - id=[self.id()] (self.props()) style=[dimensions] {} }) @@ -80,14 +77,14 @@ impl Image { // **< Image BUILDER >************************************************************************** - /// Establece el identificador único (`id`) de la imagen. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS de la imagen. + /// Modifica identificador, clases CSS o atributos HTML del componente. /// /// También acepta clases predefinidas para: /// diff --git a/extensions/pagetop-bootsier/src/theme/nav/component.rs b/extensions/pagetop-bootsier/src/theme/nav/component.rs index e79e88aa..7f792db3 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/component.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/component.rs @@ -32,9 +32,7 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Nav { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del menú. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el estilo visual seleccionado. nav_kind: nav::Kind, @@ -50,10 +48,11 @@ impl Component for Nav { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { + // Clases CSS por defecto para el menú, según el estilo y la distribución seleccionados. self.alter_prop(PropsOp::prepend_classes({ let mut classes = "nav".to_string(); self.nav_kind().push_class(&mut classes); @@ -69,7 +68,7 @@ impl Component for Nav { } Ok(html! { - ul id=[self.id()] (self.props()) { + ul (self.props()) { (items) } }) @@ -94,14 +93,14 @@ impl Nav { // **< Nav BUILDER >**************************************************************************** - /// Establece el identificador único (`id`) del menú. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del menú. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/nav/item.rs b/extensions/pagetop-bootsier/src/theme/nav/item.rs index 5d2ef3de..9fbbc47d 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/item.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/item.rs @@ -78,9 +78,7 @@ impl ItemKind { /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Item { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del elemento. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el tipo de elemento representado. item_kind: ItemKind, @@ -92,7 +90,7 @@ impl Component for Item { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { @@ -104,7 +102,7 @@ impl Component for Item { ItemKind::Void => html! {}, ItemKind::Label(label) => html! { - li id=[self.id()] (self.props()) { + li (self.props()) { span class="nav-link disabled" aria-disabled="true" { (label.using(cx)) } @@ -137,7 +135,7 @@ impl Component for Item { let aria_disabled = (*disabled).then_some("true"); html! { - li id=[self.id()] (self.props()) { + li (self.props()) { a class=(classes) href=[href] @@ -153,7 +151,7 @@ impl Component for Item { } ItemKind::Html(html) => html! { - li id=[self.id()] (self.props()) { + li (self.props()) { (html.render(cx)) } }, @@ -170,7 +168,7 @@ impl Component for Item { .unwrap_or_else(|| "Dropdown".to_string()) }); html! { - li id=[self.id()] (self.props()) { + li (self.props()) { a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" @@ -283,14 +281,14 @@ impl Item { // **< Item BUILDER >*************************************************************************** - /// Establece el identificador único (`id`) del elemento. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del elemento. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs index 9e5082b6..511ea805 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs @@ -13,8 +13,6 @@ use crate::theme::*; /// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Brand { - #[getters(skip)] - id: AttrId, /// Devuelve la imagen de marca (si la hay). image: Embed, /// Devuelve el título de la identidad de marca. @@ -32,10 +30,6 @@ impl Component for Brand { Self::default() } - fn id(&self) -> Option { - self.id.get() - } - fn prepare(&self, cx: &mut Context) -> Result { let image = self.image().render(cx); let title = self.title().using(cx); @@ -56,13 +50,6 @@ impl Component for Brand { impl Brand { // **< Brand BUILDER >************************************************************************** - /// Establece el identificador único (`id`) de la marca. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); - self - } - /// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará. #[builder_fn] pub fn with_image(mut self, image: Option) -> Self { diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index 0438c981..8709a857 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/component.rs @@ -136,9 +136,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Navbar { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS de la barra de navegación. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el punto de ruptura configurado. expand: BreakPoint, @@ -156,10 +154,14 @@ impl Component for Navbar { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { + fn setup(&mut self, cx: &Context) { + // Asegura que la barra de navegación tiene un identificador único. + self.alter_prop(PropsOp::ensure_id(cx.build_id::(1))); + + // Clases CSS por defecto para la barra de navegación. self.alter_prop(PropsOp::prepend_classes({ let mut classes = "navbar".to_string(); self.expand().push_class(&mut classes, "navbar-expand", ""); @@ -198,11 +200,11 @@ impl Component for Navbar { return Ok(html! {}); } - // Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas. - let id = cx.required_id::(self.id(), 1); + // `setup()` garantiza que habrá un `id` antes de renderizar. + let id = self.id().unwrap(); Ok(html! { - nav id=(&id) (self.props()) { + nav (self.props()) { div class="container-fluid" { @match self.layout() { // Barra más sencilla: sólo contenido. @@ -335,14 +337,14 @@ impl Navbar { // **< Navbar BUILDER >************************************************************************* - /// Establece el identificador único (`id`) de la barra de navegación. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS de la barra de navegación. + /// Modifica identificador, clases CSS o atributos HTML del componente. /// /// También acepta clases predefinidas para: /// diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs index 481414fb..1d9f2c48 100644 --- a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs @@ -43,9 +43,7 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Offcanvas { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del panel. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el título del panel. title: L10n, @@ -69,10 +67,14 @@ impl Component for Offcanvas { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { + fn setup(&mut self, cx: &Context) { + // Asegura que el panel tiene un identificador único. + self.alter_prop(PropsOp::ensure_id(cx.build_id::(1))); + + // Clases CSS por defecto para el panel. self.alter_prop(PropsOp::prepend_classes({ let mut classes = "offcanvas".to_string(); self.breakpoint().push_class(&mut classes, "offcanvas", ""); @@ -90,14 +92,14 @@ impl Component for Offcanvas { impl Offcanvas { // **< Offcanvas BUILDER >********************************************************************** - /// Establece el identificador único (`id`) del panel. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del panel. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); @@ -172,7 +174,8 @@ impl Offcanvas { return html! {}; } - let id = cx.required_id::(self.id(), 1); + // `setup()` garantiza que habrá un `id` antes de renderizar. + let id = self.id().unwrap(); let id_label = util::join!(id, "-label"); let id_target = util::join!("#", id); @@ -191,7 +194,6 @@ impl Offcanvas { html! { div - id=(&id) (self.props()) tabindex="-1" data-bs-scroll=[body_scroll] diff --git a/src/base/action/component/after_render_component.rs b/src/base/action/component/after_render_component.rs index 0778e9c8..08eefdb5 100644 --- a/src/base/action/component/after_render_component.rs +++ b/src/base/action/component/after_render_component.rs @@ -6,7 +6,7 @@ use super::FnActionWithComponent; pub struct AfterRender { f: FnActionWithComponent, referer_type_id: Option, - referer_id: AttrId, + referer_id: Option, weight: Weight, } @@ -19,7 +19,7 @@ impl ActionDispatcher for AfterRender { /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { - self.referer_id.get() + self.referer_id.clone() } /// Devuelve el peso para definir el orden de ejecución. @@ -34,7 +34,7 @@ impl AfterRender { AfterRender { f, referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), + referer_id: None, weight: 0, } } @@ -42,7 +42,8 @@ impl AfterRender { /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// `C` con identificador `id`. pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - self.referer_id.alter_id(id); + let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); + self.referer_id = if id.is_empty() { None } else { Some(id) }; self } diff --git a/src/base/action/component/before_render_component.rs b/src/base/action/component/before_render_component.rs index 051a3dd6..e91589a2 100644 --- a/src/base/action/component/before_render_component.rs +++ b/src/base/action/component/before_render_component.rs @@ -6,7 +6,7 @@ use super::FnActionWithComponent; pub struct BeforeRender { f: FnActionWithComponent, referer_type_id: Option, - referer_id: AttrId, + referer_id: Option, weight: Weight, } @@ -19,7 +19,7 @@ impl ActionDispatcher for BeforeRender { /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { - self.referer_id.get() + self.referer_id.clone() } /// Devuelve el peso para definir el orden de ejecución. @@ -34,7 +34,7 @@ impl BeforeRender { BeforeRender { f, referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), + referer_id: None, weight: 0, } } @@ -42,7 +42,8 @@ impl BeforeRender { /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// `C` con identificador `id`. pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - self.referer_id.alter_id(id); + let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); + self.referer_id = if id.is_empty() { None } else { Some(id) }; self } diff --git a/src/base/action/component/transform_markup_component.rs b/src/base/action/component/transform_markup_component.rs index 3e3a81f5..bed2e192 100644 --- a/src/base/action/component/transform_markup_component.rs +++ b/src/base/action/component/transform_markup_component.rs @@ -6,7 +6,7 @@ use super::FnActionTransformMarkup; pub struct TransformMarkup { f: FnActionTransformMarkup, referer_type_id: Option, - referer_id: AttrId, + referer_id: Option, weight: Weight, } @@ -19,7 +19,7 @@ impl ActionDispatcher for TransformMarkup { /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { - self.referer_id.get() + self.referer_id.clone() } /// Devuelve el peso para definir el orden de ejecución. @@ -34,7 +34,7 @@ impl TransformMarkup { TransformMarkup { f, referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), + referer_id: None, weight: 0, } } @@ -42,7 +42,8 @@ impl TransformMarkup { /// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el /// componente `C` con identificador `id`. pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - self.referer_id.alter_id(id); + let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); + self.referer_id = if id.is_empty() { None } else { Some(id) }; self } diff --git a/src/base/component/block.rs b/src/base/component/block.rs index b583313b..2e56c5b0 100644 --- a/src/base/component/block.rs +++ b/src/base/component/block.rs @@ -6,9 +6,7 @@ use crate::prelude::*; /// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*). #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Block { - #[getters(skip)] - id: AttrId, - /// Devuelve los atributos HTML y clases CSS del bloque. + /// Devuelve identificador, clases CSS y atributos HTML del componente. props: Props, /// Devuelve el título del bloque. title: L10n, @@ -22,11 +20,15 @@ impl Component for Block { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { - self.props.alter_prop(PropsOp::prepend_classes("block")); + fn setup(&mut self, cx: &Context) { + // Asegura que el bloque tiene un identificador único. + self.alter_prop(PropsOp::ensure_id(cx.build_id::(1))); + + // Todos los bloques tienen la clase CSS `block` por defecto. + self.alter_prop(PropsOp::prepend_classes("block")); } fn prepare(&self, cx: &mut Context) -> Result { @@ -36,14 +38,12 @@ impl Component for Block { return Ok(html! {}); } - let id = cx.required_id::(self.id(), 1); - Ok(html! { - div id=(&id) (self.props()) { + div (self.props()) { @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,14 +52,14 @@ impl Component for Block { impl Block { // **< Block BUILDER >************************************************************************** - /// Establece el identificador único (`id`) del bloque. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica los atributos HTML o las clases CSS del bloque. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/src/core/component/context.rs b/src/core/component/context.rs index f460b134..d27de662 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -9,8 +9,8 @@ use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; use crate::web::HttpRequest; use crate::{CowStr, builder_fn, util}; -use std::any::Any; -use std::cell::Cell; +use std::any::{Any, TypeId}; +use std::cell::RefCell; use std::collections::HashMap; use std::fmt; @@ -271,7 +271,7 @@ pub trait Contextual: LangId { /// assert_eq!(id, 42); /// /// // Genera un identificador para un componente de tipo `Menu`. -/// let unique_id = cx.required_id::(None, 1); +/// let unique_id = cx.build_id::(1); /// assert_eq!(unique_id, "menu-1"); // Si es el primero generado. /// } /// ``` @@ -286,7 +286,7 @@ pub struct Context { javascripts: Assets, // Scripts JavaScript. regions : ChildrenInRegions, // Regiones de componentes para renderizar. params : HashMap<&'static str, (Box, &'static str)>, // Parámetros en ejecución. - id_counter : Cell, // Cell permite incrementar desde &self en required_id(). + id_counters: RefCell>, // RefCell permite mutar desde build_id(&self). messages : Vec, // Mensajes de usuario acumulados. } @@ -314,7 +314,7 @@ impl Context { javascripts: Assets::::new(), regions : ChildrenInRegions::default(), params : HashMap::default(), - id_counter : Cell::new(0), + id_counters: RefCell::new(HashMap::new()), messages : Vec::new(), } } @@ -374,31 +374,42 @@ impl Context { route } - /// Garantiza un identificador único para un componente `C`, generándolo si no se proporciona - /// ninguno. + /// Construye un identificador HTML único para el tipo de componente `C`. /// - /// Si `id` es `None`, crea un identificador usando los últimos segmentos del *path* completo - /// del tipo `C`, separados por `-` y en minúsculas, seguidos de un contador incremental interno - /// del contexto. Por ejemplo, para un componente `MyApp::ui::Menu` con `parts = 2` podría - /// devolver un identificador como `ui-menu-1` si ha sido el primero en generarse. + /// Toma los `segments` finales del *path* completo del tipo, los une con `-` y los convierte a + /// minúsculas, y añade un contador independiente por tipo. Por ejemplo, para `MyApp::ui::Menu` + /// con `segments = 2` devuelve `ui-menu-1` la primera vez que se invoca para ese tipo, + /// `ui-menu-2` la segunda, etc. /// - /// Con `parts = 1` se usa el nombre corto del tipo. Si `parts` es `0` o supera el número de - /// segmentos del *path*, entonces se usará el *path* completo. + /// Con `segments = 1` se usa sólo el nombre corto del tipo. Si `segments` es `0` o supera el + /// número de segmentos del *path*, se usan todos. /// - /// Es útil para asignar identificadores HTML cuando el componente no recibe uno explícito. - pub fn required_id(&self, id: Option, parts: usize) -> String { - if let Some(id) = id { - return id; - } - let segments: Vec<&str> = TypeInfo::FullName.of::().split("::").collect(); - let parts = if parts == 0 || parts >= segments.len() { - segments.len() + /// Es útil para asignar identificadores cuando el componente no recibe uno explícito. El + /// contador es local a este contexto y se reinicia para cada nueva petición. + pub fn build_id(&self, segments: usize) -> String { + let path: Vec<&str> = TypeInfo::FullName.of::().split("::").collect(); + let segments = if segments == 0 || segments >= path.len() { + path.len() } else { - parts + segments }; - self.id_counter.set(self.id_counter.get() + 1); - let prefix = segments[segments.len() - parts..].join("-").to_lowercase(); - util::join!(prefix, "-", self.id_counter.get().to_string()) + let count = { + let mut map = self.id_counters.borrow_mut(); + let n = map.entry(TypeId::of::()).or_insert(0); + *n += 1; + *n + }; + let prefix = path[path.len() - segments..].join("-").to_lowercase(); + util::join!(prefix, "-", count.to_string()) + } + + /// Devuelve `id` si contiene un valor, o genera uno único con [`build_id`](Self::build_id) + /// si es `None`. + pub fn required_id(&self, id: Option, segments: usize) -> String { + match id { + Some(id) => id, + None => self.build_id::(segments), + } } /// Acumula un [`StatusMessage`] en el contexto para notificar al visitante. diff --git a/src/html.rs b/src/html.rs index bff37698..9f2daace 100644 --- a/src/html.rs +++ b/src/html.rs @@ -20,7 +20,7 @@ pub use logo::PageTopSvg; // **< HTML ATTRIBUTES >**************************************************************************** mod attr; -pub use attr::{Attr, AttrId, AttrName, AttrValue}; +pub use attr::{Attr, AttrName, AttrValue}; mod props; pub use props::{Props, PropsOp}; diff --git a/src/html/attr.rs b/src/html/attr.rs index 8f25a5eb..07fe52f5 100644 --- a/src/html/attr.rs +++ b/src/html/attr.rs @@ -8,7 +8,7 @@ use crate::{AutoDefault, builder_fn}; /// /// Este tipo **no impone ninguna normalización ni semántica concreta**; dichas reglas se definen en /// implementaciones concretas como `Attr` y `Attr`, o en tipos específicos como -/// [`AttrId`] y [`AttrName`]. +/// [`AttrName`]. #[derive(AutoDefault, Clone, Debug)] pub struct Attr(Option); @@ -128,73 +128,6 @@ impl Attr { } } -// **< AttrId >************************************************************************************* - -/// Identificador normalizado para el atributo `id` o similar de HTML. -/// -/// Este tipo encapsula `Option` garantizando un valor normalizado para su uso: -/// -/// - Se eliminan los espacios al principio y al final. -/// - Se convierte a minúsculas. -/// - Se sustituyen los espacios (`' '`) intermedios por guiones bajos (`_`). -/// - Si el resultado es una cadena vacía, se guarda `None`. -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// let id = AttrId::new(" main Section "); -/// assert_eq!(id.as_str(), Some("main_section")); -/// -/// let empty = AttrId::default(); -/// assert_eq!(empty.get(), None); -/// ``` -#[derive(AutoDefault, Clone, Debug)] -pub struct AttrId(Attr); - -impl AttrId { - /// Crea un nuevo `AttrId` normalizando el valor. - pub fn new(id: impl AsRef) -> Self { - Self::default().with_id(id) - } - - // **< AttrId BUILDER >************************************************************************* - - /// Establece un identificador nuevo normalizando el valor. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - let id = id.as_ref().trim(); - if id.is_empty() { - self.0 = Attr::default(); - } else { - self.0 = Attr::some(id.to_ascii_lowercase().replace(' ', "_")); - } - self - } - - // **< AttrId GETTERS >************************************************************************* - - /// Devuelve el identificador normalizado, si existe. - pub fn get(&self) -> Option { - self.0.get() - } - - /// Devuelve el identificador normalizado (sin clonar), si existe. - pub fn as_str(&self) -> Option<&str> { - self.0.as_str() - } - - /// Devuelve el identificador normalizado (propiedad), si existe. - pub fn into_inner(self) -> Option { - self.0.into_inner() - } - - /// `true` si no hay valor. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - // **< AttrName >*********************************************************************************** /// Nombre normalizado para el atributo `name` o similar de HTML. diff --git a/src/html/props.rs b/src/html/props.rs index 83227045..223924af 100644 --- a/src/html/props.rs +++ b/src/html/props.rs @@ -7,32 +7,57 @@ use std::fmt::Write; /// Operaciones disponibles sobre atributos HTML y clases CSS en [`Props`]. /// -/// Cada variante es autocontenida, lleva todos los datos que necesita para ejecutarse. El método -/// recomendado para construirlas es usar los constructores asociados ([`set()`](Self::set), -/// [`remove()`](Self::remove), [`add_classes()`](Self::add_classes), etc.). +/// Cada variante lleva los datos necesarios para ejecutarse. El método recomendado para usarlas es +/// recurrir a los constructores asociados como [`set()`](Self::set), [`set_id()`](Self::set_id), +/// [`remove()`](Self::remove), [`add_classes()`](Self::add_classes), etc. /// -/// Las variantes `*Classes` operan siempre sobre la lista de clases CSS para el componente. +/// Las variantes `*Id` operan sobre el atributo `id` del componente. Cuando se usa `"id"` como +/// nombre de atributo en `Set`, el valor se normaliza igual que en `SetId` o `EnsureId`. /// -/// Cuando se usa `"class"` como nombre de atributo en `Set` o `Remove` la operación se aplica a la -/// lista de clases completa. Así, `Set("class", ...)` reemplaza la lista de clases completa por las -/// nuevas clases indicadas, y `Remove("class")` vacía la lista de clases. +/// Las variantes `*Classes` operan siempre sobre la lista de clases CSS para el componente. Cuando +/// se usa `"class"` como nombre en `Set` o `Remove` la operación se aplica a la lista de clases +/// completa. Así, `Set("class", ...)` reemplaza la lista de clases completa por las nuevas clases +/// indicadas, y `Remove("class")` vacía la lista de clases. #[derive(Clone, Debug, PartialEq)] pub enum PropsOp { - /// Añade el atributo o sustituye su valor si ya existe. Usar `"class"` como nombre reemplaza la - /// lista completa de clases por las nuevas indicadas; la operación se ignora si el valor - /// contiene caracteres no ASCII. + /// Establece el identificador del elemento normalizando el valor: recorta espacios, convierte a + /// minúsculas y sustituye los espacios intermedios por `_`. Si el resultado es vacío, elimina + /// el identificador. + SetId(CowStr), + /// Establece el identificador del elemento si aún no hay ninguno definido, de modo que no + /// sobrescribe un valor asignado con anterioridad. Aplica la misma normalización que + /// [`SetId`](Self::SetId); si el resultado es vacío, la operación tampoco tiene efecto. + EnsureId(CowStr), + /// Añade el atributo o sustituye su valor si ya existe. Usar `"id"` como nombre aplica la misma + /// normalización que [`SetId`](Self::SetId). Usar `"class"` como nombre reemplaza la lista + /// completa de clases por las nuevas indicadas; la operación se ignora si el valor contiene + /// caracteres no ASCII. Set(CowStr, CowStr), - /// Elimina el atributo indicado. Usar `"class"` como nombre vacía la lista de clases. + /// Elimina el atributo indicado, incluido `"id"`. Si se usa `"class"` como nombre se vacía la + /// lista de clases. Remove(CowStr), - /// Añade las clases que no existan al final de la lista. + /// Añade las clases que no existan al final de la lista. La operación se ignora si el valor + /// contiene caracteres no ASCII. AddClasses(CowStr), - /// Añade las clases que no existan al principio de la lista. + /// Añade las clases que no existan al principio de la lista. La operación se ignora si el valor + /// contiene caracteres no ASCII. PrependClasses(CowStr), - /// Elimina las clases indicadas de la lista. + /// Elimina las clases indicadas de la lista. La operación se ignora si el valor contiene + /// caracteres no ASCII. RemoveClasses(CowStr), } impl PropsOp { + /// Crea la variante [`SetId`](Self::SetId) con el identificador indicado. + pub fn set_id(id: impl Into) -> Self { + Self::SetId(id.into()) + } + + /// Crea la variante [`EnsureId`](Self::EnsureId) con el identificador indicado. + pub fn ensure_id(id: impl Into) -> Self { + Self::EnsureId(id.into()) + } + /// Crea la variante [`Set`](Self::Set) con nombre y valor del atributo. pub fn set(name: impl Into, value: impl Into) -> Self { Self::Set(name.into(), value.into()) @@ -61,11 +86,10 @@ impl PropsOp { // **< Props >************************************************************************************** -/// Colección de pares `atributo="valor"` y clases CSS para aplicar en componentes. +/// Colección de identificador, atributos HTML y clases CSS para aplicar en componentes. /// -/// Permite añadir dinámicamente pares `atributo="valor"` y clases CSS al elemento raíz de un -/// componente. Al renderizar los atributos en `html!` primero emite el atributo `class` (si hay -/// clases) y luego el resto de atributos. +/// Al renderizar en `html!` emite primero `id` (si existe), luego `class` (si hay clases) y después +/// el resto de atributos. /// /// # Ejemplo /// @@ -85,6 +109,35 @@ impl PropsOp { /// ); /// ``` /// +/// # Identificadores +/// +/// [`SetId`](PropsOp::SetId) (usando [`PropsOp::set_id`]) normaliza el valor asignado al +/// identificador del componente: recorta espacios, convierte a minúsculas y sustituye los espacios +/// intermedios por `_`. +/// +/// ```rust +/// # use pagetop::prelude::*; +/// let props = Props::default().with_id("My Button"); +/// let markup = html! { button (props) { "OK" } }; +/// assert_eq!(markup.into_string(), r#""#); +/// ``` +/// +/// [`EnsureId`](PropsOp::EnsureId) (usando [`PropsOp::ensure_id`]) sólo asigna si no +/// hay identificador previo: +/// +/// ```rust +/// # use pagetop::prelude::*; +/// // Con `id` previo: `EnsureId` no tiene efecto. +/// let props = Props::default() +/// .with_id("explicit") +/// .with_prop(PropsOp::ensure_id("default")); +/// assert_eq!(props.get_id(), Some("explicit".to_string())); +/// +/// // Sin `id` previo: `EnsureId` asigna el valor. +/// let props = Props::default().with_prop(PropsOp::ensure_id("default")); +/// assert_eq!(props.get_id(), Some("default".to_string())); +/// ``` +/// /// # Clases CSS /// /// ```rust @@ -122,7 +175,7 @@ impl PropsOp { /// } /// /// impl MyButton { -/// /// Modifica los atributos HTML o las clases CSS del elemento raíz. +/// /// Modifica identificador, clases CSS o atributos HTML del elemento raíz. /// #[builder_fn] /// pub fn with_prop(mut self, op: PropsOp) -> Self { /// self.props.alter_prop(op); @@ -132,6 +185,7 @@ impl PropsOp { /// ``` #[derive(AutoDefault, Clone, Debug)] pub struct Props { + id: Option, attrs: Vec<(CowStr, CowStr)>, classes: Vec, } @@ -149,12 +203,23 @@ impl Props { // **< Props BUILDER >************************************************************************** - /// Modifica los atributos o clases según la operación indicada. + /// Establece el identificador del componente; equivale a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] + pub fn with_id(mut self, id: impl Into) -> Self { + self.apply_id(id.into().as_ref()); + self + } + + /// Modifica el identificador, los atributos o las clases según la operación indicada. /// + /// - [`SetId(value)`](PropsOp::SetId) establece el identificador normalizando el valor. + /// - [`EnsureId(value)`](PropsOp::EnsureId) establece el identificador (con la misma + /// normalización) sólo si no hay ninguno definido. /// - [`Set(name, value)`](PropsOp::Set) añade el atributo o reemplaza su valor. + /// `Set("id", ...)` aplica la misma normalización que `SetId`. /// `Set("class", ...)` reemplaza la lista de clases completa. - /// - [`Remove(name)`](PropsOp::Remove) elimina el atributo. `Remove("class")` vacía la lista de - /// clases. + /// - [`Remove(name)`](PropsOp::Remove) elimina el atributo. `Remove("id")` elimina el + /// identificador. `Remove("class")` vacía la lista de clases. /// - [`AddClasses(clases)`](PropsOp::AddClasses) añade clases al final (sin duplicados). /// - [`PrependClasses(clases)`](PropsOp::PrependClasses) añade clases al principio (sin /// duplicados). @@ -162,8 +227,18 @@ impl Props { #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { match op { + PropsOp::SetId(value) => { + self.apply_id(value.as_ref()); + } + PropsOp::EnsureId(value) => { + if self.id.is_none() { + self.apply_id(value.as_ref()); + } + } PropsOp::Set(name, value) => { - if name.as_ref() == "class" { + if name.as_ref() == "id" { + self.apply_id(value.as_ref()); + } else if name.as_ref() == "class" { if let Some(normalized) = util::normalize_ascii_or_empty(value.as_ref(), "Props::with_prop") { @@ -177,7 +252,9 @@ impl Props { } } PropsOp::Remove(name) => { - if name.as_ref() == "class" { + if name.as_ref() == "id" { + self.id = None; + } else if name.as_ref() == "class" { self.classes.clear(); } else { self.attrs.retain(|(k, _)| k != &name); @@ -219,18 +296,26 @@ impl Props { // **< Props GETTERS >************************************************************************** - /// Devuelve el valor del atributo indicado, si existe. - pub fn get_prop(&self, name: impl AsRef) -> Option<&str> { - let name = name.as_ref(); - self.attrs - .iter() - .find(|(k, _)| k.as_ref() == name) - .map(|(_, v)| v.as_ref()) + /// Devuelve el identificador normalizado del elemento, si existe. + #[inline] + pub fn get_id(&self) -> Option { + self.id.clone() } - /// Devuelve `true` si no hay ningún atributo definido. - pub fn is_props_empty(&self) -> bool { - self.attrs.is_empty() + /// Devuelve el valor del atributo indicado, si existe. + /// + /// Los nombres `"id"` y `"class"` son equivalentes a llamar a [`get_id()`](Self::get_id) y + /// [`get_classes()`](Self::get_classes) respectivamente. + pub fn get_prop(&self, name: impl AsRef) -> Option { + match name.as_ref() { + "id" => self.id.clone(), + "class" => self.get_classes(), + name => self + .attrs + .iter() + .find(|(k, _)| k.as_ref() == name) + .map(|(_, v)| v.to_string()), + } } /// Devuelve la lista de clases como cadena de texto, si hay clases definidas. @@ -242,11 +327,31 @@ impl Props { } } + /// Devuelve `true` si no hay ningún identificador definido. + #[inline] + pub fn is_id_empty(&self) -> bool { + self.id.is_none() + } + + /// Devuelve `true` si no hay ningún atributo extra definido, sin tener en cuenta el + /// identificador ni las clases. + #[inline] + pub fn is_attrs_empty(&self) -> bool { + self.attrs.is_empty() + } + /// Devuelve `true` si no hay ninguna clase definida. + #[inline] pub fn is_classes_empty(&self) -> bool { self.classes.is_empty() } + /// Devuelve `true` si no hay ningún identificador, atributo ni clase definidos. + #[inline] + pub fn is_empty(&self) -> bool { + self.id.is_none() && self.attrs.is_empty() && self.classes.is_empty() + } + /// Devuelve `true` si la clase o **todas** las clases indicadas están presentes. pub fn has_class(&self, classes: impl AsRef) -> bool { let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else { @@ -269,9 +374,17 @@ impl Props { .any(|class| self.classes.iter().any(|c| c == class)) } - // **< Props PRIVADO >************************************************************************** + // **< Props PRIVATE >************************************************************************** + + fn apply_id(&mut self, id: &str) { + let id = id.trim(); + self.id = if id.is_empty() { + None + } else { + Some(id.to_ascii_lowercase().replace(' ', "_")) + }; + } - #[inline] fn insert_classes<'a, I>(&mut self, classes: I, mut pos: usize) where I: IntoIterator, @@ -293,6 +406,11 @@ impl Props { #[doc(hidden)] impl Render for Props { fn render_to(&self, w: &mut String) { + if let Some(id) = self.id.as_deref() { + w.push_str(" id=\""); + let _ = write!(Escaper::new(w), "{}", id); + w.push('"'); + } if let Some((first, rest)) = self.classes.split_first() { w.push_str(" class=\""); let _ = write!(Escaper::new(w), "{}", first); diff --git a/src/response/page.rs b/src/response/page.rs index 4ca3f147..57684365 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -20,7 +20,7 @@ use crate::base::action; use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual}; use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; -use crate::html::{Attr, AttrId, Props, PropsOp}; +use crate::html::{Attr, Props, PropsOp}; use crate::html::{DOCTYPE, Markup, html}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; use crate::web::HttpRequest; @@ -89,7 +89,6 @@ pub struct Page { description : Attr, metadata : Vec<(&'static str, &'static str)>, properties : Vec<(&'static str, &'static str)>, - body_id : AttrId, body_props : Props, context : Context, } @@ -106,7 +105,6 @@ impl Page { description : Attr::::default(), metadata : Vec::default(), properties : Vec::default(), - body_id : AttrId::default(), body_props : Props::default(), context : Context::new(Some(request)), } @@ -142,14 +140,7 @@ impl Page { self } - /// Establece el atributo `id` del elemento ``. - #[builder_fn] - pub fn with_body_id(mut self, id: impl AsRef) -> Self { - self.body_id.alter_id(id); - self - } - - /// Modifica los atributos HTML o las clases CSS del elemento ``. + /// Modifica identificador, clases CSS o atributos HTML del elemento ``. #[builder_fn] pub fn with_body_props(mut self, op: PropsOp) -> Self { self.body_props.alter_prop(op); @@ -178,12 +169,7 @@ impl Page { &self.properties } - /// Devuelve el identificador del elemento ``. - pub fn body_id(&self) -> &AttrId { - &self.body_id - } - - /// Devuelve los atributos HTML y clases CSS del elemento ``. + /// Devuelve identificador, clases CSS y atributos HTML del elemento ``. pub fn body_props(&self) -> &Props { &self.body_props } @@ -261,7 +247,7 @@ impl Page { head { (head) } - body id=[self.body_id().get()] (self.body_props()) { + body (self.body_props()) { (body) } } diff --git a/tests/component_children.rs b/tests/component_children.rs index 791d23ff..dc5eb5cc 100644 --- a/tests/component_children.rs +++ b/tests/component_children.rs @@ -7,7 +7,7 @@ use pagetop::prelude::*; #[derive(AutoDefault, Clone)] struct TestComp { - id: AttrId, + props: Props, text: String, } @@ -17,7 +17,7 @@ impl Component for TestComp { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn prepare(&self, _cx: &mut Context) -> Result { @@ -29,7 +29,7 @@ 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.props.alter_prop(PropsOp::set_id(id.to_string())); c.text = text.to_string(); c } @@ -303,7 +303,8 @@ 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"); + comp.props + .alter_prop(PropsOp::set_id("modificado".to_string())); }; assert_eq!(embed.id(), Some("modificado".to_string())); } @@ -331,7 +332,8 @@ async fn embed_clone_is_deep() { 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"); + comp.props + .alter_prop(PropsOp::set_id("clone-id".to_string())); } assert_eq!(original.id(), Some("orig".to_string())); assert_eq!(clone.id(), Some("clone-id".to_string())); diff --git a/tests/html_props.rs b/tests/html_props.rs index f622dd95..3db26746 100644 --- a/tests/html_props.rs +++ b/tests/html_props.rs @@ -13,7 +13,7 @@ async fn props_default_renders_nothing() { #[pagetop::test] async fn props_new_creates_first_attr() { let p = Props::new("hx-get", "/api"); - assert_eq!(p.get_prop("hx-get"), Some("/api")); + assert_eq!(p.get_prop("hx-get"), Some("/api".to_string())); } #[pagetop::test] @@ -59,14 +59,14 @@ async fn props_set_adds_new_attrs() { let p = Props::default() .with_prop(PropsOp::set("hx-get", "/api")) .with_prop(PropsOp::set("hx-swap", "outerHTML")); - assert_eq!(p.get_prop("hx-get"), Some("/api")); - assert_eq!(p.get_prop("hx-swap"), Some("outerHTML")); + assert_eq!(p.get_prop("hx-get"), Some("/api".to_string())); + assert_eq!(p.get_prop("hx-swap"), Some("outerHTML".to_string())); } #[pagetop::test] async fn props_set_replaces_existing_value() { let p = Props::new("hx-get", "/old").with_prop(PropsOp::set("hx-get", "/new")); - assert_eq!(p.get_prop("hx-get"), Some("/new")); + assert_eq!(p.get_prop("hx-get"), Some("/new".to_string())); } #[pagetop::test] @@ -98,13 +98,13 @@ async fn props_remove_existing_attr() { .with_prop(PropsOp::set("b", "2")) .with_prop(PropsOp::remove("a")); assert_eq!(p.get_prop("a"), None); - assert_eq!(p.get_prop("b"), Some("2")); + assert_eq!(p.get_prop("b"), Some("2".to_string())); } #[pagetop::test] async fn props_remove_nonexistent_key_is_noop() { let p = Props::new("a", "1").with_prop(PropsOp::remove("missing")); - assert_eq!(p.get_prop("a"), Some("1")); + assert_eq!(p.get_prop("a"), Some("1".to_string())); assert_eq!(p.get_prop("missing"), None); } @@ -222,22 +222,42 @@ async fn props_splice_empty_string_emits_nothing() { assert_eq!(html! { span ("") { "x" } }.into_string(), "x"); } -// **< is_props_empty / is_classes_empty >********************************************************** +// **< is_attrs_empty / is_classes_empty / is_empty >*********************************************** #[pagetop::test] -async fn props_is_props_empty_on_default() { - assert!(Props::default().is_props_empty()); +async fn props_is_attrs_empty_on_default() { + assert!(Props::default().is_attrs_empty()); } #[pagetop::test] -async fn props_is_props_empty_false_after_set() { - assert!(!Props::new("hx-get", "/api").is_props_empty()); +async fn props_is_attrs_empty_false_after_set() { + assert!(!Props::new("hx-get", "/api").is_attrs_empty()); } #[pagetop::test] -async fn props_is_props_empty_true_after_removing_last_attr() { +async fn props_is_attrs_empty_true_after_removing_last_attr() { let p = Props::new("only", "one").with_prop(PropsOp::remove("only")); - assert!(p.is_props_empty()); + assert!(p.is_attrs_empty()); +} + +#[pagetop::test] +async fn props_is_empty_on_default() { + assert!(Props::default().is_empty()); +} + +#[pagetop::test] +async fn props_is_empty_false_with_id() { + assert!(!Props::default().with_id("main").is_empty()); +} + +#[pagetop::test] +async fn props_is_empty_false_with_attr() { + assert!(!Props::new("hx-get", "/api").is_empty()); +} + +#[pagetop::test] +async fn props_is_empty_false_with_class() { + assert!(!Props::classes("btn").is_empty()); } #[pagetop::test] @@ -256,6 +276,45 @@ async fn props_is_classes_empty_true_after_remove_class() { assert!(p.is_classes_empty()); } +// **< get_prop("id") / get_prop("class") >********************************************************* + +#[pagetop::test] +async fn get_prop_id_returns_none_by_default() { + assert_eq!(Props::default().get_prop("id"), None); +} + +#[pagetop::test] +async fn get_prop_id_returns_normalized_value() { + let p = Props::default().with_id("My Button"); + assert_eq!(p.get_prop("id"), Some("my_button".to_string())); +} + +#[pagetop::test] +async fn get_prop_id_matches_get_id() { + let p = Props::default().with_id("Header"); + assert_eq!(p.get_prop("id"), p.get_id()); +} + +#[pagetop::test] +async fn get_prop_class_returns_none_by_default() { + assert_eq!(Props::default().get_prop("class"), None); +} + +#[pagetop::test] +async fn get_prop_class_returns_joined_classes() { + let p = Props::classes("btn btn-primary").with_prop(PropsOp::add_classes("active")); + assert_eq!( + p.get_prop("class"), + Some("btn btn-primary active".to_string()) + ); +} + +#[pagetop::test] +async fn get_prop_class_matches_get_classes() { + let p = Props::classes("btn active"); + assert_eq!(p.get_prop("class"), p.get_classes()); +} + // **< Regression & edge cases >******************************************************************** #[pagetop::test] @@ -284,9 +343,9 @@ async fn props_chained_set_and_remove_yields_expected_state() { .with_prop(PropsOp::set("c", "3")) .with_prop(PropsOp::remove("b")) .with_prop(PropsOp::set("a", "updated")); - assert_eq!(p.get_prop("a"), Some("updated")); + assert_eq!(p.get_prop("a"), Some("updated".to_string())); assert_eq!(p.get_prop("b"), None); - assert_eq!(p.get_prop("c"), Some("3")); + assert_eq!(p.get_prop("c"), Some("3".to_string())); assert_eq!( html! { span (p) {} }.into_string(), r#""#