Compare commits

..

No commits in common. "2202a2350cc2a240422562e8bb37b75f6d864f1e" and "8d0103c257d2c60d934fc712711ea92ef4289c13" have entirely different histories.

33 changed files with 431 additions and 571 deletions

View file

@ -389,10 +389,10 @@ body {
.intro-text-body .block { .intro-text-body .block {
position: relative; position: relative;
} }
.intro-text-body .block-title { .intro-text-body .block__title {
margin: 1em 0 .8em; margin: 1em 0 .8em;
} }
.intro-text-body .block-title span { .intro-text-body .block__title span {
display: inline-block; display: inline-block;
padding: 10px 30px 14px; padding: 10px 30px 14px;
margin: 30px 20px 0; margin: 30px 20px 0;
@ -403,7 +403,7 @@ body {
border-color: orangered; border-color: orangered;
transform: rotate(-3deg) translateY(-25%); transform: rotate(-3deg) translateY(-25%);
} }
.intro-text-body .block-title:before { .intro-text-body .block__title:before {
content: ""; content: "";
height: 5px; height: 5px;
position: absolute; position: absolute;
@ -416,7 +416,7 @@ body {
transform: rotate(2deg) translateY(-50%); transform: rotate(2deg) translateY(-50%);
transform-origin: top left; transform-origin: top left;
} }
.intro-text-body .block-title:after { .intro-text-body .block__title:after {
content: ""; content: "";
height: 120%; height: 120%;
position: absolute; position: absolute;
@ -427,22 +427,22 @@ body {
background: var(--intro-bg-block-1); background: var(--intro-bg-block-1);
transform: rotate(2deg); 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); 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); 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); 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); 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); 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); background: var(--intro-bg-block-6);
} }

View file

