From e8bba0803d84214ad92c16fc3f744b2f81158cc9 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Wed, 5 Aug 2026 17:33:27 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(form):=20A=C3=B1ade=20nuevo=20comp?= =?UTF-8?q?onente=20Number?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base/component/form.rs | 7 +- src/base/component/form/number.rs | 219 ++++++++++++++++++++++++++++++ src/base/component/form/range.rs | 24 ++-- 3 files changed, 236 insertions(+), 14 deletions(-) create mode 100644 src/base/component/form/number.rs diff --git a/src/base/component/form.rs b/src/base/component/form.rs index 82c603ef..6e927367 100644 --- a/src/base/component/form.rs +++ b/src/base/component/form.rs @@ -20,11 +20,14 @@ pub mod select; pub mod input; -mod textarea; -pub use textarea::Textarea; +mod number; +pub use number::Number; mod range; pub use range::Range; +mod textarea; +pub use textarea::Textarea; + mod hidden; pub use hidden::Hidden; diff --git a/src/base/component/form/number.rs b/src/base/component/form/number.rs new file mode 100644 index 00000000..f8ad1c94 --- /dev/null +++ b/src/base/component/form/number.rs @@ -0,0 +1,219 @@ +use crate::prelude::*; + +/// Componente para crear un **campo numérico**. +/// +/// Renderiza un campo de entrada numérica (`input type="number"`) con una etiqueta opcional y un +/// texto de ayuda. El navegador restringe la entrada a valores numéricos y ofrece controles nativos +/// para incrementar o decrementar el valor. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let quantity = form::Number::new() +/// .with_name("quantity") +/// .with_label(L10n::n("Quantity")) +/// .with_min(Some(1)) +/// .with_max(Some(100)) +/// .with_value(Some(1)); +/// ``` +/// +/// Al enviar el formulario el navegador transmite `name=valor`. Un campo numérico siempre envía su +/// valor. En el servidor se deserializa como `u64`: +/// +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct FormData { +/// quantity: u64, // Siempre presente con el valor numérico introducido. +/// } +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Number { + /// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente. + props: Props, + /// Devuelve el nombre del campo. + name: AttrName, + /// Devuelve el valor inicial del campo. + value: Attr, + /// Devuelve la etiqueta del campo. + label: Attr, + /// Devuelve el texto de ayuda del campo. + help_text: Attr, + /// Devuelve el valor mínimo permitido. + min: Attr, + /// Devuelve el valor máximo permitido. + max: Attr, + /// Devuelve el incremento entre valores del campo. + step: Attr, + /// Devuelve si el campo recibe el foco automáticamente al cargar la página. + autofocus: bool, + /// Devuelve si el campo es de sólo lectura. + readonly: bool, + /// Devuelve si el campo es obligatorio. + required: bool, + /// Devuelve si el campo está deshabilitado. + disabled: bool, +} + +#[async_trait] +impl Component for Number { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.props.get_id() + } + + fn setup(&mut self, _cx: &Context) { + if let Some(container_id) = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) + { + self.alter_prop(PropsOp::ensure_id(container_id)); + } + + // Clases CSS del contenedor del campo numérico. + self.alter_prop(PropsOp::prepend_classes("form-field form-field-number")); + } + + async fn prepare(&self, cx: &mut Context) -> Result { + let container_id = self.id(); + let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); + Ok(html! { + div (self.props()) { + @if let Some(label) = self.label().lookup(cx) { + label for=[input_id.as_deref()] class="form-label" { + (label) + @if *self.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + } + input + type="number" + id=[input_id.as_deref()] + class="form-control" + name=[self.name().get()] + min=[self.min().get()] + max=[self.max().get()] + step=[self.step().get()] + value=[self.value().get()] + autofocus[*self.autofocus()] + readonly[*self.readonly()] + required[*self.required()] + disabled[*self.disabled()]; + @if let Some(description) = self.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) + } +} + +impl Number { + // **< Number 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) -> 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 campo (atributo `name`). + /// + /// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. 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 el valor inicial del campo. + #[builder_fn] + pub fn with_value(mut self, value: Option) -> Self { + self.value.alter_opt(value); + self + } + + /// Establece o elimina la etiqueta visible del campo (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 campo (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 + } + + /// Establece el valor mínimo permitido (`None` para no imponer mínimo). + #[builder_fn] + pub fn with_min(mut self, min: Option) -> Self { + self.min.alter_opt(min); + self + } + + /// Establece el valor máximo permitido (`None` para no imponer máximo). + #[builder_fn] + pub fn with_max(mut self, max: Option) -> Self { + self.max.alter_opt(max); + self + } + + /// Establece el incremento entre valores del campo. + /// + /// Pasar `None` omite el atributo `step` y deja que el navegador aplique su valor por defecto + /// (normalmente `1`). + #[builder_fn] + pub fn with_step(mut self, step: Option) -> Self { + self.step.alter_opt(step); + self + } + + /// Establece si el campo 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 campo es de sólo lectura. + #[builder_fn] + pub fn with_readonly(mut self, readonly: bool) -> Self { + self.readonly = readonly; + 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 campo está deshabilitado. + #[builder_fn] + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} diff --git a/src/base/component/form/range.rs b/src/base/component/form/range.rs index 51e8b385..97806665 100644 --- a/src/base/component/form/range.rs +++ b/src/base/component/form/range.rs @@ -35,6 +35,8 @@ pub struct Range { props: Props, /// Devuelve el nombre del campo. name: AttrName, + /// Devuelve el valor inicial del campo. + value: Attr, /// Devuelve la etiqueta del campo. label: Attr, /// Devuelve el texto de ayuda del campo. @@ -45,8 +47,6 @@ pub struct Range { max: Attr, /// Devuelve el incremento entre valores del campo. step: Attr, - /// Devuelve el valor inicial del campo. - value: Attr, /// Devuelve si el control recibe el foco automáticamente al cargar la página. autofocus: bool, /// Devuelve si el control está deshabilitado. @@ -129,6 +129,16 @@ impl Range { self } + /// Establece el valor inicial del campo. + /// + /// Pasar `None` omite el atributo `value` y deja que el navegador aplique su valor por defecto + /// (normalmente el punto medio del rango). + #[builder_fn] + pub fn with_value(mut self, value: Option) -> Self { + self.value.alter_opt(value); + self + } + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). #[builder_fn] pub fn with_label(mut self, label: impl Into>) -> Self { @@ -171,16 +181,6 @@ impl Range { self } - /// Establece el valor inicial del campo. - /// - /// Pasar `None` omite el atributo `value` y deja que el navegador aplique su valor por defecto - /// (normalmente el punto medio del rango). - #[builder_fn] - pub fn with_value(mut self, value: Option) -> Self { - self.value.alter_opt(value); - self - } - /// Establece si el control recibe el foco automáticamente al cargar la página. #[builder_fn] pub fn with_autofocus(mut self, autofocus: bool) -> Self {