♻️ (pagetop): Sustituye Attr<T> por Option<T>

`Attr<T>` no aportaba nada sobre `Option<T>` directo: `Getters` ya trata
los campos `Option<T>` como caso especial (`Option<&T>`, o `Option<T>`
con `#[getters(copy)]`) y `#[builder_fn]` funciona igual sobre ellos.
This commit is contained in:
Manuel Cillero 2026-08-17 05:28:12 +02:00
parent 213aa18b12
commit bf4c635e59
21 changed files with 171 additions and 245 deletions

View file

@ -63,9 +63,9 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result<Markup, Componen
let strict = field.kind().is_strict(); let strict = field.kind().is_strict();
let masked = *field.kind() == Kind::StrictPassword; let masked = *field.kind() == Kind::StrictPassword;
let autocomplete = if strict { let autocomplete = if strict {
Some(form::Autocomplete::Off) Some(&form::Autocomplete::Off)
} else { } else {
field.autocomplete().get() field.autocomplete()
}; };
// La etiqueta flotante requiere `placeholder` para animar la etiqueta. // La etiqueta flotante requiere `placeholder` para animar la etiqueta.
@ -100,12 +100,12 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result<Markup, Componen
type=(field.kind()) type=(field.kind())
id=[input_id.as_deref()] id=[input_id.as_deref()]
class=(input_class) class=(input_class)
name=[field.name().get()] name=[field.name().as_deref()]
value=[field.value().get()] value=[field.value().as_deref()]
minlength=[field.minlength().get()] minlength=[field.minlength()]
maxlength=[field.maxlength().get()] maxlength=[field.maxlength()]
placeholder=[placeholder] placeholder=[placeholder]
inputmode=[field.inputmode().get()] inputmode=[field.inputmode()]
autocomplete=[autocomplete] autocomplete=[autocomplete]
spellcheck=[strict.then_some("false")] spellcheck=[strict.then_some("false")]
autocorrect=[strict.then_some("off")] autocorrect=[strict.then_some("off")]

View file

