pagetop/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs
Manuel Cillero 545a87fbb0 ♻️ (bootsier): Ajusta componentes base
- Añade `#[builder_fn]` a `with_width()`/`with_floating_label()`.
- `Rounded::new()` pasa a aplicar el radio por defecto
  (`Rounded::default()` queda sin redondeo).
- `Form::action` usa `Route` en lugar de `AttrValue`, y `check::Item`
  expone `value` en vez de `name` para generar casillas con `name`
  compartido valores distintos.
2026-08-01 13:41:19 +02:00

113 lines
4.1 KiB
Rust

//! Definiciones para crear áreas de texto en formularios.
use pagetop::prelude::*;
pub use pagetop::base::component::form::Textarea;
const EXTRA_FLOATING_LABEL: &str = "bootsier.form.textarea.floating_label";
/// Extensión de Bootsier para [`form::Textarea`].
///
/// Proporciona soporte para **etiquetas flotantes** (*floating label*). La etiqueta flotante se
/// superpone al control mientras no hay ninguna opción seleccionada y permanece flotante cuando hay
/// una selección activa.
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let comentario = bs::form::Textarea::new()
/// .with_name("comment")
/// .with_label(L10n::n("Comment"))
/// .with_placeholder(L10n::n("Write here..."))
/// .with_floating_label(true);
/// ```
pub trait TextareaBootsier {
/// Establece si la etiqueta se muestra flotante sobre el campo.
///
/// Cuando está activo, la etiqueta se superpone al área de texto y asciende al enfocarlo o
/// cuando tiene contenido. Requiere que el campo tenga un atributo `placeholder` definido;
/// si no se especifica, se fuerza `placeholder=""` antes del renderizado.
///
/// Si se usa la etiqueta flotante, se anula el valor establecido con
/// [`with_rows()`](form::Textarea::with_rows) antes del renderizado.
#[builder_fn]
fn with_floating_label(self, floating: bool) -> Self;
}
impl TextareaBootsier for Textarea {
#[builder_fn]
fn with_floating_label(mut self, floating: bool) -> Self {
self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating));
self
}
}
// **< Textarea SETUP >*****************************************************************************
pub(crate) fn setup(field: &mut Textarea) {
if field.props().extra_or(EXTRA_FLOATING_LABEL, false) {
field.alter_prop(PropsOp::add_classes("form-floating"));
field.alter_rows(None::<u16>);
} else {
field.alter_prop(PropsOp::remove_classes("form-floating"));
}
}
// **< Textarea RENDER >****************************************************************************
pub(crate) fn render(field: &Textarea, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = field.id();
let textarea_id = container_id
.as_deref()
.map(|id| util::join!(id, "-textarea"));
let floating = field.props().extra_or(EXTRA_FLOATING_LABEL, false);
// La etiqueta flotante requiere `placeholder` para animar la etiqueta; si no está definido se
// fuerza `placeholder=""`.
let placeholder = if floating {
Some(field.placeholder().lookup(cx).unwrap_or_default())
} else {
field.placeholder().lookup(cx)
};
let label = match field.label().lookup(cx) {
Some(text) => html! {
label for=[textarea_id.as_deref()] class="form-label" {
(text)
@if *field.required() {
span
class="form-required"
title=(L10n::l("field_required").using(cx))
{
"*"
}
}
}
},
None => html! {},
};
Ok(html! {
div (field.props()) {
@if !floating { (label) }
textarea
id=[textarea_id.as_deref()]
class="form-control"
name=[field.name().get()]
rows=[field.rows().get()]
minlength=[field.minlength().get()]
maxlength=[field.maxlength().get()]
placeholder=[placeholder]
autocomplete=[field.autocomplete().get()]
autofocus[*field.autofocus()]
readonly[*field.readonly()]
required[*field.required()]
disabled[*field.disabled()]
{
@if let Some(value) = field.value().get() { (value) }
}
@if floating { (label) }
@if let Some(description) = field.help_text().lookup(cx) {
div class="form-text" { (description) }
}
}
})
}