pagetop/src/base/component/button/component.rs
Manuel Cillero 3c3ffc91e6 ♻️ (pagetop): with_prop() acepta impl Into<PropsOp>
`FlexItem`, `Margin` y `Padding` se pasan directamente gracias a `From`
 hacia `PropsOp`, sin `PropsOp::flex_item()`/`margin()`/`padding()` ni
`.into()` explícito. Revisadas todas las llamadas en el código para
simplificarlas.
2026-09-13 01:38:33 +02:00

268 lines
9.5 KiB
Rust

use crate::prelude::*;
/// Componente para crear un **botón**.
///
/// Renderiza un botón con soporte para las variantes:
///
/// - [`Button::submit()`]: botón de envío (por defecto).
/// - [`Button::reset()`]: botón de restablecimiento de valores.
/// - [`Button::plain()`]: botón genérico sin comportamiento predeterminado.
/// - [`Button::anchor()`]: enlace de navegación real con el aspecto de un botón.
///
/// No confundir [`Button::anchor()`] con [`button::Style::Link`]: el primero renderiza un `<a
/// href=...>` real que navega; el segundo es sólo un estilo visual (clase `button-link`) que se
/// aplica con [`with_style()`](Self::with_style) sobre cualquiera de las otras variantes, que
/// siguen siendo un `<button>`.
///
/// Un botón puede usarse dentro o fuera de un formulario.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let save = Button::submit(Lc::n("Save"));
/// let cancel = Button::plain(Lc::n("Cancel"));
/// let clear = Button::reset(Lc::n("Clear"));
/// let edit = Button::anchor(Lc::n("Edit"), "/items/1/edit");
/// ```
///
/// Cuando el botón activa el envío, el navegador incluye el par `name=value` en los datos del
/// formulario **sólo si** tiene el atributo `name` definido. Es la forma habitual de identificar
/// cuál de los botones de envío fue pulsado. En el servidor se deserializa como `Option<String>`:
///
/// ```rust,ignore
/// #[derive(serde::Deserialize)]
/// struct FormData {
/// #[serde(default)]
/// action: Option<String>, // p. ej., "save" o "delete"; `None` si el botón no tenía `name`.
/// }
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Button {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
/// Devuelve el comportamiento del botón al activarse.
kind: button::Kind,
/// Devuelve el tamaño visual del botón.
#[getters(copy)]
size: button::Size,
/// Devuelve el estilo visual del botón.
#[getters(copy)]
style: button::Style,
/// Devuelve el nombre del botón.
name: AttrName,
/// Devuelve el valor del botón.
value: AttrValue,
/// Devuelve la etiqueta del botón.
label: Lc,
/// Devuelve el texto emergente del botón (atributo `title`).
title: Lc,
/// Devuelve la ruta de destino cuando el botón se renderiza como enlace de navegación
/// (`<a href=...>` en vez de `<button>`). Vacía por defecto: en ese caso `prepare()` renderiza
/// un `<button>` normal, ignorando este campo.
href: Route,
/// Devuelve si el botón recibe el foco automáticamente al cargar la página.
autofocus: bool,
/// Devuelve si el botón está deshabilitado.
disabled: bool,
}
#[async_trait]
impl Component for Button {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.props.get_id()
}
fn setup(&mut self, cx: &mut Context) {
use button::{Size, Style};
self.alter_prop(PropsOp::prepend_classes(match self.size() {
Size::None => "",
Size::Small => "button-sm",
Size::Large => "button-lg",
}));
self.alter_prop(PropsOp::prepend_classes(match self.style() {
Style::None => "button".to_string(),
Style::Solid(intent) => util::join!("button button-", intent.color(cx)),
Style::Outline(intent) => util::join!("button button-outline-", intent.color(cx)),
Style::Link => "button button-link".to_string(),
}));
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
if let Some(route) = self.href().try_resolve(cx) {
let disabled = *self.disabled();
let href = (!disabled).then_some(route);
let aria_disabled = disabled.then_some("true");
let tabindex = disabled.then_some("-1");
return Ok(html! {
a
(self.props().unpack(cx))
href=[href]
title=[self.title().lookup(cx)]
autofocus[*self.autofocus()]
aria-disabled=[aria_disabled]
tabindex=[tabindex]
{
(self.label().using(cx))
}
});
}
Ok(html! {
button
type=(self.kind())
(self.props().unpack(cx))
name=[self.name().as_deref()]
value=[self.value().as_deref()]
title=[self.title().lookup(cx)]
autofocus[*self.autofocus()]
disabled[*self.disabled()]
{
(self.label().using(cx))
}
})
}
}
#[builder_impl]
impl Button {
/// Crea un botón de **envío** (`type="submit"`).
///
/// Es la acción predeterminada al pulsar un botón en la mayoría de los formularios: envía los
/// datos al servidor.
pub fn submit(label: Lc) -> Self {
Self {
kind: button::Kind::Submit,
label,
..Default::default()
}
}
/// Crea un botón de **restablecimiento** (`type="reset"`).
///
/// Al pulsarlo, devuelve todos los campos del formulario a sus valores iniciales.
pub fn reset(label: Lc) -> Self {
Self {
kind: button::Kind::Reset,
label,
..Default::default()
}
}
/// Crea un **botón genérico** (`type="button"`).
///
/// No tiene un comportamiento predeterminado sobre el formulario. Su comportamiento puede
/// definirse mediante JavaScript.
pub fn plain(label: Lc) -> Self {
Self {
kind: button::Kind::Plain,
label,
..Default::default()
}
}
/// Crea un **enlace de navegación** con el aspecto de un botón (`<a href=...>`).
///
/// A diferencia de [`Button::submit()`], [`Button::reset()`] y [`Button::plain()`], que
/// siempre renderizan un `<button>`, este constructor produce un enlace real. Navega a `route`
/// en vez de interactuar con un formulario. Se aplican las mismas clases de estilo (ver
/// [`with_style()`](Self::with_style)), por lo que el tema activo puede mostrarlo igual que
/// cualquier otra variante de `Button`. No confundir con [`button::Style::Link`], que es sólo
/// un estilo visual sobre un `<button>`.
pub fn anchor(label: Lc, route: impl Into<Route>) -> Self {
Self {
label,
href: route.into(),
..Default::default()
}
}
// **< Button BUILDER >*************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
self.props.alter_id(id);
self
}
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
self.props.alter_prop(op);
self
}
/// Establece el comportamiento del botón al activarse.
pub fn with_kind(mut self, kind: button::Kind) -> Self {
self.kind = kind;
self
}
/// Establece el tamaño visual del botón (usa [`button::Size::None`] para quitarlo).
pub fn with_size(mut self, size: button::Size) -> Self {
self.size = size;
self
}
/// Establece el estilo visual del botón (usa [`button::Style::None`] para quitarlo).
pub fn with_style(mut self, style: button::Style) -> Self {
self.style = style;
self
}
/// Establece el nombre del botón (atributo `name`).
///
/// Cuando el formulario tiene varios botones de envío, el navegador incluye en el envío el par
/// `name=value` sólo del botón que activó el formulario. Permite identificar cuál fue pulsado.
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.name.alter_name(name);
self
}
/// Establece el valor del botón (atributo `value`).
///
/// Es el dato que el navegador transmite al servidor junto con el `name` cuando este botón
/// activa el envío. Útil para distinguir entre varios botones de envío en un mismo formulario.
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
self.value.alter_str(value);
self
}
/// Establece la etiqueta visible del botón (usa [`Lc::none()`] para quitarla).
pub fn with_label(mut self, label: Lc) -> Self {
self.label = label;
self
}
/// Establece el texto emergente del botón (usa [`Lc::none()`] para quitarlo).
pub fn with_title(mut self, title: Lc) -> Self {
self.title = title;
self
}
/// Establece la ruta de destino y convierte el botón en enlace de navegación (`<a href=...>`).
/// Puedes usar un [`Route`] vacío (por defecto) para que vuelva a renderizarse como `<button>`.
/// Ver [`Button::anchor()`] para el constructor equivalente.
pub fn with_href(mut self, route: impl Into<Route>) -> Self {
self.href = route.into();
self
}
/// Establece si el botón recibe el foco automáticamente al cargar la página.
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
self.autofocus = autofocus;
self
}
/// Establece si el botón está deshabilitado.
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}