♻️ (html): API para id's en Props y componentes

This commit is contained in:
Manuel Cillero 2026-06-20 15:02:23 +02:00
parent 8d0103c257
commit 62219584b0
31 changed files with 541 additions and 405 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
/// implementaciones concretas como `Attr<L10n>` y `Attr<String>`, o en tipos específicos como
/// [`AttrId`] y [`AttrName`].
/// [`AttrName`].
#[derive(AutoDefault, Clone, Debug)]
pub struct Attr<T>(Option<T>);
@ -128,73 +128,6 @@ 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 >***********************************************************************************
/// Nombre normalizado para el atributo `name` o similar de HTML.

View file

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

View file

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