@ -85,10 +85,10 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result<Markup, Componen
select select
id=[select_id.as_deref()] id=[select_id.as_deref()]
class="form-select" class="form-select"
name=[field.name().get()] name=[field.name().as_deref()]
multiple[*field.multiple()] multiple[*field.multiple()]
size=[field.rows().get()] size=[field.rows()]
autocomplete=[field.autocomplete().get()] autocomplete=[field.autocomplete()]
autofocus[*field.autofocus()] autofocus[*field.autofocus()]
required[*field.required()] required[*field.required()]
disabled[*field.disabled()] disabled[*field.disabled()]
@ -97,7 +97,7 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result<Markup, Componen
@match entry { @match entry {
form::select::Entry::Item(opt) => { form::select::Entry::Item(opt) => {
option option
value=(opt.value().as_str().unwrap_or("")) value=(opt.value().as_deref().unwrap_or(""))
selected[*opt.selected()] selected[*opt.selected()]
disabled[*opt.disabled()] disabled[*opt.disabled()]
{ {
@ -111,7 +111,7 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result<Markup, Componen
{ {
@for opt in group.items() { @for opt in group.items() {
option option
value=(opt.value().as_str().unwrap_or("")) value=(opt.value().as_deref().unwrap_or(""))
selected[*opt.selected()] selected[*opt.selected()]
disabled[*opt.disabled()] disabled[*opt.disabled()]
{ {

View file

@ -91,18 +91,18 @@ pub(crate) fn render(field: &Textarea, cx: &mut Context) -> Result<Markup, Compo
textarea textarea
id=[textarea_id.as_deref()] id=[textarea_id.as_deref()]
class="form-control" class="form-control"
name=[field.name().get()] name=[field.name().as_deref()]
rows=[field.rows().get()] rows=[field.rows()]
minlength=[field.minlength().get()] minlength=[field.minlength()]
maxlength=[field.maxlength().get()] maxlength=[field.maxlength()]
placeholder=[placeholder] placeholder=[placeholder]
autocomplete=[field.autocomplete().get()] autocomplete=[field.autocomplete()]
autofocus[*field.autofocus()] autofocus[*field.autofocus()]
readonly[*field.readonly()] readonly[*field.readonly()]
required[*field.required()] required[*field.required()]
disabled[*field.disabled()] disabled[*field.disabled()]
{ {
@if let Some(value) = field.value().get() { (value) } @if let Some(value) = field.value().as_deref() { (value) }
} }
@if floating { (label) } @if floating { (label) }
@if let Some(description) = field.help_text().lookup(cx) { @if let Some(description) = field.help_text().lookup(cx) {

View file

@ -102,8 +102,8 @@ impl Component for Button {
button button
type=(self.kind()) type=(self.kind())
(self.props()) (self.props())
name=[self.name().get()] name=[self.name().as_deref()]
value=[self.value().get()] value=[self.value().as_deref()]
title=[self.title().lookup(cx)] title=[self.title().lookup(cx)]
autofocus[*self.autofocus()] autofocus[*self.autofocus()]
disabled[*self.disabled()] disabled[*self.disabled()]

View file

@ -141,7 +141,7 @@ impl Component for Field {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
let name = self.name().get().unwrap(); let name = self.name().as_deref().unwrap();
let container_id = self.id().unwrap(); let container_id = self.id().unwrap();
Ok(html! { Ok(html! {
@ -162,8 +162,8 @@ impl Component for Field {
type="checkbox" type="checkbox"
id=(&item_id) id=(&item_id)
class="form-check-input" class="form-check-input"
name=(&name) name=(name)
value=[item.value().get()] value=[item.value().as_deref()]
checked[*item.checked()] checked[*item.checked()]
disabled[*item.disabled() || *self.disabled()]; disabled[*item.disabled() || *self.disabled()];
label class="form-check-label" for=(&item_id) { label class="form-check-label" for=(&item_id) {

View file

@ -98,7 +98,7 @@ impl Component for Checkbox {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
let name = self.name().get().unwrap(); let name = self.name().as_deref().unwrap();
let container_id = self.id().unwrap(); let container_id = self.id().unwrap();
let checkbox_id = util::join!(&container_id, "-checkbox"); let checkbox_id = util::join!(&container_id, "-checkbox");
@ -111,7 +111,7 @@ impl Component for Checkbox {
role=[is_switch.then_some("switch")] role=[is_switch.then_some("switch")]
id=(&checkbox_id) id=(&checkbox_id)
class="form-check-input" class="form-check-input"
name=(&name) name=(name)
value="true" value="true"
checked[*self.checked()] checked[*self.checked()]
autofocus[*self.autofocus()] autofocus[*self.autofocus()]

View file

@ -81,7 +81,7 @@ impl Component for Form {
(self.props()) (self.props())
action=[self.action().try_resolve(cx)] action=[self.action().try_resolve(cx)]
method=[method] method=[method]
accept-charset=[self.charset().get()] accept-charset=[self.charset().as_deref()]
{ {
(self.children().render(cx).await) (self.children().render(cx).await)
} }

View file

@ -48,8 +48,8 @@ impl Component for Hidden {
Ok(html! { Ok(html! {
input input
type="hidden" type="hidden"
name=[self.name().get()] name=[self.name().as_deref()]
value=[self.value().get()]; value=[self.value().as_deref()];
}) })
} }
} }

View file

@ -165,13 +165,15 @@ pub struct Field {
/// Devuelve el texto de ayuda del campo. /// Devuelve el texto de ayuda del campo.
help_text: Lc, help_text: Lc,
/// Devuelve la longitud mínima permitida en caracteres. /// Devuelve la longitud mínima permitida en caracteres.
minlength: Attr<u16>, #[getters(copy)]
minlength: Option<u16>,
/// Devuelve la longitud máxima permitida en caracteres. /// Devuelve la longitud máxima permitida en caracteres.
maxlength: Attr<u16>, #[getters(copy)]
maxlength: Option<u16>,
/// Devuelve el texto indicativo del campo. /// Devuelve el texto indicativo del campo.
placeholder: Lc, placeholder: Lc,
/// Devuelve la configuración de autocompletado del campo. /// Devuelve la configuración de autocompletado del campo.
autocomplete: Attr<form::Autocomplete>, autocomplete: Option<form::Autocomplete>,
/// Devuelve si el campo recibe el foco automáticamente al cargar la página. /// Devuelve si el campo recibe el foco automáticamente al cargar la página.
autofocus: bool, autofocus: bool,
/// Devuelve si el campo es de sólo lectura. /// Devuelve si el campo es de sólo lectura.
@ -183,7 +185,8 @@ pub struct Field {
/// Devuelve si el campo se muestra como texto plano sin bordes ni fondo. /// Devuelve si el campo se muestra como texto plano sin bordes ni fondo.
plaintext: bool, plaintext: bool,
/// Devuelve la sugerencia de teclado virtual para el campo. /// Devuelve la sugerencia de teclado virtual para el campo.
inputmode: Attr<Mode>, #[getters(copy)]
inputmode: Option<Mode>,
} }
#[async_trait] #[async_trait]
@ -199,7 +202,7 @@ impl Component for Field {
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().get().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))
{ {
self.alter_prop(PropsOp::ensure_id(container_id)); self.alter_prop(PropsOp::ensure_id(container_id));
} }
@ -225,9 +228,9 @@ impl Component for Field {
let strict = self.kind().is_strict(); let strict = self.kind().is_strict();
let masked = *self.kind() == Kind::StrictPassword; let masked = *self.kind() == Kind::StrictPassword;
let autocomplete = if strict { let autocomplete = if strict {
Some(form::Autocomplete::Off) Some(&form::Autocomplete::Off)
} else { } else {
self.autocomplete().get() self.autocomplete()
}; };
Ok(html! { Ok(html! {
@ -249,12 +252,12 @@ impl Component for Field {
type=(self.kind()) type=(self.kind())
id=[input_id.as_deref()] id=[input_id.as_deref()]
class=(input_class) class=(input_class)
name=[self.name().get()] name=[self.name().as_deref()]
value=[self.value().get()] value=[self.value().as_deref()]
minlength=[self.minlength().get()] minlength=[self.minlength()]
maxlength=[self.maxlength().get()] maxlength=[self.maxlength()]
placeholder=[self.placeholder().lookup(cx)] placeholder=[self.placeholder().lookup(cx)]
inputmode=[self.inputmode().get()] inputmode=[self.inputmode()]
autocomplete=[autocomplete] autocomplete=[autocomplete]
spellcheck=[strict.then_some("false")] spellcheck=[strict.then_some("false")]
autocorrect=[strict.then_some("off")] autocorrect=[strict.then_some("off")]
@ -434,15 +437,15 @@ impl Field {
/// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo). /// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo).
#[builder_fn] #[builder_fn]
pub fn with_minlength(mut self, minlength: Option<u16>) -> Self { pub fn with_minlength(mut self, minlength: impl Into<Option<u16>>) -> Self {
self.minlength.alter_opt(minlength); self.minlength = minlength.into();
self self
} }
/// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite). /// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite).
#[builder_fn] #[builder_fn]
pub fn with_maxlength(mut self, maxlength: Option<u16>) -> Self { pub fn with_maxlength(mut self, maxlength: impl Into<Option<u16>>) -> Self {
self.maxlength.alter_opt(maxlength); self.maxlength = maxlength.into();
self self
} }
@ -462,8 +465,11 @@ impl Field {
/// [`Autocomplete::email()`](form::Autocomplete::email) o /// [`Autocomplete::email()`](form::Autocomplete::email) o
/// [`Autocomplete::current_password()`](form::Autocomplete::current_password)). /// [`Autocomplete::current_password()`](form::Autocomplete::current_password)).
#[builder_fn] #[builder_fn]
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self { pub fn with_autocomplete(
self.autocomplete.alter_opt(autocomplete); mut self,
autocomplete: impl Into<Option<form::Autocomplete>>,
) -> Self {
self.autocomplete = autocomplete.into();
self self
} }
@ -510,8 +516,8 @@ impl Field {
/// A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los valores aceptados /// A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los valores aceptados
/// ni activa la validación del navegador; es sólo una sugerencia de presentación. /// ni activa la validación del navegador; es sólo una sugerencia de presentación.
#[builder_fn] #[builder_fn]
pub fn with_inputmode(mut self, inputmode: Option<Mode>) -> Self { pub fn with_inputmode(mut self, inputmode: impl Into<Option<Mode>>) -> Self {
self.inputmode.alter_opt(inputmode); self.inputmode = inputmode.into();
self self
} }
} }

View file

@ -35,17 +35,21 @@ pub struct Number {
/// Devuelve el nombre del campo. /// Devuelve el nombre del campo.
name: AttrName, name: AttrName,
/// Devuelve el valor inicial del campo. /// Devuelve el valor inicial del campo.
value: Attr<u64>, #[getters(copy)]
value: Option<u64>,
/// Devuelve la etiqueta del campo. /// Devuelve la etiqueta del campo.
label: Lc, label: Lc,
/// Devuelve el texto de ayuda del campo. /// Devuelve el texto de ayuda del campo.
help_text: Lc, help_text: Lc,
/// Devuelve el valor mínimo permitido. /// Devuelve el valor mínimo permitido.
min: Attr<u64>, #[getters(copy)]
min: Option<u64>,
/// Devuelve el valor máximo permitido. /// Devuelve el valor máximo permitido.
max: Attr<u64>, #[getters(copy)]
max: Option<u64>,
/// Devuelve el incremento entre valores del campo. /// Devuelve el incremento entre valores del campo.
step: Attr<u64>, #[getters(copy)]
step: Option<u64>,
/// Devuelve si el campo recibe el foco automáticamente al cargar la página. /// Devuelve si el campo recibe el foco automáticamente al cargar la página.
autofocus: bool, autofocus: bool,
/// Devuelve si el campo es de sólo lectura. /// Devuelve si el campo es de sólo lectura.
@ -69,7 +73,7 @@ impl Component for Number {
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().get().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))
{ {
self.alter_prop(PropsOp::ensure_id(container_id)); self.alter_prop(PropsOp::ensure_id(container_id));
} }
@ -100,11 +104,11 @@ impl Component for Number {
type="number" type="number"
id=[input_id.as_deref()] id=[input_id.as_deref()]
class="form-control" class="form-control"
name=[self.name().get()] name=[self.name().as_deref()]
min=[self.min().get()] min=[self.min()]
max=[self.max().get()] max=[self.max()]
step=[self.step().get()] step=[self.step()]
value=[self.value().get()] value=[self.value()]
autofocus[*self.autofocus()] autofocus[*self.autofocus()]
readonly[*self.readonly()] readonly[*self.readonly()]
required[*self.required()] required[*self.required()]
@ -146,8 +150,8 @@ impl Number {
/// Establece el valor inicial del campo. /// Establece el valor inicial del campo.
#[builder_fn] #[builder_fn]
pub fn with_value(mut self, value: Option<u64>) -> Self { pub fn with_value(mut self, value: impl Into<Option<u64>>) -> Self {
self.value.alter_opt(value); self.value = value.into();
self self
} }
@ -167,15 +171,15 @@ impl Number {
/// Establece el valor mínimo permitido (`None` para no imponer mínimo). /// Establece el valor mínimo permitido (`None` para no imponer mínimo).
#[builder_fn] #[builder_fn]
pub fn with_min(mut self, min: Option<u64>) -> Self { pub fn with_min(mut self, min: impl Into<Option<u64>>) -> Self {
self.min.alter_opt(min); self.min = min.into();
self self
} }
/// Establece el valor máximo permitido (`None` para no imponer máximo). /// Establece el valor máximo permitido (`None` para no imponer máximo).
#[builder_fn] #[builder_fn]
pub fn with_max(mut self, max: Option<u64>) -> Self { pub fn with_max(mut self, max: impl Into<Option<u64>>) -> Self {
self.max.alter_opt(max); self.max = max.into();
self self
} }
@ -184,8 +188,8 @@ impl Number {
/// Pasar `None` omite el atributo `step` y deja que el navegador aplique su valor por defecto /// Pasar `None` omite el atributo `step` y deja que el navegador aplique su valor por defecto
/// (normalmente `1`). /// (normalmente `1`).
#[builder_fn] #[builder_fn]
pub fn with_step(mut self, step: Option<u64>) -> Self { pub fn with_step(mut self, step: impl Into<Option<u64>>) -> Self {
self.step.alter_opt(step); self.step = step.into();
self self
} }

View file

@ -141,7 +141,7 @@ impl Component for Field {
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
let name = self.name().get().unwrap(); let name = self.name().as_deref().unwrap();
let container_id = self.id().unwrap(); let container_id = self.id().unwrap();
Ok(html! { Ok(html! {
@ -178,8 +178,8 @@ impl Component for Field {
type="radio" type="radio"
id=(&item_id) id=(&item_id)
class="form-check-input" class="form-check-input"
name=(&name) name=(name)
value=[item.value().get()] value=[item.value().as_deref()]
checked[checked] checked[checked]
required[*self.required()] required[*self.required()]
disabled[*item.disabled() || *self.disabled()]; disabled[*item.disabled() || *self.disabled()];

View file

@ -36,17 +36,21 @@ pub struct Range {
/// Devuelve el nombre del campo. /// Devuelve el nombre del campo.
name: AttrName, name: AttrName,
/// Devuelve el valor inicial del campo. /// Devuelve el valor inicial del campo.
value: Attr<f64>, #[getters(copy)]
value: Option<f64>,
/// Devuelve la etiqueta del campo. /// Devuelve la etiqueta del campo.
label: Lc, label: Lc,
/// Devuelve el texto de ayuda del campo. /// Devuelve el texto de ayuda del campo.
help_text: Lc, help_text: Lc,
/// Devuelve el valor mínimo permitido. /// Devuelve el valor mínimo permitido.
min: Attr<f64>, #[getters(copy)]
min: Option<f64>,
/// Devuelve el valor máximo permitido. /// Devuelve el valor máximo permitido.
max: Attr<f64>, #[getters(copy)]
max: Option<f64>,
/// Devuelve el incremento entre valores del campo. /// Devuelve el incremento entre valores del campo.
step: Attr<f64>, #[getters(copy)]
step: Option<f64>,
/// Devuelve si el control recibe el foco automáticamente al cargar la página. /// Devuelve si el control recibe el foco automáticamente al cargar la página.
autofocus: bool, autofocus: bool,
/// Devuelve si el control está deshabilitado. /// Devuelve si el control está deshabilitado.
@ -66,7 +70,7 @@ impl Component for Range {
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().get().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))
{ {
self.alter_prop(PropsOp::ensure_id(container_id)); self.alter_prop(PropsOp::ensure_id(container_id));
}; };
@ -87,11 +91,11 @@ impl Component for Range {
type="range" type="range"
id=[range_id.as_deref()] id=[range_id.as_deref()]
class="form-range" class="form-range"
name=[self.name().get()] name=[self.name().as_deref()]
min=[self.min().get()] min=[self.min()]
max=[self.max().get()] max=[self.max()]
step=[self.step().get()] step=[self.step()]
value=[self.value().get()] value=[self.value()]
autofocus[*self.autofocus()] autofocus[*self.autofocus()]
disabled[*self.disabled()]; disabled[*self.disabled()];
@if let Some(description) = self.help_text().lookup(cx) { @if let Some(description) = self.help_text().lookup(cx) {
@ -134,8 +138,8 @@ impl Range {
/// Pasar `None` omite el atributo `value` y deja que el navegador aplique su valor por defecto /// Pasar `None` omite el atributo `value` y deja que el navegador aplique su valor por defecto
/// (normalmente el punto medio del rango). /// (normalmente el punto medio del rango).
#[builder_fn] #[builder_fn]
pub fn with_value(mut self, value: Option<f64>) -> Self { pub fn with_value(mut self, value: impl Into<Option<f64>>) -> Self {
self.value.alter_opt(value); self.value = value.into();
self self
} }
@ -157,8 +161,8 @@ impl Range {
/// ///
/// Pasar `None` omite el atributo `min` y deja que el navegador aplique su valor por defecto. /// Pasar `None` omite el atributo `min` y deja que el navegador aplique su valor por defecto.
#[builder_fn] #[builder_fn]
pub fn with_min(mut self, min: Option<f64>) -> Self { pub fn with_min(mut self, min: impl Into<Option<f64>>) -> Self {
self.min.alter_opt(min); self.min = min.into();
self self
} }
@ -166,8 +170,8 @@ impl Range {
/// ///
/// Pasar `None` omite el atributo `max` y deja que el navegador aplique su valor por defecto. /// Pasar `None` omite el atributo `max` y deja que el navegador aplique su valor por defecto.
#[builder_fn] #[builder_fn]
pub fn with_max(mut self, max: Option<f64>) -> Self { pub fn with_max(mut self, max: impl Into<Option<f64>>) -> Self {
self.max.alter_opt(max); self.max = max.into();
self self
} }
@ -176,8 +180,8 @@ impl Range {
/// Pasar `None` omite el atributo `step` y deja que el navegador aplique su valor por defecto /// Pasar `None` omite el atributo `step` y deja que el navegador aplique su valor por defecto
/// (normalmente `1`). /// (normalmente `1`).
#[builder_fn] #[builder_fn]
pub fn with_step(mut self, step: Option<f64>) -> Self { pub fn with_step(mut self, step: impl Into<Option<f64>>) -> Self {
self.step.alter_opt(step); self.step = step.into();
self self
} }

View file

@ -205,9 +205,10 @@ pub struct Field {
/// Devuelve si la lista permite selección múltiple. /// Devuelve si la lista permite selección múltiple.
multiple: bool, multiple: bool,
/// Devuelve el número de filas visibles de la lista de selección. /// Devuelve el número de filas visibles de la lista de selección.
rows: Attr<u16>, #[getters(copy)]
rows: Option<u16>,
/// Devuelve la configuración de autocompletado del campo. /// Devuelve la configuración de autocompletado del campo.
autocomplete: Attr<form::Autocomplete>, autocomplete: Option<form::Autocomplete>,
/// Devuelve si la lista recibe el foco automáticamente al cargar la página. /// Devuelve si la lista recibe el foco automáticamente al cargar la página.
autofocus: bool, autofocus: bool,
/// Devuelve si la selección de un elemento es obligatoria. /// Devuelve si la selección de un elemento es obligatoria.
@ -229,7 +230,7 @@ impl Component for Field {
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().get().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))
{ {
self.alter_prop(PropsOp::ensure_id(container_id)); self.alter_prop(PropsOp::ensure_id(container_id));
} }
@ -260,10 +261,10 @@ impl Component for Field {
select select
id=[select_id.as_deref()] id=[select_id.as_deref()]
class="form-select" class="form-select"
name=[self.name().get()] name=[self.name().as_deref()]
multiple[*self.multiple()] multiple[*self.multiple()]
size=[self.rows().get()] size=[self.rows()]
autocomplete=[self.autocomplete().get()] autocomplete=[self.autocomplete()]
autofocus[*self.autofocus()] autofocus[*self.autofocus()]
required[*self.required()] required[*self.required()]
disabled[*self.disabled()] disabled[*self.disabled()]
@ -272,7 +273,7 @@ impl Component for Field {
@match entry { @match entry {
Entry::Item(opt) => { Entry::Item(opt) => {
option option
value=(opt.value().as_str().unwrap_or("")) value=(opt.value().as_deref().unwrap_or(""))
selected[*opt.selected()] selected[*opt.selected()]
disabled[*opt.disabled()] disabled[*opt.disabled()]
{ {
@ -286,7 +287,7 @@ impl Component for Field {
{ {
@for opt in group.items() { @for opt in group.items() {
option option
value=(opt.value().as_str().unwrap_or("")) value=(opt.value().as_deref().unwrap_or(""))
selected[*opt.selected()] selected[*opt.selected()]
disabled[*opt.disabled()] disabled[*opt.disabled()]
{ {
@ -389,8 +390,8 @@ impl Field {
/// Es especialmente útil con selección múltiple para controlar el número de filas visibles sin /// Es especialmente útil con selección múltiple para controlar el número de filas visibles sin
/// necesidad de recurrir al desplazamiento. /// necesidad de recurrir al desplazamiento.
#[builder_fn] #[builder_fn]
pub fn with_rows(mut self, rows: Option<u16>) -> Self { pub fn with_rows(mut self, rows: impl Into<Option<u16>>) -> Self {
self.rows.alter_opt(rows); self.rows = rows.into();
self self
} }
@ -404,8 +405,11 @@ impl Field {
/// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para /// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para
/// omitir el atributo. /// omitir el atributo.
#[builder_fn] #[builder_fn]
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self { pub fn with_autocomplete(
self.autocomplete.alter_opt(autocomplete); mut self,
autocomplete: impl Into<Option<form::Autocomplete>>,
) -> Self {
self.autocomplete = autocomplete.into();
self self
} }

View file

@ -44,15 +44,18 @@ pub struct Textarea {
/// Devuelve el texto de ayuda del campo. /// Devuelve el texto de ayuda del campo.
help_text: Lc, help_text: Lc,
/// Devuelve el número de filas visibles del área de texto. /// Devuelve el número de filas visibles del área de texto.
rows: Attr<u16>, #[getters(copy)]
rows: Option<u16>,
/// Devuelve la longitud mínima permitida en caracteres. /// Devuelve la longitud mínima permitida en caracteres.
minlength: Attr<u16>, #[getters(copy)]
minlength: Option<u16>,
/// Devuelve la longitud máxima permitida en caracteres. /// Devuelve la longitud máxima permitida en caracteres.
maxlength: Attr<u16>, #[getters(copy)]
maxlength: Option<u16>,
/// Devuelve el texto indicativo del área de texto. /// Devuelve el texto indicativo del área de texto.
placeholder: Lc, placeholder: Lc,
/// Devuelve la configuración de autocompletado del campo. /// Devuelve la configuración de autocompletado del campo.
autocomplete: Attr<form::Autocomplete>, autocomplete: Option<form::Autocomplete>,
/// Devuelve si el campo recibe el foco automáticamente al cargar la página. /// Devuelve si el campo recibe el foco automáticamente al cargar la página.
autofocus: bool, autofocus: bool,
/// Devuelve si el campo es de sólo lectura. /// Devuelve si el campo es de sólo lectura.
@ -76,7 +79,7 @@ impl Component for Textarea {
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
if let Some(container_id) = self if let Some(container_id) = self
.id() .id()
.or_else(|| self.name().get().map(|n| util::join!("edit-", n))) .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n)))
{ {
self.alter_prop(PropsOp::ensure_id(container_id)); self.alter_prop(PropsOp::ensure_id(container_id));
} }
@ -109,18 +112,18 @@ impl Component for Textarea {
textarea textarea
id=[textarea_id.as_deref()] id=[textarea_id.as_deref()]
class="form-control" class="form-control"
name=[self.name().get()] name=[self.name().as_deref()]
rows=[self.rows().get()] rows=[self.rows()]
minlength=[self.minlength().get()] minlength=[self.minlength()]
maxlength=[self.maxlength().get()] maxlength=[self.maxlength()]
placeholder=[self.placeholder().lookup(cx)] placeholder=[self.placeholder().lookup(cx)]
autocomplete=[self.autocomplete().get()] autocomplete=[self.autocomplete()]
autofocus[*self.autofocus()] autofocus[*self.autofocus()]
readonly[*self.readonly()] readonly[*self.readonly()]
required[*self.required()] required[*self.required()]
disabled[*self.disabled()] disabled[*self.disabled()]
{ {
@if let Some(value) = self.value().get() { @if let Some(value) = self.value().as_deref() {
(value) (value)
} }
} }
@ -185,22 +188,22 @@ impl Textarea {
/// Sin valor o pasando `None`, el área muestra su altura predeterminada, dos filas según el /// Sin valor o pasando `None`, el área muestra su altura predeterminada, dos filas según el
/// estándar. /// estándar.
#[builder_fn] #[builder_fn]
pub fn with_rows(mut self, rows: Option<u16>) -> Self { pub fn with_rows(mut self, rows: impl Into<Option<u16>>) -> Self {
self.rows.alter_opt(rows); self.rows = rows.into();
self self
} }
/// Establece la longitud mínima permitida en caracteres. /// Establece la longitud mínima permitida en caracteres.
#[builder_fn] #[builder_fn]
pub fn with_minlength(mut self, minlength: Option<u16>) -> Self { pub fn with_minlength(mut self, minlength: impl Into<Option<u16>>) -> Self {
self.minlength.alter_opt(minlength); self.minlength = minlength.into();
self self
} }
/// Establece la longitud máxima permitida en caracteres. /// Establece la longitud máxima permitida en caracteres.
#[builder_fn] #[builder_fn]
pub fn with_maxlength(mut self, maxlength: Option<u16>) -> Self { pub fn with_maxlength(mut self, maxlength: impl Into<Option<u16>>) -> Self {
self.maxlength.alter_opt(maxlength); self.maxlength = maxlength.into();
self self
} }
@ -222,8 +225,11 @@ impl Textarea {
/// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para /// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para
/// omitir el atributo. /// omitir el atributo.
#[builder_fn] #[builder_fn]
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self { pub fn with_autocomplete(
self.autocomplete.alter_opt(autocomplete); mut self,
autocomplete: impl Into<Option<form::Autocomplete>>,
) -> Self {
self.autocomplete = autocomplete.into();
self self
} }

View file

@ -263,8 +263,8 @@ impl Intro {
/// let intro_no_button = Intro::default().with_button(None); /// let intro_no_button = Intro::default().with_button(None);
/// ``` /// ```
#[builder_fn] #[builder_fn]
pub fn with_button(mut self, button: Option<(Lc, Route)>) -> Self { pub fn with_button(mut self, button: impl Into<Option<(Lc, Route)>>) -> Self {
self.button = button; self.button = button.into();
self self
} }

View file

@ -203,11 +203,11 @@ impl Component for Pager {
return Ok(html! {}); return Ok(html! {});
} }
let page = self.current_page().clamp(1, total_pages); let page = self.current_page().clamp(1, total_pages);
let base_path = self.base_path().as_str().unwrap_or_default(); let base_path = self.base_path().as_deref().unwrap_or("");
// Ruta común a los enlaces del paginador, con los parámetros de `extra_query` añadidos a // Ruta común a los enlaces del paginador, con los parámetros de `extra_query` añadidos a
// `base_path`. Pasa por `cx.route()` para preservar el parámetro `lang` si corresponde. // `base_path`. Pasa por `cx.route()` para preservar el parámetro `lang` si corresponde.
let mut route = cx.route(base_path.to_owned()); let mut route = cx.route(base_path);
for (key, value) in self.extra_query() { for (key, value) in self.extra_query() {
route.alter_param(key, value); route.alter_param(key, value);
} }

View file

@ -99,7 +99,7 @@ impl Column {
html! { html! {
th (self.props()) scope="col" aria-sort=(aria_sort) { th (self.props()) scope="col" aria-sort=(aria_sort) {
a href=[sort.href().as_str()] (link_props) { (label) } a href=[sort.href().as_deref()] (link_props) { (label) }
} }
} }
} }

View file

@ -21,7 +21,7 @@ use crate::prelude::*;
/// .with_prop(PropsOp::set("data-sort", "email")); /// .with_prop(PropsOp::set("data-sort", "email"));
/// ///
/// assert_eq!(link.props().get_id(), Some("sort-email".to_string())); /// assert_eq!(link.props().get_id(), Some("sort-email".to_string()));
/// assert_eq!(link.href().as_str(), Some("/admin/users?sort=email")); /// assert_eq!(link.href().as_deref(), Some("/admin/users?sort=email"));
/// assert_eq!(link.dir(), Some(&SortDir::Desc)); /// assert_eq!(link.dir(), Some(&SortDir::Desc));
/// ``` /// ```
/// ///

View file

@ -45,8 +45,8 @@ impl Child {
/// ///
/// Si se proporciona `Some(component)`, se encapsula como [`Child`]; y si es `None`, se limpia. /// Si se proporciona `Some(component)`, se encapsula como [`Child`]; y si es `None`, se limpia.
#[builder_fn] #[builder_fn]
pub fn with_component<C: Component>(mut self, component: Option<C>) -> Self { pub fn with_component<C: Component>(mut self, component: impl Into<Option<C>>) -> Self {
self.0 = component.map(|c| Arc::new(c) as Arc<dyn Component>); self.0 = component.into().map(|c| Arc::new(c) as Arc<dyn Component>);
self self
} }
@ -166,8 +166,8 @@ impl<C: Component> Embed<C> {
/// ///
/// Si se proporciona `Some(component)`, se encapsula como [`Embed`]; y si es `None`, se limpia. /// Si se proporciona `Some(component)`, se encapsula como [`Embed`]; y si es `None`, se limpia.
#[builder_fn] #[builder_fn]
pub fn with_component(mut self, component: Option<C>) -> Self { pub fn with_component(mut self, component: impl Into<Option<C>>) -> Self {
self.0 = component.map(Arc::new); self.0 = component.into().map(Arc::new);
self self
} }

View file

@ -26,7 +26,7 @@ pub use logo::PageTopSvg;
// **< HTML ATTRIBUTES >**************************************************************************** // **< HTML ATTRIBUTES >****************************************************************************
mod attr; mod attr;
pub use attr::{Attr, AttrName, AttrValue}; pub use attr::{AttrName, AttrValue};
mod props; mod props;
pub use props::{Props, PropsError, PropsExtra, PropsOp}; pub use props::{Props, PropsError, PropsExtra, PropsOp};

View file

@ -1,87 +1,5 @@
use crate::{AutoDefault, builder_fn, util}; use crate::{AutoDefault, builder_fn, util};
/// Valor opcional para atributos HTML.
///
/// `Attr<T>` encapsula un `Option<T>` y sirve como tipo base para representar atributos HTML
/// opcionales, uniformes y tipados.
///
/// Este tipo **no impone ninguna normalización ni semántica concreta**; dichas reglas se definen en
/// implementaciones concretas como `Attr<String>`, o en tipos específicos como [`AttrName`]. Para
/// texto localizado usa directamente [`Lc`](crate::locale::Lc) que ya representa su propia ausencia
/// con [`Lc::none()`](crate::locale::Lc::none()), sin necesidad de envolverlo en `Attr<Lc>`.
#[derive(AutoDefault, Clone, Debug)]
pub struct Attr<T>(Option<T>);
impl<T> Attr<T> {
/// Crea un atributo vacío.
pub fn empty() -> Self {
Self(None)
}
/// Crea un atributo con valor.
pub fn some(value: T) -> Self {
Self(Some(value))
}
// **< Attr<T> BUILDER >************************************************************************
/// Establece un valor opcional para el atributo.
#[builder_fn]
pub fn with_opt(mut self, opt: Option<T>) -> Self {
self.0 = opt;
self
}
/// Establece un valor para el atributo.
#[builder_fn]
pub fn with_value(mut self, value: T) -> Self {
self.0 = Some(value);
self
}
/// Elimina el valor del atributo.
#[builder_fn]
pub fn with_none(mut self) -> Self {
self.0 = None;
self
}
// **< Attr<T> GETTERS >************************************************************************
/// Devuelve el valor (clonado), si existe.
pub fn get(&self) -> Option<T>
where
T: Clone,
{
self.0.clone()
}
/// Devuelve una referencia al valor, si existe.
pub fn as_ref(&self) -> Option<&T> {
self.0.as_ref()
}
/// Devuelve el valor (propiedad), si existe.
pub fn into_inner(self) -> Option<T> {
self.0
}
/// `true` si no hay valor.
pub fn is_empty(&self) -> bool {
self.0.is_none()
}
}
// **< Attr<String> >*******************************************************************************
/// Extiende [`Attr`] para cadenas de texto.
impl Attr<String> {
/// Devuelve el texto como `&str` si existe.
pub fn as_str(&self) -> Option<&str> {
self.0.as_deref()
}
}
// **< AttrName >*********************************************************************************** // **< AttrName >***********************************************************************************
/// Nombre normalizado para el atributo `name` o similar de HTML. /// Nombre normalizado para el atributo `name` o similar de HTML.
@ -98,13 +16,13 @@ impl Attr<String> {
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// let name = AttrName::new(" DISplay name "); /// let name = AttrName::new(" DISplay name ");
/// assert_eq!(name.as_str(), Some("display_name")); /// assert_eq!(name.as_deref(), Some("display_name"));
/// ///
/// let empty = AttrName::default(); /// let empty = AttrName::default();
/// assert_eq!(empty.get(), None); /// assert_eq!(empty.get(), None);
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]
pub struct AttrName(Attr<String>); pub struct AttrName(Option<String>);
impl AttrName { impl AttrName {
/// Crea un nuevo `AttrName` normalizando el valor. /// Crea un nuevo `AttrName` normalizando el valor.
@ -117,33 +35,25 @@ impl AttrName {
/// Establece un nombre nuevo normalizando el valor. /// Establece un nombre nuevo normalizando el valor.
#[builder_fn] #[builder_fn]
pub fn with_name(mut self, name: impl AsRef<str>) -> Self { pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.0 = match util::normalize_token(name) { self.0 = util::normalize_token(name);
Some(name) => Attr::some(name),
None => Attr::default(),
};
self self
} }
// **< AttrName GETTERS >*********************************************************************** // **< AttrName GETTERS >***********************************************************************
/// Devuelve el nombre normalizado, si existe.
pub fn get(&self) -> Option<String> {
self.0.get()
}
/// Devuelve el nombre normalizado (sin clonar), si existe. /// Devuelve el nombre normalizado (sin clonar), si existe.
pub fn as_str(&self) -> Option<&str> { pub fn as_deref(&self) -> Option<&str> {
self.0.as_str() self.0.as_deref()
} }
/// Devuelve el nombre normalizado (propiedad), si existe. /// Devuelve el nombre normalizado (clonado), si existe.
pub fn into_inner(self) -> Option<String> { pub fn get(&self) -> Option<String> {
self.0.into_inner() self.0.clone()
} }
/// `true` si no hay valor. /// `true` si no hay valor.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0.is_empty() self.0.is_none()
} }
} }
@ -161,13 +71,13 @@ impl AttrName {
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// let s = AttrValue::new(" a new string "); /// let s = AttrValue::new(" a new string ");
/// assert_eq!(s.as_str(), Some("a new string")); /// assert_eq!(s.as_deref(), Some("a new string"));
/// ///
/// let empty = AttrValue::default(); /// let empty = AttrValue::default();
/// assert_eq!(empty.get(), None); /// assert_eq!(empty.get(), None);
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]
pub struct AttrValue(Attr<String>); pub struct AttrValue(Option<String>);
impl AttrValue { impl AttrValue {
/// Crea un nuevo `AttrValue` normalizando el valor. /// Crea un nuevo `AttrValue` normalizando el valor.
@ -180,32 +90,24 @@ impl AttrValue {
/// Establece una cadena nueva normalizando el valor. /// Establece una cadena nueva normalizando el valor.
#[builder_fn] #[builder_fn]
pub fn with_str(mut self, value: impl AsRef<str>) -> Self { pub fn with_str(mut self, value: impl AsRef<str>) -> Self {
self.0 = match util::non_blank(value.as_ref()) { self.0 = util::non_blank(value.as_ref()).map(str::to_string);
Some(value) => Attr::some(value.to_string()),
None => Attr::default(),
};
self self
} }
// **< AttrValue GETTERS >********************************************************************** // **< AttrValue GETTERS >**********************************************************************
/// Devuelve la cadena normalizada, si existe.
pub fn get(&self) -> Option<String> {
self.0.get()
}
/// Devuelve la cadena normalizada (sin clonar), si existe. /// Devuelve la cadena normalizada (sin clonar), si existe.
pub fn as_str(&self) -> Option<&str> { pub fn as_deref(&self) -> Option<&str> {
self.0.as_str() self.0.as_deref()
} }
/// Devuelve la cadena normalizada (propiedad), si existe. /// Devuelve la cadena normalizada (clonada), si existe.
pub fn into_inner(self) -> Option<String> { pub fn get(&self) -> Option<String> {
self.0.into_inner() self.0.clone()
} }
/// `true` si no hay valor. /// `true` si no hay valor.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0.is_empty() self.0.is_none()
} }
} }