✨ (button): Añade componentes Button y ButtonSet
This commit is contained in:
parent
c8654a5742
commit
99a4837e11
10 changed files with 566 additions and 258 deletions
|
|
@ -87,6 +87,103 @@ body {
|
|||
.badge-warning { background-color: var(--val-color--warning); }
|
||||
.badge-danger { background-color: var(--val-color--danger); }
|
||||
|
||||
/*
|
||||
* Buttons
|
||||
*/
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-size: var(--val-fs--base);
|
||||
font-weight: 600;
|
||||
line-height: var(--val-lh--base);
|
||||
color: #fff;
|
||||
background-color: var(--val-color--primary);
|
||||
border: 1px solid var(--val-color--primary);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: background-color .15s ease-in-out, border-color .15s ease-in-out;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
||||
border-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
||||
}
|
||||
.button:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: default;
|
||||
}
|
||||
.button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.button-primary,
|
||||
.button-secondary,
|
||||
.button-success,
|
||||
.button-danger {
|
||||
color: #fff;
|
||||
}
|
||||
.button-info,
|
||||
.button-warning {
|
||||
color: #000;
|
||||
}
|
||||
.button-primary { background-color: var(--val-color--primary); border-color: var(--val-color--primary); }
|
||||
.button-secondary { background-color: var(--val-color--secondary); border-color: var(--val-color--secondary); }
|
||||
.button-success { background-color: var(--val-color--success); border-color: var(--val-color--success); }
|
||||
.button-info { background-color: var(--val-color--info); border-color: var(--val-color--info); }
|
||||
.button-warning { background-color: var(--val-color--warning); border-color: var(--val-color--warning); }
|
||||
.button-danger { background-color: var(--val-color--danger); border-color: var(--val-color--danger); }
|
||||
|
||||
.button-primary:hover { background-color: color-mix(in srgb, var(--val-color--primary) 85%, black); border-color: color-mix(in srgb, var(--val-color--primary) 85%, black); }
|
||||
.button-secondary:hover { background-color: color-mix(in srgb, var(--val-color--secondary) 85%, black); border-color: color-mix(in srgb, var(--val-color--secondary) 85%, black); }
|
||||
.button-success:hover { background-color: color-mix(in srgb, var(--val-color--success) 85%, black); border-color: color-mix(in srgb, var(--val-color--success) 85%, black); }
|
||||
.button-info:hover { background-color: color-mix(in srgb, var(--val-color--info) 85%, black); border-color: color-mix(in srgb, var(--val-color--info) 85%, black); }
|
||||
.button-warning:hover { background-color: color-mix(in srgb, var(--val-color--warning) 85%, black); border-color: color-mix(in srgb, var(--val-color--warning) 85%, black); }
|
||||
.button-danger:hover { background-color: color-mix(in srgb, var(--val-color--danger) 85%, black); border-color: color-mix(in srgb, var(--val-color--danger) 85%, black); }
|
||||
|
||||
.button-outline-primary,
|
||||
.button-outline-secondary,
|
||||
.button-outline-success,
|
||||
.button-outline-info,
|
||||
.button-outline-warning,
|
||||
.button-outline-danger {
|
||||
background-color: transparent;
|
||||
}
|
||||
.button-outline-primary { border-color: var(--val-color--primary); color: var(--val-color--primary); }
|
||||
.button-outline-secondary { border-color: var(--val-color--secondary); color: var(--val-color--secondary); }
|
||||
.button-outline-success { border-color: var(--val-color--success); color: var(--val-color--success); }
|
||||
.button-outline-info { border-color: var(--val-color--info); color: var(--val-color--info); }
|
||||
.button-outline-warning { border-color: var(--val-color--warning); color: var(--val-color--warning); }
|
||||
.button-outline-danger { border-color: var(--val-color--danger); color: var(--val-color--danger); }
|
||||
|
||||
.button-outline-primary:hover { background-color: var(--val-color--primary); border-color: var(--val-color--primary); color: #fff; }
|
||||
.button-outline-secondary:hover { background-color: var(--val-color--secondary); border-color: var(--val-color--secondary); color: #fff; }
|
||||
.button-outline-success:hover { background-color: var(--val-color--success); border-color: var(--val-color--success); color: #fff; }
|
||||
.button-outline-info:hover { background-color: var(--val-color--info); border-color: var(--val-color--info); color: #000; }
|
||||
.button-outline-warning:hover { background-color: var(--val-color--warning); border-color: var(--val-color--warning); color: #000; }
|
||||
.button-outline-danger:hover { background-color: var(--val-color--danger); border-color: var(--val-color--danger); color: #fff; }
|
||||
|
||||
.button-link {
|
||||
font-weight: 400;
|
||||
color: var(--val-color--primary);
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.button-link:hover {
|
||||
color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
.button-link:disabled {
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
.button-set {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* Form components
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -138,20 +138,22 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
)
|
||||
// Campo oculto (form::Hidden).
|
||||
.with_child(form::Hidden::field("origin", "form-selections"))
|
||||
// Botones de acción.
|
||||
.with_child(Button::submit(Lc::t("btn_submit", &LOC)).with_prop(
|
||||
PropsOp::add_classes(class::ButtonColor::solid(
|
||||
ThemeColor::Primary,
|
||||
)),
|
||||
))
|
||||
.with_child(Button::reset(Lc::t("btn_reset", &LOC)).with_prop(
|
||||
PropsOp::add_classes(class::ButtonColor::outline(
|
||||
ThemeColor::Secondary,
|
||||
)),
|
||||
))
|
||||
// Botonera de acciones.
|
||||
.with_child(
|
||||
Button::plain(Lc::t("btn_cancel", &LOC)).with_prop(
|
||||
PropsOp::add_classes(class::ButtonColor::link()),
|
||||
button::ButtonSet::new()
|
||||
.with_button(
|
||||
Button::submit(Lc::t("btn_submit", &LOC)).with_style(
|
||||
button::ButtonStyle::Solid(Intent::Primary),
|
||||
),
|
||||
)
|
||||
.with_button(
|
||||
Button::reset(Lc::t("btn_reset", &LOC)).with_style(
|
||||
button::ButtonStyle::Outline(Intent::Secondary),
|
||||
),
|
||||
)
|
||||
.with_button(
|
||||
Button::plain(Lc::t("btn_cancel", &LOC))
|
||||
.with_style(button::ButtonStyle::Link),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -252,20 +254,22 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
)
|
||||
// Campo oculto (form::Hidden).
|
||||
.with_child(form::Hidden::field("origin", "form-text"))
|
||||
// Botones de acción.
|
||||
.with_child(Button::submit(Lc::t("btn_submit", &LOC)).with_prop(
|
||||
PropsOp::add_classes(class::ButtonColor::solid(
|
||||
ThemeColor::Primary,
|
||||
)),
|
||||
))
|
||||
.with_child(Button::reset(Lc::t("btn_reset", &LOC)).with_prop(
|
||||
PropsOp::add_classes(class::ButtonColor::outline(
|
||||
ThemeColor::Secondary,
|
||||
)),
|
||||
))
|
||||
// Botonera de acciones.
|
||||
.with_child(
|
||||
Button::plain(Lc::t("btn_cancel", &LOC)).with_prop(
|
||||
PropsOp::add_classes(class::ButtonColor::link()),
|
||||
button::ButtonSet::new()
|
||||
.with_button(
|
||||
Button::submit(Lc::t("btn_submit", &LOC)).with_style(
|
||||
button::ButtonStyle::Solid(Intent::Primary),
|
||||
),
|
||||
)
|
||||
.with_button(
|
||||
Button::reset(Lc::t("btn_reset", &LOC)).with_style(
|
||||
button::ButtonStyle::Outline(Intent::Secondary),
|
||||
),
|
||||
)
|
||||
.with_button(
|
||||
Button::plain(Lc::t("btn_cancel", &LOC))
|
||||
.with_style(button::ButtonStyle::Link),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -395,20 +399,20 @@ fn form_lists() -> Form {
|
|||
form
|
||||
// Campo oculto (form::Hidden).
|
||||
.with_child(form::Hidden::field("origin", "form-lists"))
|
||||
// Botones de acción.
|
||||
// Botonera de acciones.
|
||||
.with_child(
|
||||
Button::submit(Lc::t("btn_submit", &LOC)).with_prop(PropsOp::add_classes(
|
||||
class::ButtonColor::solid(ThemeColor::Primary),
|
||||
)),
|
||||
button::ButtonSet::new()
|
||||
.with_button(
|
||||
Button::submit(Lc::t("btn_submit", &LOC))
|
||||
.with_style(button::ButtonStyle::Solid(Intent::Primary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::reset(Lc::t("btn_reset", &LOC)).with_prop(PropsOp::add_classes(
|
||||
class::ButtonColor::outline(ThemeColor::Secondary),
|
||||
)),
|
||||
.with_button(
|
||||
Button::reset(Lc::t("btn_reset", &LOC))
|
||||
.with_style(button::ButtonStyle::Outline(Intent::Secondary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::plain(Lc::t("btn_cancel", &LOC))
|
||||
.with_prop(PropsOp::add_classes(class::ButtonColor::link())),
|
||||
.with_button(
|
||||
Button::plain(Lc::t("btn_cancel", &LOC)).with_style(button::ButtonStyle::Link),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,3 +75,5 @@ help_rating = From 1 (very poor) to 10 (excellent).
|
|||
btn_submit = Submit
|
||||
btn_reset = Reset
|
||||
btn_cancel = Cancel
|
||||
btn_ok = Ok
|
||||
btn_delete = Delete record
|
||||
|
|
|
|||
|
|
@ -75,3 +75,5 @@ help_rating = De 1 (muy malo) a 10 (excelente).
|
|||
btn_submit = Enviar
|
||||
btn_reset = Restablecer
|
||||
btn_cancel = Cancelar
|
||||
btn_ok = Aceptar
|
||||
btn_delete = Eliminar registro
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ pub use breadcrumb::Breadcrumb;
|
|||
mod block;
|
||||
pub use block::Block;
|
||||
|
||||
mod button;
|
||||
pub use button::{Button, ButtonAction};
|
||||
pub mod button;
|
||||
#[doc(inline)]
|
||||
pub use button::{Button, ButtonSet};
|
||||
|
||||
pub mod container;
|
||||
#[doc(inline)]
|
||||
|
|
|
|||
|
|
@ -1,218 +1,10 @@
|
|||
use crate::prelude::*;
|
||||
//! Definiciones para crear botones ([`Button`]) y conjuntos de botones ([`ButtonSet`]).
|
||||
|
||||
use std::fmt;
|
||||
mod props;
|
||||
pub use props::{ButtonKind, ButtonStyle};
|
||||
|
||||
// **< ButtonAction >*******************************************************************************
|
||||
mod component;
|
||||
pub use component::Button;
|
||||
|
||||
/// Comportamiento de un [`Button`] al activarse.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ButtonAction {
|
||||
/// Envía un formulario al servidor. Es el **tipo por defecto**.
|
||||
#[default]
|
||||
Submit,
|
||||
/// Restablece todos los campos de un formulario a sus valores iniciales.
|
||||
Reset,
|
||||
/// Botón de propósito general, sin efecto predeterminado. Su comportamiento podría definirse
|
||||
/// mediante JavaScript.
|
||||
Plain,
|
||||
}
|
||||
|
||||
impl fmt::Display for ButtonAction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
ButtonAction::Submit => "submit",
|
||||
ButtonAction::Reset => "reset",
|
||||
ButtonAction::Plain => "button",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// **< Button >*************************************************************************************
|
||||
|
||||
/// Componente para crear un **botón**.
|
||||
///
|
||||
/// Renderiza un botón con soporte para las variantes disponibles en [`ButtonAction`] (`submit`,
|
||||
/// `reset` y botón genérico).
|
||||
///
|
||||
/// El comportamiento del botón se establece al crearlo:
|
||||
///
|
||||
/// - [`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.
|
||||
///
|
||||
/// El 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"));
|
||||
/// ```
|
||||
///
|
||||
/// 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: ButtonAction,
|
||||
/// 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 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: &Context) {
|
||||
self.alter_prop(PropsOp::prepend_classes("button"));
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(html! {
|
||||
button
|
||||
type=(self.kind())
|
||||
(self.props())
|
||||
name=[self.name().as_deref()]
|
||||
value=[self.value().as_deref()]
|
||||
title=[self.title().lookup(cx)]
|
||||
autofocus[*self.autofocus()]
|
||||
disabled[*self.disabled()]
|
||||
{
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
(label)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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: ButtonAction::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: ButtonAction::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: ButtonAction::Plain,
|
||||
label,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Button BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
#[builder_fn]
|
||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
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.
|
||||
#[builder_fn]
|
||||
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.
|
||||
#[builder_fn]
|
||||
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).
|
||||
#[builder_fn]
|
||||
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).
|
||||
#[builder_fn]
|
||||
pub fn with_title(mut self, title: Lc) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el botón recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el botón está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
mod set;
|
||||
pub use set::ButtonSet;
|
||||
|
|
|
|||
210
src/base/component/button/component.rs
Normal file
210
src/base/component/button/component.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
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.
|
||||
///
|
||||
/// 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"));
|
||||
/// ```
|
||||
///
|
||||
/// 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::ButtonKind,
|
||||
/// Devuelve el estilo visual del botón.
|
||||
#[getters(copy)]
|
||||
style: button::ButtonStyle,
|
||||
/// 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 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: &Context) {
|
||||
use button::ButtonStyle;
|
||||
|
||||
self.alter_prop(PropsOp::prepend_classes(match self.style() {
|
||||
ButtonStyle::None => "button".to_string(),
|
||||
ButtonStyle::Solid(intent) => util::join!("button button-", intent.as_str()),
|
||||
ButtonStyle::Outline(intent) => util::join!("button button-outline-", intent.as_str()),
|
||||
ButtonStyle::Link => "button button-link".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(html! {
|
||||
button
|
||||
type=(self.kind())
|
||||
(self.props())
|
||||
name=[self.name().as_deref()]
|
||||
value=[self.value().as_deref()]
|
||||
title=[self.title().lookup(cx)]
|
||||
autofocus[*self.autofocus()]
|
||||
disabled[*self.disabled()]
|
||||
{
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
(label)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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::ButtonKind::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::ButtonKind::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::ButtonKind::Plain,
|
||||
label,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Button BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
#[builder_fn]
|
||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el comportamiento del botón al activarse.
|
||||
#[builder_fn]
|
||||
pub fn with_kind(mut self, kind: button::ButtonKind) -> Self {
|
||||
self.kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el estilo visual del botón (usa [`button::ButtonStyle::None`] para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_style(mut self, style: button::ButtonStyle) -> 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.
|
||||
#[builder_fn]
|
||||
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.
|
||||
#[builder_fn]
|
||||
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).
|
||||
#[builder_fn]
|
||||
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).
|
||||
#[builder_fn]
|
||||
pub fn with_title(mut self, title: Lc) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el botón recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el botón está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
44
src/base/component/button/props.rs
Normal file
44
src/base/component/button/props.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
// **< ButtonStyle >********************************************************************************
|
||||
|
||||
/// Estilo visual de un [`Button`](super::Button).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ButtonStyle {
|
||||
/// Sin clase de estilo (estilo por defecto del tema).
|
||||
#[default]
|
||||
None,
|
||||
/// Botón sólido: genera la clase `button-{color}`.
|
||||
Solid(Intent),
|
||||
/// Botón con contorno: genera la clase `button-outline-{color}`.
|
||||
Outline(Intent),
|
||||
/// Botón tipo enlace: genera la clase `button-link`.
|
||||
Link,
|
||||
}
|
||||
|
||||
// **< ButtonKind >*********************************************************************************
|
||||
|
||||
/// Comportamiento de un [`Button`](super::Button) al activarse.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ButtonKind {
|
||||
/// Envía un formulario al servidor. Es el **tipo por defecto**.
|
||||
#[default]
|
||||
Submit,
|
||||
/// Restablece todos los campos de un formulario a sus valores iniciales.
|
||||
Reset,
|
||||
/// Botón de propósito general, sin efecto predeterminado. Su comportamiento podría definirse
|
||||
/// mediante JavaScript.
|
||||
Plain,
|
||||
}
|
||||
|
||||
impl fmt::Display for ButtonKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
ButtonKind::Submit => "submit",
|
||||
ButtonKind::Reset => "reset",
|
||||
ButtonKind::Plain => "button",
|
||||
})
|
||||
}
|
||||
}
|
||||
73
src/base/component/button/set.rs
Normal file
73
src/base/component/button/set.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente para mostrar un **conjunto de botones**.
|
||||
///
|
||||
/// Envuelve los botones en un contenedor que cada tema estiliza para separarlos visualmente. Sólo
|
||||
/// admite componentes [`Button`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let actions = button::ButtonSet::new()
|
||||
/// .with_button(Button::submit(Lc::n("Save")))
|
||||
/// .with_button(Button::plain(Lc::n("Cancel")));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct ButtonSet {
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||
props: Props,
|
||||
/// Devuelve los botones del conjunto.
|
||||
buttons: Children,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for ButtonSet {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.props.get_id()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_prop(PropsOp::prepend_classes("button-set"));
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let buttons = self.buttons().render(cx).await;
|
||||
if buttons.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
Ok(html! {
|
||||
div (self.props()) { (buttons) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonSet {
|
||||
// **< ButtonSet BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
#[builder_fn]
|
||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un botón al conjunto, o modifica su lista de botones con una operación [`TypedOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_button(mut self, op: impl Into<TypedOp<Button>>) -> Self {
|
||||
self.buttons.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Button >*************************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn label_is_rendered_when_set() {
|
||||
let mut button = Button::submit(Lc::n("Save"));
|
||||
|
|
@ -43,3 +45,84 @@ async fn title_attribute_can_be_cleared_with_lc_none() {
|
|||
|
||||
assert!(!html.contains("title="));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn style_class_reflects_intent_and_style() {
|
||||
let mut button =
|
||||
Button::submit(Lc::n("Save")).with_style(button::ButtonStyle::Solid(Intent::Danger));
|
||||
let html = button.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.contains("button-danger"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn outline_style_generates_outline_class() {
|
||||
let mut button =
|
||||
Button::submit(Lc::n("Save")).with_style(button::ButtonStyle::Outline(Intent::Primary));
|
||||
let html = button.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.contains("button-outline-primary"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn link_style_generates_link_class_without_intent() {
|
||||
let mut button = Button::plain(Lc::n("Cancel")).with_style(button::ButtonStyle::Link);
|
||||
let html = button.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.contains("button-link"));
|
||||
}
|
||||
|
||||
// **< ButtonSet >**********************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn button_set_is_not_rendered_when_empty() {
|
||||
let mut set = button::ButtonSet::new();
|
||||
let html = set.render(&mut Context::default()).await;
|
||||
|
||||
assert!(html.is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn button_set_wraps_buttons_in_button_set_class() {
|
||||
let mut set = button::ButtonSet::new().with_button(Button::submit(Lc::n("Save")));
|
||||
let html = set.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.contains("button-set"));
|
||||
assert!(html.contains("Save"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn button_set_renders_buttons_in_insertion_order() {
|
||||
let mut set = button::ButtonSet::new()
|
||||
.with_button(Button::submit(Lc::n("First")))
|
||||
.with_button(Button::plain(Lc::n("Second")));
|
||||
let html = set.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.find("First").unwrap() < html.find("Second").unwrap());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn button_set_add_many_appends_all_buttons() {
|
||||
let mut set = button::ButtonSet::new().with_button(TypedOp::AddMany(vec![
|
||||
Button::submit(Lc::n("Save")),
|
||||
Button::reset(Lc::n("Reset")),
|
||||
Button::plain(Lc::n("Cancel")),
|
||||
]));
|
||||
let html = set.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.contains("Save"));
|
||||
assert!(html.contains("Reset"));
|
||||
assert!(html.contains("Cancel"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn button_set_remove_by_id_drops_matching_button() {
|
||||
let mut set = button::ButtonSet::new()
|
||||
.with_button(Button::submit(Lc::n("Save")).with_id("save-button"))
|
||||
.with_button(Button::plain(Lc::n("Cancel")))
|
||||
.with_button(TypedOp::RemoveById("save-button"));
|
||||
let html = set.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(!html.contains("Save"));
|
||||
assert!(html.contains("Cancel"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue