diff --git a/extensions/pagetop-bootsier/src/theme/form.rs b/extensions/pagetop-bootsier/src/theme/form.rs index 8ccb0c2d..ef2a6c32 100644 --- a/extensions/pagetop-bootsier/src/theme/form.rs +++ b/extensions/pagetop-bootsier/src/theme/form.rs @@ -2,7 +2,7 @@ mod props; pub use props::{Autocomplete, AutofillField}; -pub use props::{CheckboxKind, InputType, Method}; +pub use props::{InputType, Method}; mod component; pub use component::Form; @@ -12,10 +12,3 @@ pub use fieldset::Fieldset; mod input; pub use input::Input; - -mod checkbox; -pub use checkbox::Checkbox; - -pub mod check; - -pub mod radio; diff --git a/extensions/pagetop-bootsier/src/theme/form/check.rs b/extensions/pagetop-bootsier/src/theme/form/check.rs deleted file mode 100644 index 59208382..00000000 --- a/extensions/pagetop-bootsier/src/theme/form/check.rs +++ /dev/null @@ -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, 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, - /// Devuelve el texto de ayuda del grupo. - help_text: Attr, - /// Devuelve las casillas del grupo. - items: Vec, - /// 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 { - 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 { - let name = self - .name() - .get() - .unwrap_or_else(|| cx.required_id::(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) -> 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) -> 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) -> 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>) -> 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>) -> 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 - } -} diff --git a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs deleted file mode 100644 index 2296ac1d..00000000 --- a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs +++ /dev/null @@ -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, - /// 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 { - 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 { - let name = self - .name() - .get() - .unwrap_or_else(|| cx.required_id::(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) -> 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) -> 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) -> 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>) -> 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 - } -} diff --git a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs index c7f2f24d..02567373 100644 --- a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs +++ b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs @@ -1,40 +1,15 @@ use pagetop::prelude::*; -/// Componente para crear un **grupo de controles relacionados** en un formulario. +/// Agrupa controles relacionados de un formulario (`
`). /// -/// Renderiza un `
` con una leyenda opcional que sirve de encabezado y una descripción -/// 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"))); -/// ``` +/// Se usa para mejorar la accesibilidad cuando se acompaña de una leyenda que encabeza el grupo. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Fieldset { #[getters(skip)] id: AttrId, - /// Devuelve las clases CSS del `fieldset`. classes: Classes, - /// Devuelve la leyenda del `fieldset`. legend: Attr, - /// Devuelve la descripción del `fieldset`. - description: Attr, - /// Devuelve si el `fieldset` está deshabilitado. disabled: bool, - /// Devuelve la lista de componentes del `fieldset`. children: Children, } @@ -48,21 +23,12 @@ impl Component for Fieldset { } fn prepare(&self, cx: &mut Context) -> Result { - let children = self.children().render(cx); - - if children.is_empty() { - return Ok(html! {}); - } - Ok(html! { fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] { @if let Some(legend) = self.legend().lookup(cx) { legend { (legend) } } - @if let Some(description) = self.description().lookup(cx) { - p class="fieldset-description" { (description) } - } - (children) + (self.children().render(cx)) } }) } @@ -85,17 +51,10 @@ impl Fieldset { self } - /// Establece o elimina la leyenda del `fieldset` (basta pasar `None` para quitarla). + /// Establece la leyenda del `fieldset`. #[builder_fn] - pub fn with_legend(mut self, legend: impl Into>) -> Self { - self.legend.alter_opt(legend.into()); - 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>) -> Self { - self.description.alter_opt(description.into()); + pub fn with_legend(mut self, legend: L10n) -> Self { + self.legend.alter_value(legend); self } @@ -106,8 +65,8 @@ impl Fieldset { self } - /// Añade un nuevo componente al `fieldset`, o aplica una operación [`ChildOp`] sobre la lista - /// de componentes (`children`). + /// Añade un nuevo componente al `fieldset` o modifica la lista de de componentes (`children`) + /// con una operación [`ChildOp`]. #[builder_fn] pub fn with_child(mut self, op: impl Into) -> Self { self.children.alter_child(op.into()); diff --git a/extensions/pagetop-bootsier/src/theme/form/radio.rs b/extensions/pagetop-bootsier/src/theme/form/radio.rs deleted file mode 100644 index 377710f2..00000000 --- a/extensions/pagetop-bootsier/src/theme/form/radio.rs +++ /dev/null @@ -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, 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`: -/// -/// ```rust,ignore -/// #[derive(serde::Deserialize)] -/// struct FormData { -/// plan: Option, // 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, - /// Devuelve el texto de ayuda del grupo. - help_text: Attr, - /// Devuelve las opciones del grupo. - items: Vec, - /// 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 { - 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 { - let name = self - .name() - .get() - .unwrap_or_else(|| cx.required_id::(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) -> 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) -> 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) -> 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>) -> 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>) -> 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 - } -}