@ -18,11 +18,6 @@
font-display: swap; 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. // Required field indicator in forms.
.form-required { .form-required {
color: var(--bs-danger); color: var(--bs-danger);

View file

@ -45,7 +45,9 @@ use crate::theme::{ButtonAction, ButtonColor, ButtonSize};
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Button { pub struct Button {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del botón.
props: Props, props: Props,
/// Devuelve el comportamiento del botón al activarse. /// Devuelve el comportamiento del botón al activarse.
kind: ButtonAction, kind: ButtonAction,
@ -71,7 +73,7 @@ impl Component for Button {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
@ -84,6 +86,7 @@ impl Component for Button {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! { Ok(html! {
button button
id=[self.id()]
type=(self.kind()) type=(self.kind())
(self.props()) (self.props())
name=[self.name().get()] name=[self.name().get()]
@ -137,14 +140,14 @@ impl Button {
// **< Button BUILDER >************************************************************************* // **< Button BUILDER >*************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del botón.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del botón.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -20,7 +20,9 @@ use crate::theme::*;
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Container { pub struct Container {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del contenedor.
props: Props, props: Props,
/// Devuelve el tipo semántico del contenedor. /// Devuelve el tipo semántico del contenedor.
container_kind: container::Kind, container_kind: container::Kind,
@ -36,7 +38,7 @@ impl Component for Container {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
@ -56,32 +58,32 @@ impl Component for Container {
}; };
Ok(match self.container_kind() { Ok(match self.container_kind() {
container::Kind::Default => html! { container::Kind::Default => html! {
div (self.props()) style=[style] { div id=[self.id()] (self.props()) style=[style] {
(output) (output)
} }
}, },
container::Kind::Main => html! { container::Kind::Main => html! {
main (self.props()) style=[style] { main id=[self.id()] (self.props()) style=[style] {
(output) (output)
} }
}, },
container::Kind::Header => html! { container::Kind::Header => html! {
header (self.props()) style=[style] { header id=[self.id()] (self.props()) style=[style] {
(output) (output)
} }
}, },
container::Kind::Footer => html! { container::Kind::Footer => html! {
footer (self.props()) style=[style] { footer id=[self.id()] (self.props()) style=[style] {
(output) (output)
} }
}, },
container::Kind::Section => html! { container::Kind::Section => html! {
section (self.props()) style=[style] { section id=[self.id()] (self.props()) style=[style] {
(output) (output)
} }
}, },
container::Kind::Article => html! { container::Kind::Article => html! {
article (self.props()) style=[style] { article id=[self.id()] (self.props()) style=[style] {
(output) (output)
} }
}, },
@ -132,14 +134,14 @@ impl Container {
// **< Container BUILDER >********************************************************************** // **< Container BUILDER >**********************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del contenedor.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del contenedor.
/// ///
/// También acepta clases predefinidas para: /// También acepta clases predefinidas para:
/// ///

View file

@ -38,7 +38,9 @@ use crate::theme::*;
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Dropdown { pub struct Dropdown {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del menú desplegable.
props: Props, props: Props,
/// Devuelve el título del menú desplegable. /// Devuelve el título del menú desplegable.
title: L10n, title: L10n,
@ -68,7 +70,7 @@ impl Component for Dropdown {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
@ -88,7 +90,7 @@ impl Component for Dropdown {
let title = self.title().using(cx); let title = self.title().using(cx);
Ok(html! { Ok(html! {
div (self.props()) { div id=[self.id()] (self.props()) {
@if !title.is_empty() { @if !title.is_empty() {
@let btn_base = { @let btn_base = {
let mut classes = "btn".to_string(); let mut classes = "btn".to_string();
@ -176,14 +178,14 @@ impl Component for Dropdown {
impl Dropdown { impl Dropdown {
// **< Dropdown BUILDER >*********************************************************************** // **< Dropdown BUILDER >***********************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del menú desplegable.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del menú desplegable.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -45,7 +45,9 @@ pub enum ItemKind {
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Item { pub struct Item {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del elemento.
props: Props, props: Props,
/// Devuelve el tipo de elemento representado. /// Devuelve el tipo de elemento representado.
item_kind: ItemKind, item_kind: ItemKind,
@ -57,7 +59,7 @@ impl Component for Item {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
@ -65,7 +67,7 @@ impl Component for Item {
ItemKind::Void => html! {}, ItemKind::Void => html! {},
ItemKind::Label(label) => html! { ItemKind::Label(label) => html! {
li (self.props()) { li id=[self.id()] (self.props()) {
span class="dropdown-item-text" { span class="dropdown-item-text" {
(label.using(cx)) (label.using(cx))
} }
@ -99,7 +101,7 @@ impl Component for Item {
let tabindex = disabled.then_some("-1"); let tabindex = disabled.then_some("-1");
html! { html! {
li (self.props()) { li id=[self.id()] (self.props()) {
a a
class=(classes) class=(classes)
href=[href] href=[href]
@ -125,7 +127,7 @@ impl Component for Item {
let disabled_attr = disabled.then_some("disabled"); let disabled_attr = disabled.then_some("disabled");
html! { html! {
li (self.props()) { li id=[self.id()] (self.props()) {
button button
class=(classes) class=(classes)
type="button" type="button"
@ -139,7 +141,7 @@ impl Component for Item {
} }
ItemKind::Header(label) => html! { ItemKind::Header(label) => html! {
li (self.props()) { li id=[self.id()] (self.props()) {
h6 class="dropdown-header" { h6 class="dropdown-header" {
(label.using(cx)) (label.using(cx))
} }
@ -147,7 +149,7 @@ impl Component for Item {
}, },
ItemKind::Divider => html! { ItemKind::Divider => html! {
li (self.props()) { hr class="dropdown-divider" {} } li id=[self.id()] (self.props()) { hr class="dropdown-divider" {} }
}, },
}) })
} }
@ -258,14 +260,14 @@ impl Item {
// **< Item BUILDER >*************************************************************************** // **< Item BUILDER >***************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del elemento.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del elemento.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -108,7 +108,9 @@ impl Item {
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Field { pub struct Field {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del contenedor del grupo.
props: Props, props: Props,
/// Devuelve el nombre base compartido por todas las casillas del grupo. /// Devuelve el nombre base compartido por todas las casillas del grupo.
name: AttrName, name: AttrName,
@ -130,31 +132,21 @@ impl Component for Field {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
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>(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")); self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. let name = self
let name = self.name().get().unwrap(); .name()
let container_id = self.id().unwrap(); .get()
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 3));
let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
Ok(html! { Ok(html! {
div (self.props()) { div id=(&container_id) (self.props()) {
@if let Some(label) = self.label().lookup(cx) { @if let Some(label) = self.label().lookup(cx) {
label class="form-label" { (label) } label class="form-label" { (label) }
} }
@ -196,14 +188,14 @@ impl Component for Field {
impl Field { impl Field {
// **< Field BUILDER >************************************************************************** // **< Field BUILDER >**************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del grupo de casillas.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del contenedor del grupo de casillas.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -43,7 +43,9 @@ use crate::theme::form;
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Checkbox { pub struct Checkbox {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del contenedor del control.
props: Props, props: Props,
/// Devuelve la variante visual del control. /// Devuelve la variante visual del control.
checkbox_kind: form::CheckboxKind, checkbox_kind: form::CheckboxKind,
@ -71,21 +73,10 @@ impl Component for Checkbox {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
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>(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(); let mut classes = "form-field form-check".to_string();
if *self.checkbox_kind() == form::CheckboxKind::Switch { if *self.checkbox_kind() == form::CheckboxKind::Switch {
classes.push_str(" form-switch"); classes.push_str(" form-switch");
@ -100,15 +91,15 @@ impl Component for Checkbox {
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. let name = self
let name = self.name().get().unwrap(); .name()
let container_id = self.id().unwrap(); .get()
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 1));
let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
let checkbox_id = util::join!(&container_id, "-checkbox"); let checkbox_id = util::join!(&container_id, "-checkbox");
let is_switch = *self.checkbox_kind() == form::CheckboxKind::Switch; let is_switch = *self.checkbox_kind() == form::CheckboxKind::Switch;
Ok(html! { Ok(html! {
div (self.props()) { div id=(&container_id) (self.props()) {
input input
type="checkbox" type="checkbox"
role=[is_switch.then_some("switch")] role=[is_switch.then_some("switch")]
@ -154,14 +145,14 @@ impl Checkbox {
// **< Checkbox BUILDER >*********************************************************************** // **< Checkbox BUILDER >***********************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del control.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del contenedor del control.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -47,7 +47,9 @@ use crate::theme::form;
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Form { pub struct Form {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del formulario.
props: Props, props: Props,
/// Devuelve la URL/ruta de destino del formulario. /// Devuelve la URL/ruta de destino del formulario.
action: AttrValue, action: AttrValue,
@ -66,7 +68,7 @@ impl Component for Form {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
@ -80,6 +82,7 @@ impl Component for Form {
}; };
Ok(html! { Ok(html! {
form form
id=[self.id()]
(self.props()) (self.props())
action=[self.action().get()] action=[self.action().get()]
method=[method] method=[method]
@ -94,14 +97,14 @@ impl Component for Form {
impl Form { impl Form {
// **< Form BUILDER >*************************************************************************** // **< Form BUILDER >***************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del formulario.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del formulario.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -24,7 +24,9 @@ use pagetop::prelude::*;
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Fieldset { pub struct Fieldset {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del `fieldset`.
props: Props, props: Props,
/// Devuelve la leyenda del `fieldset`. /// Devuelve la leyenda del `fieldset`.
legend: Attr<L10n>, legend: Attr<L10n>,
@ -42,7 +44,7 @@ impl Component for Fieldset {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
@ -53,7 +55,7 @@ impl Component for Fieldset {
} }
Ok(html! { Ok(html! {
fieldset (self.props()) disabled[*self.disabled()] { fieldset id=[self.id()] (self.props()) disabled[*self.disabled()] {
@if let Some(legend) = self.legend().lookup(cx) { @if let Some(legend) = self.legend().lookup(cx) {
legend { (legend) } legend { (legend) }
} }
@ -69,14 +71,14 @@ impl Component for Fieldset {
impl Fieldset { impl Fieldset {
// **< Fieldset BUILDER >*********************************************************************** // **< Fieldset BUILDER >***********************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del `fieldset` (grupo de controles).
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del `fieldset`.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -126,7 +126,9 @@ impl fmt::Display for Mode {
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Field { pub struct Field {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del contenedor del campo.
props: Props, props: Props,
/// Devuelve el tipo de campo. /// Devuelve el tipo de campo.
kind: Kind, kind: Kind,
@ -168,18 +170,10 @@ impl Component for Field {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { 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() { if *self.floating_label() {
self.alter_prop(PropsOp::prepend_classes("form-floating")); self.alter_prop(PropsOp::prepend_classes("form-floating"));
} }
@ -190,7 +184,9 @@ impl Component for Field {
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id(); let container_id = self
.id()
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
let input_class = if *self.plaintext() { let input_class = if *self.plaintext() {
"form-control-plaintext" "form-control-plaintext"
@ -221,7 +217,7 @@ impl Component for Field {
None => html! {}, None => html! {},
}; };
Ok(html! { Ok(html! {
div (self.props()) { div id=[container_id.as_deref()] (self.props()) {
@if !*self.floating_label() { @if !*self.floating_label() {
(label) (label)
} }
@ -317,14 +313,14 @@ impl Field {
// **< Field BUILDER >************************************************************************** // **< Field BUILDER >**************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del contenedor del campo.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del contenedor del campo.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -96,7 +96,9 @@ impl Item {
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Field { pub struct Field {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del contenedor del grupo.
props: Props, props: Props,
/// Devuelve el nombre compartido por todos los botones de opción del grupo. /// Devuelve el nombre compartido por todos los botones de opción del grupo.
name: AttrName, name: AttrName,
@ -120,31 +122,21 @@ impl Component for Field {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
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>(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")); self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. let name = self
let name = self.name().get().unwrap(); .name()
let container_id = self.id().unwrap(); .get()
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 3));
let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
Ok(html! { Ok(html! {
div (self.props()) { div id=(&container_id) (self.props()) {
@if let Some(label) = self.label().lookup(cx) { @if let Some(label) = self.label().lookup(cx) {
label class="form-label" { label class="form-label" {
(label) (label)
@ -198,14 +190,14 @@ impl Component for Field {
impl Field { impl Field {
// **< Field BUILDER >************************************************************************** // **< Field BUILDER >**************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del grupo de opciones.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del contenedor del grupo de opciones.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -31,7 +31,9 @@ use pagetop::prelude::*;
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Range { pub struct Range {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del contenedor del control deslizante.
props: Props, props: Props,
/// Devuelve el nombre del campo. /// Devuelve el nombre del campo.
name: AttrName, name: AttrName,
@ -59,26 +61,20 @@ impl Component for Range {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { 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")); self.alter_prop(PropsOp::prepend_classes("form-field form-field-range"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id(); let container_id = self
.id()
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
let range_id = container_id.as_deref().map(|id| util::join!(id, "-range")); let range_id = container_id.as_deref().map(|id| util::join!(id, "-range"));
Ok(html! { Ok(html! {
div (self.props()) { div id=[container_id.as_deref()] (self.props()) {
@if let Some(label) = self.label().lookup(cx) { @if let Some(label) = self.label().lookup(cx) {
label for=[range_id.as_deref()] class="form-label" { (label) } label for=[range_id.as_deref()] class="form-label" { (label) }
} }
@ -104,14 +100,14 @@ impl Component for Range {
impl Range { impl Range {
// **< Range BUILDER >************************************************************************** // **< Range BUILDER >**************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del contenedor del control deslizante.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del contenedor del control deslizante.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -191,7 +191,9 @@ pub enum Entry {
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Field { pub struct Field {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del contenedor de la lista de selección.
props: Props, props: Props,
/// Devuelve el nombre del campo. /// Devuelve el nombre del campo.
name: AttrName, name: AttrName,
@ -223,18 +225,10 @@ impl Component for Field {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { 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() { if *self.floating_label() {
self.alter_multiple(false); self.alter_multiple(false);
self.alter_rows(None::<u16>); self.alter_rows(None::<u16>);
@ -244,7 +238,9 @@ impl Component for Field {
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id(); let container_id = self
.id()
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
let select_id = container_id.as_deref().map(|id| util::join!(id, "-select")); let select_id = container_id.as_deref().map(|id| util::join!(id, "-select"));
let label = match self.label().lookup(cx) { let label = match self.label().lookup(cx) {
Some(text) => html! { Some(text) => html! {
@ -263,7 +259,7 @@ impl Component for Field {
None => html! {}, None => html! {},
}; };
Ok(html! { Ok(html! {
div (self.props()) { div id=[container_id.as_deref()] (self.props()) {
@if !*self.floating_label() { @if !*self.floating_label() {
(label) (label)
} }
@ -322,14 +318,14 @@ impl Component for Field {
impl Field { impl Field {
// **< Field BUILDER >*************************************************************************** // **< Field BUILDER >***************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del contenedor del campo.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del contenedor de la lista de selección.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -34,7 +34,9 @@ use crate::theme::form;
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Textarea { pub struct Textarea {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del contenedor del área de texto.
props: Props, props: Props,
/// Devuelve el nombre del campo. /// Devuelve el nombre del campo.
name: AttrName, name: AttrName,
@ -72,18 +74,10 @@ impl Component for Textarea {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { 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() { if *self.floating_label() {
self.alter_rows(None::<u16>); self.alter_rows(None::<u16>);
self.alter_prop(PropsOp::prepend_classes("form-floating")); self.alter_prop(PropsOp::prepend_classes("form-floating"));
@ -92,7 +86,9 @@ impl Component for Textarea {
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id(); let container_id = self
.id()
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
let textarea_id = container_id let textarea_id = container_id
.as_deref() .as_deref()
.map(|id| util::join!(id, "-textarea")); .map(|id| util::join!(id, "-textarea"));
@ -120,7 +116,7 @@ impl Component for Textarea {
None => html! {}, None => html! {},
}; };
Ok(html! { Ok(html! {
div (self.props()) { div id=[container_id.as_deref()] (self.props()) {
@if !*self.floating_label() { @if !*self.floating_label() {
(label) (label)
} }
@ -156,14 +152,14 @@ impl Component for Textarea {
impl Textarea { impl Textarea {
// **< Textarea BUILDER >*********************************************************************** // **< Textarea BUILDER >***********************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del contenedor del campo.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del contenedor del campo.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -15,7 +15,7 @@ pub enum IconKind {
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Icon { pub struct Icon {
/// Devuelve los atributos HTML y clases CSS del componente. /// Devuelve los atributos HTML y clases CSS del icono.
props: Props, props: Props,
icon_kind: IconKind, icon_kind: IconKind,
aria_label: AttrL10n, aria_label: AttrL10n,
@ -26,10 +26,6 @@ impl Component for Icon {
Self::default() Self::default()
} }
fn id(&self) -> Option<String> {
self.props.get_id()
}
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
if !matches!(self.icon_kind(), IconKind::None) { if !matches!(self.icon_kind(), IconKind::None) {
self.alter_prop(PropsOp::prepend_classes("icon")); self.alter_prop(PropsOp::prepend_classes("icon"));
@ -102,14 +98,7 @@ impl Icon {
// **< Icon BUILDER >*************************************************************************** // **< Icon BUILDER >***************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Modifica los atributos HTML o las clases CSS del icono.
#[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
self.props.alter_id(id);
self
}
/// Modifica identificador, clases CSS o atributos HTML del componente.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -13,7 +13,9 @@ use crate::theme::*;
/// - Aplicar el texto alternativo `alt` con **localización** mediante [`L10n`]. /// - Aplicar el texto alternativo `alt` con **localización** mediante [`L10n`].
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Image { pub struct Image {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS de la imagen.
props: Props, props: Props,
/// Devuelve las dimensiones de la imagen. /// Devuelve las dimensiones de la imagen.
size: image::Size, size: image::Size,
@ -29,11 +31,10 @@ impl Component for Image {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { 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())); self.alter_prop(PropsOp::prepend_classes(self.source().to_class()));
} }
@ -45,6 +46,7 @@ impl Component for Image {
image::Source::Logo(logo) => { image::Source::Logo(logo) => {
return Ok(html! { return Ok(html! {
span span
id=[self.id()]
(self.props()) (self.props())
style=[dimensions] style=[dimensions]
role=[(!is_decorative).then_some("img")] role=[(!is_decorative).then_some("img")]
@ -63,6 +65,7 @@ impl Component for Image {
img img
src=[source] src=[source]
alt=(alt_text) alt=(alt_text)
id=[self.id()]
(self.props()) (self.props())
style=[dimensions] {} style=[dimensions] {}
}) })
@ -77,14 +80,14 @@ impl Image {
// **< Image BUILDER >************************************************************************** // **< Image BUILDER >**************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) de la imagen.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS de la imagen.
/// ///
/// También acepta clases predefinidas para: /// También acepta clases predefinidas para:
/// ///

View file

@ -32,7 +32,9 @@ use crate::theme::*;
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Nav { pub struct Nav {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del menú.
props: Props, props: Props,
/// Devuelve el estilo visual seleccionado. /// Devuelve el estilo visual seleccionado.
nav_kind: nav::Kind, nav_kind: nav::Kind,
@ -48,11 +50,10 @@ impl Component for Nav {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { 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({ self.alter_prop(PropsOp::prepend_classes({
let mut classes = "nav".to_string(); let mut classes = "nav".to_string();
self.nav_kind().push_class(&mut classes); self.nav_kind().push_class(&mut classes);
@ -68,7 +69,7 @@ impl Component for Nav {
} }
Ok(html! { Ok(html! {
ul (self.props()) { ul id=[self.id()] (self.props()) {
(items) (items)
} }
}) })
@ -93,14 +94,14 @@ impl Nav {
// **< Nav BUILDER >**************************************************************************** // **< Nav BUILDER >****************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del menú.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del menú.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -78,7 +78,9 @@ impl ItemKind {
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Item { pub struct Item {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del elemento.
props: Props, props: Props,
/// Devuelve el tipo de elemento representado. /// Devuelve el tipo de elemento representado.
item_kind: ItemKind, item_kind: ItemKind,
@ -90,7 +92,7 @@ impl Component for Item {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
@ -102,7 +104,7 @@ impl Component for Item {
ItemKind::Void => html! {}, ItemKind::Void => html! {},
ItemKind::Label(label) => html! { ItemKind::Label(label) => html! {
li (self.props()) { li id=[self.id()] (self.props()) {
span class="nav-link disabled" aria-disabled="true" { span class="nav-link disabled" aria-disabled="true" {
(label.using(cx)) (label.using(cx))
} }
@ -135,7 +137,7 @@ impl Component for Item {
let aria_disabled = (*disabled).then_some("true"); let aria_disabled = (*disabled).then_some("true");
html! { html! {
li (self.props()) { li id=[self.id()] (self.props()) {
a a
class=(classes) class=(classes)
href=[href] href=[href]
@ -151,7 +153,7 @@ impl Component for Item {
} }
ItemKind::Html(html) => html! { ItemKind::Html(html) => html! {
li (self.props()) { li id=[self.id()] (self.props()) {
(html.render(cx)) (html.render(cx))
} }
}, },
@ -168,7 +170,7 @@ impl Component for Item {
.unwrap_or_else(|| "Dropdown".to_string()) .unwrap_or_else(|| "Dropdown".to_string())
}); });
html! { html! {
li (self.props()) { li id=[self.id()] (self.props()) {
a a
class="nav-link dropdown-toggle" class="nav-link dropdown-toggle"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
@ -281,14 +283,14 @@ impl Item {
// **< Item BUILDER >*************************************************************************** // **< Item BUILDER >***************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del elemento.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del elemento.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -13,6 +13,8 @@ use crate::theme::*;
/// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido. /// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido.
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Brand { pub struct Brand {
#[getters(skip)]
id: AttrId,
/// Devuelve la imagen de marca (si la hay). /// Devuelve la imagen de marca (si la hay).
image: Embed<Image>, image: Embed<Image>,
/// Devuelve el título de la identidad de marca. /// Devuelve el título de la identidad de marca.
@ -30,6 +32,10 @@ impl Component for Brand {
Self::default() Self::default()
} }
fn id(&self) -> Option<String> {
self.id.get()
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let image = self.image().render(cx); let image = self.image().render(cx);
let title = self.title().using(cx); let title = self.title().using(cx);
@ -50,6 +56,13 @@ impl Component for Brand {
impl Brand { impl Brand {
// **< Brand BUILDER >************************************************************************** // **< Brand BUILDER >**************************************************************************
/// Establece el identificador único (`id`) de la marca.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_id(id);
self
}
/// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará. /// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará.
#[builder_fn] #[builder_fn]
pub fn with_image(mut self, image: Option<Image>) -> Self { pub fn with_image(mut self, image: Option<Image>) -> Self {

View file

@ -136,7 +136,9 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Navbar { pub struct Navbar {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS de la barra de navegación.
props: Props, props: Props,
/// Devuelve el punto de ruptura configurado. /// Devuelve el punto de ruptura configurado.
expand: BreakPoint, expand: BreakPoint,
@ -154,14 +156,10 @@ impl Component for Navbar {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
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::<Self>(1)));
// Clases CSS por defecto para la barra de navegación.
self.alter_prop(PropsOp::prepend_classes({ self.alter_prop(PropsOp::prepend_classes({
let mut classes = "navbar".to_string(); let mut classes = "navbar".to_string();
self.expand().push_class(&mut classes, "navbar-expand", ""); self.expand().push_class(&mut classes, "navbar-expand", "");
@ -200,11 +198,11 @@ impl Component for Navbar {
return Ok(html! {}); return Ok(html! {});
} }
// `setup()` garantiza que habrá un `id` antes de renderizar. // Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas.
let id = self.id().unwrap(); let id = cx.required_id::<Self>(self.id(), 1);
Ok(html! { Ok(html! {
nav (self.props()) { nav id=(&id) (self.props()) {
div class="container-fluid" { div class="container-fluid" {
@match self.layout() { @match self.layout() {
// Barra más sencilla: sólo contenido. // Barra más sencilla: sólo contenido.
@ -337,14 +335,14 @@ impl Navbar {
// **< Navbar BUILDER >************************************************************************* // **< Navbar BUILDER >*************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) de la barra de navegación.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS de la barra de navegación.
/// ///
/// También acepta clases predefinidas para: /// También acepta clases predefinidas para:
/// ///

View file

@ -43,7 +43,9 @@ use crate::theme::*;
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Offcanvas { pub struct Offcanvas {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del panel.
props: Props, props: Props,
/// Devuelve el título del panel. /// Devuelve el título del panel.
title: L10n, title: L10n,
@ -67,14 +69,10 @@ impl Component for Offcanvas {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
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::<Self>(1)));
// Clases CSS por defecto para el panel.
self.alter_prop(PropsOp::prepend_classes({ self.alter_prop(PropsOp::prepend_classes({
let mut classes = "offcanvas".to_string(); let mut classes = "offcanvas".to_string();
self.breakpoint().push_class(&mut classes, "offcanvas", ""); self.breakpoint().push_class(&mut classes, "offcanvas", "");
@ -92,14 +90,14 @@ impl Component for Offcanvas {
impl Offcanvas { impl Offcanvas {
// **< Offcanvas BUILDER >********************************************************************** // **< Offcanvas BUILDER >**********************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del panel.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del panel.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);
@ -174,8 +172,7 @@ impl Offcanvas {
return html! {}; return html! {};
} }
// `setup()` garantiza que habrá un `id` antes de renderizar. let id = cx.required_id::<Self>(self.id(), 1);
let id = self.id().unwrap();
let id_label = util::join!(id, "-label"); let id_label = util::join!(id, "-label");
let id_target = util::join!("#", id); let id_target = util::join!("#", id);
@ -194,6 +191,7 @@ impl Offcanvas {
html! { html! {
div div
id=(&id)
(self.props()) (self.props())
tabindex="-1" tabindex="-1"
data-bs-scroll=[body_scroll] data-bs-scroll=[body_scroll]

View file

@ -6,7 +6,7 @@ use super::FnActionWithComponent;
pub struct AfterRender<C: Component> { pub struct AfterRender<C: Component> {
f: FnActionWithComponent<C>, f: FnActionWithComponent<C>,
referer_type_id: Option<UniqueId>, referer_type_id: Option<UniqueId>,
referer_id: Option<String>, referer_id: AttrId,
weight: Weight, weight: Weight,
} }
@ -19,7 +19,7 @@ impl<C: Component> ActionDispatcher for AfterRender<C> {
/// Devuelve el identificador del componente. /// Devuelve el identificador del componente.
fn referer_id(&self) -> Option<String> { fn referer_id(&self) -> Option<String> {
self.referer_id.clone() self.referer_id.get()
} }
/// Devuelve el peso para definir el orden de ejecución. /// Devuelve el peso para definir el orden de ejecución.
@ -34,7 +34,7 @@ impl<C: Component> AfterRender<C> {
AfterRender { AfterRender {
f, f,
referer_type_id: Some(UniqueId::of::<C>()), referer_type_id: Some(UniqueId::of::<C>()),
referer_id: None, referer_id: AttrId::default(),
weight: 0, weight: 0,
} }
} }
@ -42,8 +42,7 @@ impl<C: Component> AfterRender<C> {
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
/// `C` con identificador `id`. /// `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self { pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); self.referer_id.alter_id(id);
self.referer_id = if id.is_empty() { None } else { Some(id) };
self self
} }

View file

@ -6,7 +6,7 @@ use super::FnActionWithComponent;
pub struct BeforeRender<C: Component> { pub struct BeforeRender<C: Component> {
f: FnActionWithComponent<C>, f: FnActionWithComponent<C>,
referer_type_id: Option<UniqueId>, referer_type_id: Option<UniqueId>,
referer_id: Option<String>, referer_id: AttrId,
weight: Weight, weight: Weight,
} }
@ -19,7 +19,7 @@ impl<C: Component> ActionDispatcher for BeforeRender<C> {
/// Devuelve el identificador del componente. /// Devuelve el identificador del componente.
fn referer_id(&self) -> Option<String> { fn referer_id(&self) -> Option<String> {
self.referer_id.clone() self.referer_id.get()
} }
/// Devuelve el peso para definir el orden de ejecución. /// Devuelve el peso para definir el orden de ejecución.
@ -34,7 +34,7 @@ impl<C: Component> BeforeRender<C> {
BeforeRender { BeforeRender {
f, f,
referer_type_id: Some(UniqueId::of::<C>()), referer_type_id: Some(UniqueId::of::<C>()),
referer_id: None, referer_id: AttrId::default(),
weight: 0, weight: 0,
} }
} }
@ -42,8 +42,7 @@ impl<C: Component> BeforeRender<C> {
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
/// `C` con identificador `id`. /// `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self { pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); self.referer_id.alter_id(id);
self.referer_id = if id.is_empty() { None } else { Some(id) };
self self
} }

View file

@ -6,7 +6,7 @@ use super::FnActionTransformMarkup;
pub struct TransformMarkup<C: Component> { pub struct TransformMarkup<C: Component> {
f: FnActionTransformMarkup<C>, f: FnActionTransformMarkup<C>,
referer_type_id: Option<UniqueId>, referer_type_id: Option<UniqueId>,
referer_id: Option<String>, referer_id: AttrId,
weight: Weight, weight: Weight,
} }
@ -19,7 +19,7 @@ impl<C: Component> ActionDispatcher for TransformMarkup<C> {
/// Devuelve el identificador del componente. /// Devuelve el identificador del componente.
fn referer_id(&self) -> Option<String> { fn referer_id(&self) -> Option<String> {
self.referer_id.clone() self.referer_id.get()
} }
/// Devuelve el peso para definir el orden de ejecución. /// Devuelve el peso para definir el orden de ejecución.
@ -34,7 +34,7 @@ impl<C: Component> TransformMarkup<C> {
TransformMarkup { TransformMarkup {
f, f,
referer_type_id: Some(UniqueId::of::<C>()), referer_type_id: Some(UniqueId::of::<C>()),
referer_id: None, referer_id: AttrId::default(),
weight: 0, weight: 0,
} }
} }
@ -42,8 +42,7 @@ impl<C: Component> TransformMarkup<C> {
/// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el /// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el
/// componente `C` con identificador `id`. /// componente `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self { pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); self.referer_id.alter_id(id);
self.referer_id = if id.is_empty() { None } else { Some(id) };
self self
} }

View file

@ -6,7 +6,9 @@ use crate::prelude::*;
/// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*). /// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*).
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Block { pub struct Block {
/// Devuelve identificador, clases CSS y atributos HTML del componente. #[getters(skip)]
id: AttrId,
/// Devuelve los atributos HTML y clases CSS del bloque.
props: Props, props: Props,
/// Devuelve el título del bloque. /// Devuelve el título del bloque.
title: L10n, title: L10n,
@ -20,15 +22,11 @@ impl Component for Block {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn setup(&mut self, cx: &Context) { fn setup(&mut self, _cx: &Context) {
// Asegura que el bloque tiene un identificador único. self.props.alter_prop(PropsOp::prepend_classes("block"));
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(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<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
@ -38,12 +36,14 @@ impl Component for Block {
return Ok(html! {}); return Ok(html! {});
} }
let id = cx.required_id::<Self>(self.id(), 1);
Ok(html! { Ok(html! {
div (self.props()) { div id=(&id) (self.props()) {
@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,14 +52,14 @@ impl Component for Block {
impl Block { impl Block {
// **< Block BUILDER >************************************************************************** // **< Block BUILDER >**************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. /// Establece el identificador único (`id`) del bloque.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.props.alter_id(id); self.id.alter_id(id);
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del componente. /// Modifica los atributos HTML o las clases CSS del bloque.
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op); self.props.alter_prop(op);

View file

@ -9,8 +9,8 @@ use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
use crate::web::HttpRequest; use crate::web::HttpRequest;
use crate::{CowStr, builder_fn, util}; use crate::{CowStr, builder_fn, util};
use std::any::{Any, TypeId}; use std::any::Any;
use std::cell::RefCell; use std::cell::Cell;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
@ -271,7 +271,7 @@ pub trait Contextual: LangId {
/// assert_eq!(id, 42); /// assert_eq!(id, 42);
/// ///
/// // Genera un identificador para un componente de tipo `Menu`. /// // Genera un identificador para un componente de tipo `Menu`.
/// let unique_id = cx.build_id::<Menu>(1); /// let unique_id = cx.required_id::<Menu>(None, 1);
/// assert_eq!(unique_id, "menu-1"); // Si es el primero generado. /// assert_eq!(unique_id, "menu-1"); // Si es el primero generado.
/// } /// }
/// ``` /// ```
@ -286,7 +286,7 @@ pub struct Context {
javascripts: Assets<JavaScript>, // Scripts JavaScript. javascripts: Assets<JavaScript>, // Scripts JavaScript.
regions : ChildrenInRegions, // Regiones de componentes para renderizar. regions : ChildrenInRegions, // Regiones de componentes para renderizar.
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución. params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
id_counters: RefCell<HashMap<TypeId, usize>>, // RefCell permite mutar desde build_id(&self). id_counter : Cell<usize>, // Cell permite incrementar desde &self en required_id().
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados. messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
} }
@ -314,7 +314,7 @@ impl Context {
javascripts: Assets::<JavaScript>::new(), javascripts: Assets::<JavaScript>::new(),
regions : ChildrenInRegions::default(), regions : ChildrenInRegions::default(),
params : HashMap::default(), params : HashMap::default(),
id_counters: RefCell::new(HashMap::new()), id_counter : Cell::new(0),
messages : Vec::new(), messages : Vec::new(),
} }
} }
@ -374,42 +374,31 @@ impl Context {
route route
} }
/// Construye un identificador HTML único para el tipo de componente `C`. /// Garantiza un identificador único para un componente `C`, generándolo si no se proporciona
/// ninguno.
/// ///
/// Toma los `segments` finales del *path* completo del tipo, los une con `-` y los convierte a /// Si `id` es `None`, crea un identificador usando los últimos segmentos del *path* completo
/// minúsculas, y añade un contador independiente por tipo. Por ejemplo, para `MyApp::ui::Menu` /// del tipo `C`, separados por `-` y en minúsculas, seguidos de un contador incremental interno
/// con `segments = 2` devuelve `ui-menu-1` la primera vez que se invoca para ese tipo, /// del contexto. Por ejemplo, para un componente `MyApp::ui::Menu` con `parts = 2` podría
/// `ui-menu-2` la segunda, etc. /// devolver un identificador como `ui-menu-1` si ha sido el primero en generarse.
/// ///
/// Con `segments = 1` se usa sólo el nombre corto del tipo. Si `segments` es `0` o supera el /// Con `parts = 1` se usa el nombre corto del tipo. Si `parts` es `0` o supera el número de
/// número de segmentos del *path*, se usan todos. /// segmentos del *path*, entonces se usará el *path* completo.
/// ///
/// Es útil para asignar identificadores cuando el componente no recibe uno explícito. El /// Es útil para asignar identificadores HTML cuando el componente no recibe uno explícito.
/// contador es local a este contexto y se reinicia para cada nueva petición. pub fn required_id<C: Component>(&self, id: Option<String>, parts: usize) -> String {
pub fn build_id<C: Component>(&self, segments: usize) -> String { if let Some(id) = id {
let path: Vec<&str> = TypeInfo::FullName.of::<C>().split("::").collect(); return id;
let segments = if segments == 0 || segments >= path.len() { }
path.len() let segments: Vec<&str> = TypeInfo::FullName.of::<C>().split("::").collect();
let parts = if parts == 0 || parts >= segments.len() {
segments.len()
} else { } else {
segments parts
}; };
let count = { self.id_counter.set(self.id_counter.get() + 1);
let mut map = self.id_counters.borrow_mut(); let prefix = segments[segments.len() - parts..].join("-").to_lowercase();
let n = map.entry(TypeId::of::<C>()).or_insert(0); util::join!(prefix, "-", self.id_counter.get().to_string())
*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<C: Component>(&self, id: Option<String>, segments: usize) -> String {
match id {
Some(id) => id,
None => self.build_id::<C>(segments),
}
} }
/// Acumula un [`StatusMessage`] en el contexto para notificar al visitante. /// Acumula un [`StatusMessage`] en el contexto para notificar al visitante.

View file

@ -20,7 +20,7 @@ pub use logo::PageTopSvg;
// **< HTML ATTRIBUTES >**************************************************************************** // **< HTML ATTRIBUTES >****************************************************************************
mod attr; mod attr;
pub use attr::{Attr, AttrName, AttrValue}; pub use attr::{Attr, AttrId, AttrName, AttrValue};
mod props; mod props;
pub use props::{Props, PropsOp}; pub use props::{Props, PropsOp};

View file

@ -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 /// Este tipo **no impone ninguna normalización ni semántica concreta**; dichas reglas se definen en
/// implementaciones concretas como `Attr<L10n>` y `Attr<String>`, o en tipos específicos como /// implementaciones concretas como `Attr<L10n>` y `Attr<String>`, o en tipos específicos como
/// [`AttrName`]. /// [`AttrId`] y [`AttrName`].
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]
pub struct Attr<T>(Option<T>); pub struct Attr<T>(Option<T>);
@ -128,6 +128,73 @@ impl Attr<String> {
} }
} }
// **< AttrId >*************************************************************************************
/// Identificador normalizado para el atributo `id` o similar de HTML.
///
/// Este tipo encapsula `Option<String>` 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<String>);
impl AttrId {
/// Crea un nuevo `AttrId` normalizando el valor.
pub fn new(id: impl AsRef<str>) -> 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<str>) -> 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<String> {
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<String> {
self.0.into_inner()
}
/// `true` si no hay valor.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
// **< AttrName >*********************************************************************************** // **< AttrName >***********************************************************************************
/// Nombre normalizado para el atributo `name` o similar de HTML. /// Nombre normalizado para el atributo `name` o similar de HTML.

View file

@ -7,57 +7,32 @@ use std::fmt::Write;
/// Operaciones disponibles sobre atributos HTML y clases CSS en [`Props`]. /// Operaciones disponibles sobre atributos HTML y clases CSS en [`Props`].
/// ///
/// Cada variante lleva los datos necesarios para ejecutarse. El método recomendado para usarlas es /// Cada variante es autocontenida, lleva todos los datos que necesita para ejecutarse. El método
/// recurrir a los constructores asociados como [`set()`](Self::set), [`set_id()`](Self::set_id), /// recomendado para construirlas es usar los constructores asociados ([`set()`](Self::set),
/// [`remove()`](Self::remove), [`add_classes()`](Self::add_classes), etc. /// [`remove()`](Self::remove), [`add_classes()`](Self::add_classes), etc.).
/// ///
/// Las variantes `*Id` operan sobre el atributo `id` del componente. Cuando se usa `"id"` como /// Las variantes `*Classes` operan siempre sobre la lista de clases CSS para el componente.
/// nombre de atributo en `Set`, el valor se normaliza igual que en `SetId` o `EnsureId`.
/// ///
/// Las variantes `*Classes` operan siempre sobre la lista de clases CSS para el componente. Cuando /// Cuando se usa `"class"` como nombre de atributo en `Set` o `Remove` la operación se aplica a la
/// se usa `"class"` como nombre en `Set` o `Remove` la operación se aplica a la lista de clases /// lista de clases completa. Así, `Set("class", ...)` reemplaza la lista de clases completa por las
/// completa. Así, `Set("class", ...)` reemplaza la lista de clases completa por las nuevas clases /// nuevas clases indicadas, y `Remove("class")` vacía la lista de clases.
/// indicadas, y `Remove("class")` vacía la lista de clases.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum PropsOp { pub enum PropsOp {
/// Establece el identificador del elemento normalizando el valor: recorta espacios, convierte a /// Añade el atributo o sustituye su valor si ya existe. Usar `"class"` como nombre reemplaza la
/// minúsculas y sustituye los espacios intermedios por `_`. Si el resultado es vacío, elimina /// lista completa de clases por las nuevas indicadas; la operación se ignora si el valor
/// el identificador. /// contiene caracteres no ASCII.
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), Set(CowStr, CowStr),
/// Elimina el atributo indicado, incluido `"id"`. Si se usa `"class"` como nombre se vacía la /// Elimina el atributo indicado. Usar `"class"` como nombre vacía la lista de clases.
/// lista de clases.
Remove(CowStr), Remove(CowStr),
/// Añade las clases que no existan al final de la lista. La operación se ignora si el valor /// Añade las clases que no existan al final de la lista.
/// contiene caracteres no ASCII.
AddClasses(CowStr), AddClasses(CowStr),
/// Añade las clases que no existan al principio de la lista. La operación se ignora si el valor /// Añade las clases que no existan al principio de la lista.
/// contiene caracteres no ASCII.
PrependClasses(CowStr), PrependClasses(CowStr),
/// Elimina las clases indicadas de la lista. La operación se ignora si el valor contiene /// Elimina las clases indicadas de la lista.
/// caracteres no ASCII.
RemoveClasses(CowStr), RemoveClasses(CowStr),
} }
impl PropsOp { impl PropsOp {
/// Crea la variante [`SetId`](Self::SetId) con el identificador indicado.
pub fn set_id(id: impl Into<CowStr>) -> Self {
Self::SetId(id.into())
}
/// Crea la variante [`EnsureId`](Self::EnsureId) con el identificador indicado.
pub fn ensure_id(id: impl Into<CowStr>) -> Self {
Self::EnsureId(id.into())
}
/// Crea la variante [`Set`](Self::Set) con nombre y valor del atributo. /// Crea la variante [`Set`](Self::Set) con nombre y valor del atributo.
pub fn set(name: impl Into<CowStr>, value: impl Into<CowStr>) -> Self { pub fn set(name: impl Into<CowStr>, value: impl Into<CowStr>) -> Self {
Self::Set(name.into(), value.into()) Self::Set(name.into(), value.into())
@ -86,10 +61,11 @@ impl PropsOp {
// **< Props >************************************************************************************** // **< Props >**************************************************************************************
/// Colección de identificador, atributos HTML y clases CSS para aplicar en componentes. /// Colección de pares `atributo="valor"` y clases CSS para aplicar en componentes.
/// ///
/// Al renderizar en `html!` emite primero `id` (si existe), luego `class` (si hay clases) y después /// Permite añadir dinámicamente pares `atributo="valor"` y clases CSS al elemento raíz de un
/// el resto de atributos. /// componente. Al renderizar los atributos en `html!` primero emite el atributo `class` (si hay
/// clases) y luego el resto de atributos.
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
@ -109,35 +85,6 @@ 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#"<button id="my_button">OK</button>"#);
/// ```
///
/// [`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 /// # Clases CSS
/// ///
/// ```rust /// ```rust
@ -175,7 +122,7 @@ impl PropsOp {
/// } /// }
/// ///
/// impl MyButton { /// impl MyButton {
/// /// Modifica identificador, clases CSS o atributos HTML del elemento raíz. /// /// Modifica los atributos HTML o las clases CSS del elemento raíz.
/// #[builder_fn] /// #[builder_fn]
/// pub fn with_prop(mut self, op: PropsOp) -> Self { /// pub fn with_prop(mut self, op: PropsOp) -> Self {
/// self.props.alter_prop(op); /// self.props.alter_prop(op);
@ -185,7 +132,6 @@ impl PropsOp {
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]
pub struct Props { pub struct Props {
id: Option<String>,
attrs: Vec<(CowStr, CowStr)>, attrs: Vec<(CowStr, CowStr)>,
classes: Vec<String>, classes: Vec<String>,
} }
@ -203,23 +149,12 @@ impl Props {
// **< Props BUILDER >************************************************************************** // **< Props BUILDER >**************************************************************************
/// Establece el identificador del componente; equivale a `with_prop(PropsOp::set_id(id))`. /// Modifica los atributos o clases según la operación indicada.
#[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> 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(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. /// `Set("class", ...)` reemplaza la lista de clases completa.
/// - [`Remove(name)`](PropsOp::Remove) elimina el atributo. `Remove("id")` elimina el /// - [`Remove(name)`](PropsOp::Remove) elimina el atributo. `Remove("class")` vacía la lista de
/// identificador. `Remove("class")` vacía la lista de clases. /// clases.
/// - [`AddClasses(clases)`](PropsOp::AddClasses) añade clases al final (sin duplicados). /// - [`AddClasses(clases)`](PropsOp::AddClasses) añade clases al final (sin duplicados).
/// - [`PrependClasses(clases)`](PropsOp::PrependClasses) añade clases al principio (sin /// - [`PrependClasses(clases)`](PropsOp::PrependClasses) añade clases al principio (sin
/// duplicados). /// duplicados).
@ -227,18 +162,8 @@ impl Props {
#[builder_fn] #[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self { pub fn with_prop(mut self, op: PropsOp) -> Self {
match op { 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) => { PropsOp::Set(name, value) => {
if name.as_ref() == "id" { if name.as_ref() == "class" {
self.apply_id(value.as_ref());
} else if name.as_ref() == "class" {
if let Some(normalized) = if let Some(normalized) =
util::normalize_ascii_or_empty(value.as_ref(), "Props::with_prop") util::normalize_ascii_or_empty(value.as_ref(), "Props::with_prop")
{ {
@ -252,9 +177,7 @@ impl Props {
} }
} }
PropsOp::Remove(name) => { PropsOp::Remove(name) => {
if name.as_ref() == "id" { if name.as_ref() == "class" {
self.id = None;
} else if name.as_ref() == "class" {
self.classes.clear(); self.classes.clear();
} else { } else {
self.attrs.retain(|(k, _)| k != &name); self.attrs.retain(|(k, _)| k != &name);
@ -296,26 +219,18 @@ impl Props {
// **< Props GETTERS >************************************************************************** // **< Props GETTERS >**************************************************************************
/// Devuelve el identificador normalizado del elemento, si existe.
#[inline]
pub fn get_id(&self) -> Option<String> {
self.id.clone()
}
/// Devuelve el valor del atributo indicado, si existe. /// Devuelve el valor del atributo indicado, si existe.
/// pub fn get_prop(&self, name: impl AsRef<str>) -> Option<&str> {
/// Los nombres `"id"` y `"class"` son equivalentes a llamar a [`get_id()`](Self::get_id) y let name = name.as_ref();
/// [`get_classes()`](Self::get_classes) respectivamente. self.attrs
pub fn get_prop(&self, name: impl AsRef<str>) -> Option<String> {
match name.as_ref() {
"id" => self.id.clone(),
"class" => self.get_classes(),
name => self
.attrs
.iter() .iter()
.find(|(k, _)| k.as_ref() == name) .find(|(k, _)| k.as_ref() == name)
.map(|(_, v)| v.to_string()), .map(|(_, v)| v.as_ref())
} }
/// Devuelve `true` si no hay ningún atributo definido.
pub fn is_props_empty(&self) -> bool {
self.attrs.is_empty()
} }
/// Devuelve la lista de clases como cadena de texto, si hay clases definidas. /// Devuelve la lista de clases como cadena de texto, si hay clases definidas.
@ -327,31 +242,11 @@ 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. /// Devuelve `true` si no hay ninguna clase definida.
#[inline]
pub fn is_classes_empty(&self) -> bool { pub fn is_classes_empty(&self) -> bool {
self.classes.is_empty() 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. /// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
pub fn has_class(&self, classes: impl AsRef<str>) -> bool { pub fn has_class(&self, classes: impl AsRef<str>) -> bool {
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else { let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
@ -374,17 +269,9 @@ impl Props {
.any(|class| self.classes.iter().any(|c| c == class)) .any(|class| self.classes.iter().any(|c| c == class))
} }
// **< Props PRIVATE >************************************************************************** // **< Props PRIVADO >**************************************************************************
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) fn insert_classes<'a, I>(&mut self, classes: I, mut pos: usize)
where where
I: IntoIterator<Item = &'a str>, I: IntoIterator<Item = &'a str>,
@ -406,11 +293,6 @@ impl Props {
#[doc(hidden)] #[doc(hidden)]
impl Render for Props { impl Render for Props {
fn render_to(&self, w: &mut String) { 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() { if let Some((first, rest)) = self.classes.split_first() {
w.push_str(" class=\""); w.push_str(" class=\"");
let _ = write!(Escaper::new(w), "{}", first); let _ = write!(Escaper::new(w), "{}", first);

View file

@ -20,7 +20,7 @@ use crate::base::action;
use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual}; use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual};
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef}; use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::html::{Attr, Props, PropsOp}; use crate::html::{Attr, AttrId, Props, PropsOp};
use crate::html::{DOCTYPE, Markup, html}; use crate::html::{DOCTYPE, Markup, html};
use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
use crate::web::HttpRequest; use crate::web::HttpRequest;
@ -89,6 +89,7 @@ pub struct Page {
description : Attr<L10n>, description : Attr<L10n>,
metadata : Vec<(&'static str, &'static str)>, metadata : Vec<(&'static str, &'static str)>,
properties : Vec<(&'static str, &'static str)>, properties : Vec<(&'static str, &'static str)>,
body_id : AttrId,
body_props : Props, body_props : Props,
context : Context, context : Context,
} }
@ -105,6 +106,7 @@ impl Page {
description : Attr::<L10n>::default(), description : Attr::<L10n>::default(),
metadata : Vec::default(), metadata : Vec::default(),
properties : Vec::default(), properties : Vec::default(),
body_id : AttrId::default(),
body_props : Props::default(), body_props : Props::default(),
context : Context::new(Some(request)), context : Context::new(Some(request)),
} }
@ -140,7 +142,14 @@ impl Page {
self self
} }
/// Modifica identificador, clases CSS o atributos HTML del elemento `<body>`. /// Establece el atributo `id` del elemento `<body>`.
#[builder_fn]
pub fn with_body_id(mut self, id: impl AsRef<str>) -> Self {
self.body_id.alter_id(id);
self
}
/// Modifica los atributos HTML o las clases CSS del elemento `<body>`.
#[builder_fn] #[builder_fn]
pub fn with_body_props(mut self, op: PropsOp) -> Self { pub fn with_body_props(mut self, op: PropsOp) -> Self {
self.body_props.alter_prop(op); self.body_props.alter_prop(op);
@ -169,7 +178,12 @@ impl Page {
&self.properties &self.properties
} }
/// Devuelve identificador, clases CSS y atributos HTML del elemento `<body>`. /// Devuelve el identificador del elemento `<body>`.
pub fn body_id(&self) -> &AttrId {
&self.body_id
}
/// Devuelve los atributos HTML y clases CSS del elemento `<body>`.
pub fn body_props(&self) -> &Props { pub fn body_props(&self) -> &Props {
&self.body_props &self.body_props
} }
@ -247,7 +261,7 @@ impl Page {
head { head {
(head) (head)
} }
body (self.body_props()) { body id=[self.body_id().get()] (self.body_props()) {
(body) (body)
} }
} }

View file

@ -7,7 +7,7 @@ use pagetop::prelude::*;
#[derive(AutoDefault, Clone)] #[derive(AutoDefault, Clone)]
struct TestComp { struct TestComp {
props: Props, id: AttrId,
text: String, text: String,
} }
@ -17,7 +17,7 @@ impl Component for TestComp {
} }
fn id(&self) -> Option<String> { fn id(&self) -> Option<String> {
self.props.get_id() self.id.get()
} }
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
@ -29,7 +29,7 @@ impl TestComp {
/// Crea un componente con id y texto de salida fijos. /// Crea un componente con id y texto de salida fijos.
fn tagged(id: &str, text: &str) -> Self { fn tagged(id: &str, text: &str) -> Self {
let mut c = Self::default(); let mut c = Self::default();
c.props.alter_prop(PropsOp::set_id(id.to_string())); c.id.alter_id(id);
c.text = text.to_string(); c.text = text.to_string();
c c
} }
@ -303,8 +303,7 @@ async fn embed_get_allows_mutating_component() {
let embed = Embed::with(TestComp::tagged("orig", "texto")); let embed = Embed::with(TestComp::tagged("orig", "texto"));
// El `;` final convierte el `if let` en sentencia y libera el guard antes que `embed`. // El `;` final convierte el `if let` en sentencia y libera el guard antes que `embed`.
if let Some(mut comp) = embed.get() { if let Some(mut comp) = embed.get() {
comp.props comp.id.alter_id("modificado");
.alter_prop(PropsOp::set_id("modificado".to_string()));
}; };
assert_eq!(embed.id(), Some("modificado".to_string())); assert_eq!(embed.id(), Some("modificado".to_string()));
} }
@ -332,8 +331,7 @@ async fn embed_clone_is_deep() {
let clone = original.clone(); let clone = original.clone();
// Mutar el clon no debe afectar al original. // Mutar el clon no debe afectar al original.
if let Some(mut comp) = clone.get() { if let Some(mut comp) = clone.get() {
comp.props comp.id.alter_id("clone-id");
.alter_prop(PropsOp::set_id("clone-id".to_string()));
} }
assert_eq!(original.id(), Some("orig".to_string())); assert_eq!(original.id(), Some("orig".to_string()));
assert_eq!(clone.id(), Some("clone-id".to_string())); assert_eq!(clone.id(), Some("clone-id".to_string()));

View file

@ -13,7 +13,7 @@ async fn props_default_renders_nothing() {
#[pagetop::test] #[pagetop::test]
async fn props_new_creates_first_attr() { async fn props_new_creates_first_attr() {
let p = Props::new("hx-get", "/api"); let p = Props::new("hx-get", "/api");
assert_eq!(p.get_prop("hx-get"), Some("/api".to_string())); assert_eq!(p.get_prop("hx-get"), Some("/api"));
} }
#[pagetop::test] #[pagetop::test]
@ -59,14 +59,14 @@ async fn props_set_adds_new_attrs() {
let p = Props::default() let p = Props::default()
.with_prop(PropsOp::set("hx-get", "/api")) .with_prop(PropsOp::set("hx-get", "/api"))
.with_prop(PropsOp::set("hx-swap", "outerHTML")); .with_prop(PropsOp::set("hx-swap", "outerHTML"));
assert_eq!(p.get_prop("hx-get"), Some("/api".to_string())); assert_eq!(p.get_prop("hx-get"), Some("/api"));
assert_eq!(p.get_prop("hx-swap"), Some("outerHTML".to_string())); assert_eq!(p.get_prop("hx-swap"), Some("outerHTML"));
} }
#[pagetop::test] #[pagetop::test]
async fn props_set_replaces_existing_value() { async fn props_set_replaces_existing_value() {
let p = Props::new("hx-get", "/old").with_prop(PropsOp::set("hx-get", "/new")); let p = Props::new("hx-get", "/old").with_prop(PropsOp::set("hx-get", "/new"));
assert_eq!(p.get_prop("hx-get"), Some("/new".to_string())); assert_eq!(p.get_prop("hx-get"), Some("/new"));
} }
#[pagetop::test] #[pagetop::test]
@ -98,13 +98,13 @@ async fn props_remove_existing_attr() {
.with_prop(PropsOp::set("b", "2")) .with_prop(PropsOp::set("b", "2"))
.with_prop(PropsOp::remove("a")); .with_prop(PropsOp::remove("a"));
assert_eq!(p.get_prop("a"), None); assert_eq!(p.get_prop("a"), None);
assert_eq!(p.get_prop("b"), Some("2".to_string())); assert_eq!(p.get_prop("b"), Some("2"));
} }
#[pagetop::test] #[pagetop::test]
async fn props_remove_nonexistent_key_is_noop() { async fn props_remove_nonexistent_key_is_noop() {
let p = Props::new("a", "1").with_prop(PropsOp::remove("missing")); let p = Props::new("a", "1").with_prop(PropsOp::remove("missing"));
assert_eq!(p.get_prop("a"), Some("1".to_string())); assert_eq!(p.get_prop("a"), Some("1"));
assert_eq!(p.get_prop("missing"), None); assert_eq!(p.get_prop("missing"), None);
} }
@ -222,42 +222,22 @@ async fn props_splice_empty_string_emits_nothing() {
assert_eq!(html! { span ("") { "x" } }.into_string(), "<span>x</span>"); assert_eq!(html! { span ("") { "x" } }.into_string(), "<span>x</span>");
} }
// **< is_attrs_empty / is_classes_empty / is_empty >*********************************************** // **< is_props_empty / is_classes_empty >**********************************************************
#[pagetop::test] #[pagetop::test]
async fn props_is_attrs_empty_on_default() { async fn props_is_props_empty_on_default() {
assert!(Props::default().is_attrs_empty()); assert!(Props::default().is_props_empty());
} }
#[pagetop::test] #[pagetop::test]
async fn props_is_attrs_empty_false_after_set() { async fn props_is_props_empty_false_after_set() {
assert!(!Props::new("hx-get", "/api").is_attrs_empty()); assert!(!Props::new("hx-get", "/api").is_props_empty());
} }
#[pagetop::test] #[pagetop::test]
async fn props_is_attrs_empty_true_after_removing_last_attr() { async fn props_is_props_empty_true_after_removing_last_attr() {
let p = Props::new("only", "one").with_prop(PropsOp::remove("only")); let p = Props::new("only", "one").with_prop(PropsOp::remove("only"));
assert!(p.is_attrs_empty()); assert!(p.is_props_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] #[pagetop::test]
@ -276,45 +256,6 @@ async fn props_is_classes_empty_true_after_remove_class() {
assert!(p.is_classes_empty()); 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 >******************************************************************** // **< Regression & edge cases >********************************************************************
#[pagetop::test] #[pagetop::test]
@ -343,9 +284,9 @@ async fn props_chained_set_and_remove_yields_expected_state() {
.with_prop(PropsOp::set("c", "3")) .with_prop(PropsOp::set("c", "3"))
.with_prop(PropsOp::remove("b")) .with_prop(PropsOp::remove("b"))
.with_prop(PropsOp::set("a", "updated")); .with_prop(PropsOp::set("a", "updated"));
assert_eq!(p.get_prop("a"), Some("updated".to_string())); assert_eq!(p.get_prop("a"), Some("updated"));
assert_eq!(p.get_prop("b"), None); assert_eq!(p.get_prop("b"), None);
assert_eq!(p.get_prop("c"), Some("3".to_string())); assert_eq!(p.get_prop("c"), Some("3"));
assert_eq!( assert_eq!(
html! { span (p) {} }.into_string(), html! { span (p) {} }.into_string(),
r#"<span a="updated" c="3"></span>"# r#"<span a="updated" c="3"></span>"#