♻️ (html): API para id's en Props y componentes
This commit is contained in:
parent
8d0103c257
commit
62219584b0
31 changed files with 541 additions and 405 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue