Compare commits

..

No commits in common. "800b5676d2576f0a92a0905a752cc13982b89bcc" and "041965819278696f8d6c9664c2719a62f5ae1047" have entirely different histories.

5 changed files with 9 additions and 805 deletions

View file

@ -2,7 +2,7 @@
mod props; mod props;
pub use props::{Autocomplete, AutofillField}; pub use props::{Autocomplete, AutofillField};
pub use props::{CheckboxKind, InputType, Method}; pub use props::{InputType, Method};
mod component; mod component;
pub use component::Form; pub use component::Form;
@ -12,10 +12,3 @@ pub use fieldset::Fieldset;
mod input; mod input;
pub use input::Input; pub use input::Input;
mod checkbox;
pub use checkbox::Checkbox;
pub mod check;
pub mod radio;

View file

@ -1,257 +0,0 @@
//! Definiciones para crear grupos de casillas de verificación (*check buttons*).
use pagetop::prelude::*;
// **< Item >***************************************************************************************
/// Casilla de verificación individual de un [`form::check::Group`](Group).
///
/// Representa cada casilla de un grupo de casillas de verificación, con una etiqueta localizable
/// visible. Puede marcarse como seleccionada o deshabilitada de forma independiente al resto.
///
/// El parámetro `name` de [`form::check::Item::new()`](Item::new) se combina con el `name` del
/// grupo para componer el atributo `name` de la casilla. Por ejemplo, si el grupo tiene
/// `name=interests` y el ítem se crea con `name=tech`, la casilla tendrá `name=interests_tech`.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let item = form::check::Item::new("apple", L10n::n("Apple")).with_checked(true);
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Item {
/// Devuelve el nombre que se combina con el del grupo para componer el atributo `name`.
name: AttrValue,
/// Devuelve la etiqueta de la casilla.
label: L10n,
/// Devuelve si la casilla debe aparecer marcada por defecto.
checked: bool,
/// Devuelve si la casilla está deshabilitada.
disabled: bool,
}
impl Item {
/// Crea una nueva casilla con el nombre y la etiqueta indicados.
///
/// El parámetro `name` se combina con el del grupo para componer el atributo `name` de la
/// casilla.
pub fn new(name: impl AsRef<str>, label: L10n) -> Self {
Self {
name: AttrValue::new(name),
label,
checked: false,
disabled: false,
}
}
// **< Item BUILDER >***************************************************************************
/// Establece si la casilla debe aparecer marcada por defecto.
pub fn with_checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
/// Establece si la casilla está deshabilitada.
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
// **< Group >**************************************************************************************
/// Componente para crear un **grupo de casillas de verificación**.
///
/// Renderiza un conjunto de casillas de verificación donde, a diferencia de un grupo de botones
/// [`form::radio::Group`](crate::theme::form::radio::Group), cada casilla puede marcarse de forma
/// independiente.
///
/// Las casillas se añaden mediante [`with_item()`](Group::with_item) usando instancias de
/// [`form::check::Item`](Item). Si se activa el modo en línea con
/// [`with_inline()`](Group::with_inline), las casillas se disponen horizontalmente.
///
/// El atributo `name` de cada casilla se construye automáticamente combinando el `name` del grupo
/// y el `name` del [`form::check::Item`](Item) con un guion bajo. Por ejemplo, para el grupo con
/// `name=interests` y casillas con `name=art` y `name=tech`, se genera `name=interests_art` y
/// `name=interests_tech`.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let interests = form::check::Group::new()
/// .with_name("interests")
/// .with_label(L10n::n("Areas of interest"))
/// .with_item(form::check::Item::new("art", L10n::n("Art")))
/// .with_item(form::check::Item::new("tech", L10n::n("Technology")))
/// .with_item(form::check::Item::new("science", L10n::n("Science")).with_checked(true));
/// ```
///
/// Cada `name` debe ser único y válido como identificador de campo. Cuando el usuario marca una
/// casilla, el navegador envía algo como `interests_tech=true`; mientras que si no la marca, no
/// envía nada. En el servidor cada campo se deserializa como `bool` con `#[serde(default)]`:
///
/// ```rust,ignore
/// #[derive(serde::Deserialize)]
/// struct FormData {
/// #[serde(default)]
/// interests_art: bool,
/// #[serde(default)]
/// interests_tech: bool,
/// #[serde(default)]
/// interests_science: bool,
/// }
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Group {
#[getters(skip)]
id: AttrId,
/// Devuelve las clases CSS del contenedor del grupo.
classes: Classes,
/// Devuelve el nombre base compartido por todas las casillas del grupo.
name: AttrName,
/// Devuelve la etiqueta del grupo.
label: Attr<L10n>,
/// Devuelve el texto de ayuda del grupo.
help_text: Attr<L10n>,
/// Devuelve las casillas del grupo.
items: Vec<Item>,
/// Devuelve si todo el grupo está deshabilitado.
disabled: bool,
/// Devuelve si las casillas se muestran en línea horizontalmente.
inline: bool,
}
impl Component for Group {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup(&mut self, _cx: &Context) {
self.alter_classes(ClassesOp::Prepend, "form-item form-item-checkboxes");
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let name = self
.name()
.get()
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 3));
let group_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
Ok(html! {
div id=(&group_id) class=[self.classes().get()] {
@if let Some(label) = self.label().lookup(cx) {
label class="form-label" { (label) }
}
@let item_classes = if *self.inline() {
"form-check form-check-inline"
} else {
"form-check"
};
@for (item, i) in self.items().iter().zip(1..) {
@let i = i.to_string();
@let item_id = util::join!(&group_id, "-", &i);
@let item_name = if let Some(item_name) = item.name().get() {
util::join!(&name, "_", &item_name)
} else {
util::join!(&name, "_", &i)
};
div class=(item_classes) {
input
type="checkbox"
id=(&item_id)
class="form-check-input"
name=(&item_name)
value="true"
checked[*item.checked()]
disabled[*item.disabled() || *self.disabled()];
label class="form-check-label" for=(&item_id) {
(item.label().using(cx))
}
}
}
@if let Some(description) = self.help_text().lookup(cx) {
div class="form-text" { (description) }
}
}
})
}
}
impl Group {
// **< Group BUILDER >**************************************************************************
/// Establece el identificador único (`id`) del grupo de casillas.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_id(id);
self
}
/// Modifica la lista de clases CSS aplicadas al contenedor del grupo de casillas.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_classes(op, classes);
self
}
/// Establece el nombre base para el grupo de casillas.
///
/// Se combina con el `name` de cada [`form::check::Item`](Item) para generar el atributo `name`
/// de cada casilla de verificación. Por ejemplo, con `name=interests` en el grupo y `name=tech`
/// en el ítem, se genera `name=interests_tech`.
///
/// Si se omite, se asigna un nombre generado automáticamente. Para deserializar los campos en
/// el servidor es recomendable establecer un `name` explícito.
#[builder_fn]
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.name.alter_name(name);
self
}
/// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla).
#[builder_fn]
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
self.label.alter_opt(label.into());
self
}
/// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo).
#[builder_fn]
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
self.help_text.alter_opt(help_text.into());
self
}
/// Añade una casilla al grupo. Las casillas se muestran en el orden en que se añaden.
#[builder_fn]
pub fn with_item(mut self, item: Item) -> Self {
self.items.push(item);
self
}
/// Establece si todo el grupo está deshabilitado.
///
/// Cuando está activo, se combina con el estado `disabled` de cada [`Item`].
#[builder_fn]
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Establece si las casillas se muestran en línea horizontalmente.
///
/// Al activar este modo, se añade la clase `form-check-inline` al contenedor de cada casilla.
#[builder_fn]
pub fn with_inline(mut self, inline: bool) -> Self {
self.inline = inline;
self
}
}

View file

@ -1,222 +0,0 @@
use pagetop::prelude::*;
use crate::theme::form;
use crate::LOCALES_BOOTSIER;
/// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*).
///
/// Renderiza un control binario (marcado/no marcado) en dos variantes visuales, por defecto se
/// muestra como una casilla de verificación estándar, pero también puede renderizarse como un
/// interruptor de encendido/apagado ([`Checkbox::switch()`]).
///
/// Se puede mostrar en línea con otros controles usando [`with_inline()`](Checkbox::with_inline), o
/// justificar a la derecha del contenedor invirtiendo el orden de la etiqueta y el control usando
/// [`with_reverse()`](Checkbox::with_reverse).
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let accept_terms = form::Checkbox::check() // También sirve new() o default().
/// .with_name("terms_accepted")
/// .with_label(L10n::n("I accept the terms and conditions"))
/// .with_required(true);
///
/// let notifications = form::Checkbox::switch()
/// .with_name("notifications_enabled")
/// .with_label(L10n::n("Receive email notifications"))
/// .with_checked(true);
/// ```
///
/// Cuando el control está activo, el navegador envía `name=true`; si no lo está, no envía nada.
/// En el servidor el campo se deserializa como `bool` con `#[serde(default)]`:
///
/// ```rust,ignore
/// #[derive(serde::Deserialize)]
/// struct FormData {
/// #[serde(default)]
/// terms_accepted: bool, // true = marcada, false = no marcada.
/// #[serde(default)]
/// notifications_enabled: bool, // true = activo, false = inactivo.
/// }
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Checkbox {
#[getters(skip)]
id: AttrId,
/// Devuelve las clases CSS del contenedor del control.
classes: Classes,
/// Devuelve la variante visual del control.
checkbox_kind: form::CheckboxKind,
/// Devuelve el nombre del campo.
name: AttrName,
/// Devuelve la etiqueta del control.
label: Attr<L10n>,
/// Devuelve si el control debe estar marcado/activo por defecto.
checked: bool,
/// Devuelve si el campo es obligatorio.
required: bool,
/// Devuelve si el control está deshabilitado.
disabled: bool,
/// Devuelve si el control se muestra en línea con otros controles.
inline: bool,
/// Devuelve si el control y su etiqueta se justifican a la derecha del contenedor.
reverse: bool,
}
impl Component for Checkbox {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup(&mut self, _cx: &Context) {
let mut classes = "form-item form-check".to_string();
if *self.checkbox_kind() == form::CheckboxKind::Switch {
classes.push_str(" form-switch");
}
if *self.inline() {
classes.push_str(" form-check-inline");
}
if *self.reverse() {
classes.push_str(" form-check-reverse");
}
self.alter_classes(ClassesOp::Prepend, classes);
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let name = self
.name()
.get()
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 1));
let id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
let is_switch = *self.checkbox_kind() == form::CheckboxKind::Switch;
Ok(html! {
div class=[self.classes().get()] {
input
type="checkbox"
role=[is_switch.then_some("switch")]
id=(&id)
class="form-check-input"
name=(&name)
value="true"
checked[*self.checked()]
required[*self.required()]
disabled[*self.disabled()]
switch[is_switch];
@if let Some(label) = self.label().lookup(cx) {
label class="form-check-label" for=(&id) {
(label)
@if *self.required() {
span
class="form-required"
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
{
"*"
}
}
}
}
}
})
}
}
impl Checkbox {
/// Crea una casilla de verificación estándar.
pub fn check() -> Self {
Self::default()
}
/// Crea un interruptor de encendido/apagado (*toggle switch*).
pub fn switch() -> Self {
Self {
checkbox_kind: form::CheckboxKind::Switch,
..Self::default()
}
}
// **< Checkbox BUILDER >***********************************************************************
/// Establece el identificador único (`id`) del control.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_id(id);
self
}
/// Modifica la lista de clases CSS aplicadas al contenedor del control.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_classes(op, classes);
self
}
/// Establece la variante visual del control.
#[builder_fn]
pub fn with_kind(mut self, kind: form::CheckboxKind) -> Self {
self.checkbox_kind = kind;
self
}
/// Establece el nombre del campo (atributo `name`).
///
/// Si se omite, se asigna un identificador generado automáticamente. Para deserializar el campo
/// en el servidor es recomendable establecer un `name` explícito.
#[builder_fn]
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.name.alter_name(name);
self
}
/// Establece o elimina la etiqueta visible del control (basta pasar `None` para quitarla).
#[builder_fn]
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
self.label.alter_opt(label.into());
self
}
/// Establece si el control debe aparecer marcado/activo por defecto.
#[builder_fn]
pub fn with_checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
/// Establece si el campo es obligatorio.
#[builder_fn]
pub fn with_required(mut self, required: bool) -> Self {
self.required = required;
self
}
/// Establece si el control está deshabilitado.
#[builder_fn]
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Establece si el control se muestra en línea con otros controles.
///
/// Al activar este modo, se añade la clase `form-check-inline` al contenedor, lo que permite
/// alinear varios controles horizontalmente.
#[builder_fn]
pub fn with_inline(mut self, inline: bool) -> Self {
self.inline = inline;
self
}
/// Establece si el control y su etiqueta se justifican a la derecha del contenedor.
///
/// Al activar este modo, se añade la clase `form-check-reverse` al contenedor.
#[builder_fn]
pub fn with_reverse(mut self, reverse: bool) -> Self {
self.reverse = reverse;
self
}
}

View file

@ -1,40 +1,15 @@
use pagetop::prelude::*; use pagetop::prelude::*;
/// Componente para crear un **grupo de controles relacionados** en un formulario. /// Agrupa controles relacionados de un formulario (`<fieldset>`).
/// ///
/// Renderiza un `<fieldset>` con una leyenda opcional que sirve de encabezado y una descripción /// Se usa para mejorar la accesibilidad cuando se acompaña de una leyenda que encabeza el grupo.
/// también opcional que aparece justo antes de los controles. Es un elemento semántico que mejora
/// la accesibilidad porque los lectores de pantalla anuncian la leyenda antes de leer cada control
/// del contenido.
///
/// Los componentes del grupo se añaden con [`with_child()`](Fieldset::with_child). Si no hay
/// contenido para renderizar, el `fieldset` no se genera. Si está deshabilitado, todos sus
/// controles hijos quedan deshabilitados automáticamente por el navegador.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let personal_data = form::Fieldset::new()
/// .with_legend(L10n::n("Personal data"))
/// .with_description(L10n::n("Enter your full name and contact email."))
/// .with_child(form::Input::textfield().with_name("name").with_label(L10n::n("Full name")))
/// .with_child(form::Input::email().with_name("email").with_label(L10n::n("Email")));
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Fieldset { pub struct Fieldset {
#[getters(skip)] #[getters(skip)]
id: AttrId, id: AttrId,
/// Devuelve las clases CSS del `fieldset`.
classes: Classes, classes: Classes,
/// Devuelve la leyenda del `fieldset`.
legend: Attr<L10n>, legend: Attr<L10n>,
/// Devuelve la descripción del `fieldset`.
description: Attr<L10n>,
/// Devuelve si el `fieldset` está deshabilitado.
disabled: bool, disabled: bool,
/// Devuelve la lista de componentes del `fieldset`.
children: Children, children: Children,
} }
@ -48,21 +23,12 @@ impl Component for Fieldset {
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let children = self.children().render(cx);
if children.is_empty() {
return Ok(html! {});
}
Ok(html! { Ok(html! {
fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] { fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] {
@if let Some(legend) = self.legend().lookup(cx) { @if let Some(legend) = self.legend().lookup(cx) {
legend { (legend) } legend { (legend) }
} }
@if let Some(description) = self.description().lookup(cx) { (self.children().render(cx))
p class="fieldset-description" { (description) }
}
(children)
} }
}) })
} }
@ -85,17 +51,10 @@ impl Fieldset {
self self
} }
/// Establece o elimina la leyenda del `fieldset` (basta pasar `None` para quitarla). /// Establece la leyenda del `fieldset`.
#[builder_fn] #[builder_fn]
pub fn with_legend(mut self, legend: impl Into<Option<L10n>>) -> Self { pub fn with_legend(mut self, legend: L10n) -> Self {
self.legend.alter_opt(legend.into()); self.legend.alter_value(legend);
self
}
/// Establece o elimina la descripción del `fieldset` (basta pasar `None` para quitarla).
#[builder_fn]
pub fn with_description(mut self, description: impl Into<Option<L10n>>) -> Self {
self.description.alter_opt(description.into());
self self
} }
@ -106,8 +65,8 @@ impl Fieldset {
self self
} }
/// Añade un nuevo componente al `fieldset`, o aplica una operación [`ChildOp`] sobre la lista /// Añade un nuevo componente al `fieldset` o modifica la lista de de componentes (`children`)
/// de componentes (`children`). /// con una operación [`ChildOp`].
#[builder_fn] #[builder_fn]
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self { pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
self.children.alter_child(op.into()); self.children.alter_child(op.into());

View file

@ -1,269 +0,0 @@
//! Definiciones para crear grupos de botones de opción (*radio buttons*).
use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
// **< Item >***************************************************************************************
/// Botón de opción individual de un [`form::radio::Group`](Group).
///
/// Representa cada opción de un grupo de opciones exclusivas entre sí, con un valor (el que se
/// envía al servidor), una etiqueta localizable visible y puede marcarse como seleccionada o
/// inicialmente deshabilitada de forma independiente.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let item = form::radio::Item::new("monthly", L10n::n("Monthly")).with_checked(true);
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Item {
/// Devuelve el valor enviado al servidor cuando la opción está seleccionada.
value: AttrValue,
/// Devuelve la etiqueta de la opción.
label: L10n,
/// Devuelve si la opción debe aparecer seleccionada por defecto.
checked: bool,
/// Devuelve si la opción está deshabilitada.
disabled: bool,
}
impl Item {
/// Crea una nueva opción con el valor y la etiqueta indicados.
pub fn new(value: impl AsRef<str>, label: L10n) -> Self {
Self {
value: AttrValue::new(value),
label,
checked: false,
disabled: false,
}
}
// **< Item BUILDER >***************************************************************************
/// Establece si la opción aparece seleccionada por defecto.
///
/// Si varias opciones del grupo tienen `checked` activo, sólo la primera se renderizará como
/// seleccionada; las demás se ignorarán.
pub fn with_checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
/// Establece si la opción está inicialmente deshabilitada.
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
// **< Group >**************************************************************************************
/// Componente para crear un **grupo de botones de opción**.
///
/// Renderiza un grupo de botones de opción [`form::radio::Item`](Item) que comparten el mismo
/// atributo `name`, por lo que sólo puede seleccionarse uno a la vez. Las opciones se añaden con
/// [`with_item()`](Group::with_item).
///
/// Si se activa el modo en línea [`with_inline()`](Group::with_inline), los botones se
/// disponen horizontalmente. El atributo `required` se propaga a todos los botones del grupo para
/// cumplir con la especificación HTML.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let plan = form::radio::Group::new()
/// .with_name("plan")
/// .with_label(L10n::n("Subscription plan"))
/// .with_item(form::radio::Item::new("monthly", L10n::n("Monthly")))
/// .with_item(form::radio::Item::new("annual", L10n::n("Annual")).with_checked(true))
/// .with_required(true);
/// ```
///
/// Cuando el usuario selecciona un botón, el navegador envía algo como `plan=monthly`; si no
/// selecciona ninguno, no envía nada. En el servidor el campo se deserializa como `Option<String>`:
///
/// ```rust,ignore
/// #[derive(serde::Deserialize)]
/// struct FormData {
/// plan: Option<String>, // Some("monthly"), Some("annual"), ..., o None si no se seleccionó.
/// }
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Group {
#[getters(skip)]
id: AttrId,
/// Devuelve las clases CSS del contenedor del grupo.
classes: Classes,
/// Devuelve el nombre compartido por todos los botones de opción del grupo.
name: AttrName,
/// Devuelve la etiqueta del grupo.
label: Attr<L10n>,
/// Devuelve el texto de ayuda del grupo.
help_text: Attr<L10n>,
/// Devuelve las opciones del grupo.
items: Vec<Item>,
/// Devuelve si la selección de alguna opción del grupo es obligatoria.
required: bool,
/// Devuelve si todo el grupo está deshabilitado.
disabled: bool,
/// Devuelve si los botones se muestran en línea horizontalmente.
inline: bool,
}
impl Component for Group {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup(&mut self, _cx: &Context) {
self.alter_classes(ClassesOp::Prepend, "form-item form-item-radios");
}
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let name = self
.name()
.get()
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 3));
let group_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
Ok(html! {
div id=(&group_id) class=[self.classes().get()] {
@if let Some(label) = self.label().lookup(cx) {
label class="form-label" {
(label)
@if *self.required() {
span
class="form-required"
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
{
"*"
}
}
}
}
@let item_classes = if *self.inline() {
"form-check form-check-inline"
} else {
"form-check"
};
@let mut do_check = true;
@for (item, i) in self.items().iter().zip(1..) {
@let checked = {
let c = *item.checked() && do_check;
if c { do_check = false; }
c
};
@let i = i.to_string();
@let item_id = util::join!(&group_id, "-", &i);
div class=(item_classes) {
input
type="radio"
id=(&item_id)
class="form-check-input"
name=(&name)
value=[item.value().get()]
checked[checked]
required[*self.required()]
disabled[*item.disabled() || *self.disabled()];
label class="form-check-label" for=(&item_id) {
(item.label().using(cx))
}
}
}
@if let Some(description) = self.help_text().lookup(cx) {
div class="form-text" { (description) }
}
}
})
}
}
impl Group {
// **< Group BUILDER >**************************************************************************
/// Establece el identificador único (`id`) del grupo de opciones.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_id(id);
self
}
/// Modifica la lista de clases CSS aplicadas al contenedor del grupo de opciones.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_classes(op, classes);
self
}
/// Establece el nombre compartido por todos los botones de opción del grupo.
///
/// Todas las opciones [`form::radio::Item`](Item) del grupo llevarán este mismo `name`, lo que
/// garantiza la exclusividad de la selección. Es imprescindible establecer un `name`; sin él
/// los botones no se envían al servidor.
///
/// Si se omite, se asigna un nombre generado automáticamente. Para deserializar los campos en
/// el servidor es recomendable establecer un `name` explícito.
#[builder_fn]
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.name.alter_name(name);
self
}
/// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla).
#[builder_fn]
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
self.label.alter_opt(label.into());
self
}
/// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo).
#[builder_fn]
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
self.help_text.alter_opt(help_text.into());
self
}
/// Añade una opción al grupo. Las opciones se muestran en el orden en que se añaden.
#[builder_fn]
pub fn with_item(mut self, item: Item) -> Self {
self.items.push(item);
self
}
/// Establece si la selección de alguna opción del grupo es obligatoria.
///
/// El atributo `required` se propaga a todos los botones del grupo para cumplir con la
/// especificación HTML.
#[builder_fn]
pub fn with_required(mut self, required: bool) -> Self {
self.required = required;
self
}
/// Establece si todo el grupo está deshabilitado.
///
/// Cuando está activo, se combina con el estado `disabled` de cada [`Item`].
#[builder_fn]
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Establece si los botones se muestran en línea horizontalmente.
///
/// Al activar este modo, se añade la clase `form-check-inline` al contenedor de cada opción.
#[builder_fn]
pub fn with_inline(mut self, inline: bool) -> Self {
self.inline = inline;
self
}
}