diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 7b83bde2..a1f6245d 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -143,6 +143,8 @@ impl Theme for Bootsier { cx: &mut Context, ) -> Option> { setup_component!(component, { + Badge => |c| theme::bs::badge::setup(c), + Brand => |c| theme::bs::brand::setup(c), Button => |c| theme::bs::button::setup(c), Container => |c| theme::bs::container::setup(c), Image => |c| theme::bs::image::setup(c), diff --git a/extensions/pagetop-bootsier/src/theme/bs/brand.rs b/extensions/pagetop-bootsier/src/theme/bs/brand.rs new file mode 100644 index 00000000..45763c40 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/bs/brand.rs @@ -0,0 +1,9 @@ +use pagetop::prelude::*; + +pub use pagetop::base::component::Brand; + +// **< Brand SETUP >******************************************************************************** + +pub(crate) fn setup(brand: &mut Brand) { + brand.alter_prop(PropsOp::replace_classes("brand", "navbar-brand")); +} diff --git a/extensions/pagetop-bootsier/src/theme/bs/form/input.rs b/extensions/pagetop-bootsier/src/theme/bs/form/input.rs index c5451ea0..6de0698e 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/form/input.rs @@ -63,9 +63,9 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result Result Result Result { option - value=(opt.value().as_str().unwrap_or("")) + value=(opt.value().as_deref().unwrap_or("")) selected[*opt.selected()] disabled[*opt.disabled()] { @@ -111,7 +111,7 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result Result, - /// Devuelve el título de la identidad de marca. - #[default(_code = "Lc::n(&global::SETTINGS.app.name)")] - title: Lc, - /// Devuelve el eslogan de la marca. - slogan: Lc, - /// Devuelve la ruta asociada a la marca (si existe). - #[default(_code = "Some(\"/\".into())")] - route: Option, -} - -#[async_trait] -impl Component for Brand { - fn new() -> Self { - Self::default() - } - - async fn prepare(&self, cx: &mut Context) -> Result { - let image = self.image().render(cx).await; - let title = self.title().using(cx); - if title.is_empty() && image.is_empty() { - return Ok(html! {}); - } - let slogan = self.slogan().using(cx); - Ok(html! { - @if let Some(route) = self.route() { - a class="navbar-brand" href=(route.resolve(cx)) { (image) (title) (slogan) } - } @else { - span class="navbar-brand" { (image) (title) (slogan) } - } - }) - } -} - -impl Brand { - // **< Brand BUILDER >************************************************************************** - - /// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará. - #[builder_fn] - pub fn with_image(mut self, image: Option) -> Self { - self.image.alter_component(image); - self - } - - /// Establece el título de la identidad de marca. - #[builder_fn] - pub fn with_title(mut self, title: Lc) -> Self { - self.title = title; - self - } - - /// Define el eslogan de la marca. - #[builder_fn] - pub fn with_slogan(mut self, slogan: Lc) -> Self { - self.slogan = slogan; - self - } - - /// Define la ruta de destino. Si es `None`, la marca no será un enlace. - #[builder_fn] - pub fn with_route(mut self, route: Option) -> Self { - self.route = route; - self - } -} diff --git a/src/base/component.rs b/src/base/component.rs index 28fcf30a..e01ed9af 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -5,6 +5,9 @@ pub mod layout; mod badge; pub use badge::Badge; +mod brand; +pub use brand::Brand; + pub mod breadcrumb; #[doc(inline)] pub use breadcrumb::Breadcrumb; diff --git a/src/base/component/brand.rs b/src/base/component/brand.rs new file mode 100644 index 00000000..fa7398b1 --- /dev/null +++ b/src/base/component/brand.rs @@ -0,0 +1,120 @@ +use crate::prelude::*; + +/// Componente para mostrar la **identidad de marca** de un sitio o aplicación. +/// +/// Combina una imagen, un título y un eslogan opcional, típicamente en la cabecera de la página o +/// dentro de una barra de navegación proporcionada por un tema. +/// +/// - Si hay ruta ([`with_route()`]), el bloque completo actúa como enlace. Por defecto enlaza a la +/// raíz del sitio (`/`). +/// - Si no hay imagen ([`with_image()`]) ni título ([`with_title()`]), la marca de identidad no se +/// renderiza. +/// - El eslogan ([`with_slogan()`]) es opcional; por defecto no tiene contenido. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let brand = Brand::new() +/// .with_image(Some(Image::with(image::Source::logo(PageTopSvg::Color)))) +/// .with_title(Lc::n("PageTop")) +/// .with_route(Route::from("/")); +/// ``` +/// +/// [`with_route()`]: Self::with_route +/// [`with_image()`]: Self::with_image +/// [`with_title()`]: Self::with_title +/// [`with_slogan()`]: Self::with_slogan +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Brand { + /// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente. + props: Props, + /// Devuelve la imagen de marca (si la hay). + image: Embed, + /// Devuelve el título de la identidad de marca. + #[default(_code = "Lc::n(&global::SETTINGS.app.name)")] + title: Lc, + /// Devuelve el eslogan de la marca. + slogan: Lc, + /// Devuelve la ruta asociada a la marca (si existe). + #[default(_code = "Some(\"/\".into())")] + route: Option, +} + +#[async_trait] +impl Component for Brand { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.props.get_id() + } + + fn setup(&mut self, _cx: &Context) { + self.alter_prop(PropsOp::prepend_classes("brand")); + } + + async fn prepare(&self, cx: &mut Context) -> Result { + let image = self.image().render(cx).await; + let title = self.title().using(cx); + if image.is_empty() && title.is_empty() { + return Ok(html! {}); + } + let slogan = self.slogan().using(cx); + Ok(html! { + @if let Some(route) = self.route() { + a (self.props()) href=(route.resolve(cx)) { (image) (title) (slogan) } + } @else { + span (self.props()) { (image) (title) (slogan) } + } + }) + } +} + +impl Brand { + // **< Brand 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 + } + + /// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará. + #[builder_fn] + pub fn with_image(mut self, image: impl Into>) -> Self { + self.image.alter_component(image); + self + } + + /// Establece el título de la identidad de marca. + #[builder_fn] + pub fn with_title(mut self, title: Lc) -> Self { + self.title = title; + self + } + + /// Define el eslogan de la marca. + #[builder_fn] + pub fn with_slogan(mut self, slogan: Lc) -> Self { + self.slogan = slogan; + self + } + + /// Define la ruta de destino. Si es `None`, la marca no será un enlace. + #[builder_fn] + pub fn with_route(mut self, route: impl Into>) -> Self { + self.route = route.into(); + self + } +} diff --git a/src/base/component/button.rs b/src/base/component/button.rs index 4316766e..4ecc95d3 100644 --- a/src/base/component/button.rs +++ b/src/base/component/button.rs @@ -74,9 +74,9 @@ pub struct Button { /// Devuelve el valor del botón. value: AttrValue, /// Devuelve la etiqueta del botón. - label: Attr, + label: Lc, /// Devuelve el texto emergente del botón (atributo `title`). - title: Attr, + title: Lc, /// Devuelve si el botón recibe el foco automáticamente al cargar la página. autofocus: bool, /// Devuelve si el botón está deshabilitado. @@ -102,8 +102,8 @@ impl Component for Button { button type=(self.kind()) (self.props()) - name=[self.name().get()] - value=[self.value().get()] + name=[self.name().as_deref()] + value=[self.value().as_deref()] title=[self.title().lookup(cx)] autofocus[*self.autofocus()] disabled[*self.disabled()] @@ -124,7 +124,7 @@ impl Button { pub fn submit(label: Lc) -> Self { Self { kind: ButtonAction::Submit, - label: Attr::some(label), + label, ..Default::default() } } @@ -135,7 +135,7 @@ impl Button { pub fn reset(label: Lc) -> Self { Self { kind: ButtonAction::Reset, - label: Attr::some(label), + label, ..Default::default() } } @@ -147,7 +147,7 @@ impl Button { pub fn plain(label: Lc) -> Self { Self { kind: ButtonAction::Plain, - label: Attr::some(label), + label, ..Default::default() } } @@ -188,17 +188,17 @@ impl Button { self } - /// Establece o elimina la etiqueta visible del botón (basta pasar `None` para quitarla). + /// Establece la etiqueta visible del botón (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); + pub fn with_label(mut self, label: Lc) -> Self { + self.label = label; self } - /// Establece o elimina el texto emergente del botón (basta pasar `None` para quitarlo). + /// Establece el texto emergente del botón (usa [`Lc::none()`] para quitarlo). #[builder_fn] - pub fn with_title(mut self, title: impl Into>) -> Self { - self.title.alter_opt(title.into()); + pub fn with_title(mut self, title: Lc) -> Self { + self.title = title; self } diff --git a/src/base/component/form/check.rs b/src/base/component/form/check.rs index e0021e6c..b5fbbd07 100644 --- a/src/base/component/form/check.rs +++ b/src/base/component/form/check.rs @@ -103,9 +103,9 @@ pub struct Field { /// Devuelve el nombre compartido por todas las casillas del grupo. name: AttrName, /// Devuelve la etiqueta del grupo. - label: Attr, + label: Lc, /// Devuelve el texto de ayuda del grupo. - help_text: Attr, + help_text: Lc, /// Devuelve las casillas del grupo. items: Vec, /// Devuelve si todo el grupo está deshabilitado. @@ -141,7 +141,7 @@ impl Component for Field { async fn prepare(&self, cx: &mut Context) -> Result { // 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(); Ok(html! { @@ -162,8 +162,8 @@ impl Component for Field { type="checkbox" id=(&item_id) class="form-check-input" - name=(&name) - value=[item.value().get()] + name=(name) + value=[item.value().as_deref()] checked[*item.checked()] disabled[*item.disabled() || *self.disabled()]; label class="form-check-label" for=(&item_id) { @@ -207,17 +207,17 @@ impl Field { self } - /// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla). + /// Establece la etiqueta visible del grupo (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); + pub fn with_label(mut self, label: Lc) -> Self { + self.label = label; self } - /// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo). + /// Establece el texto de ayuda del grupo (usa [`Lc::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()); + pub fn with_help_text(mut self, help_text: Lc) -> Self { + self.help_text = help_text; self } diff --git a/src/base/component/form/checkbox.rs b/src/base/component/form/checkbox.rs index af721490..3f173394 100644 --- a/src/base/component/form/checkbox.rs +++ b/src/base/component/form/checkbox.rs @@ -46,7 +46,7 @@ pub struct Checkbox { /// Devuelve el nombre del campo. name: AttrName, /// Devuelve la etiqueta del control. - label: Attr, + label: Lc, /// Devuelve si el control debe estar marcado/activo por defecto. checked: bool, /// Devuelve si el control recibe el foco automáticamente al cargar la página. @@ -98,7 +98,7 @@ impl Component for Checkbox { async fn prepare(&self, cx: &mut Context) -> Result { // 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 checkbox_id = util::join!(&container_id, "-checkbox"); @@ -111,7 +111,7 @@ impl Component for Checkbox { role=[is_switch.then_some("switch")] id=(&checkbox_id) class="form-check-input" - name=(&name) + name=(name) value="true" checked[*self.checked()] autofocus[*self.autofocus()] @@ -182,10 +182,10 @@ impl Checkbox { self } - /// Establece o elimina la etiqueta visible del control (basta pasar `None` para quitarla). + /// Establece la etiqueta visible del control (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); + pub fn with_label(mut self, label: Lc) -> Self { + self.label = label; self } diff --git a/src/base/component/form/component.rs b/src/base/component/form/component.rs index bad71b8c..ea5082ec 100644 --- a/src/base/component/form/component.rs +++ b/src/base/component/form/component.rs @@ -81,7 +81,7 @@ impl Component for Form { (self.props()) action=[self.action().try_resolve(cx)] method=[method] - accept-charset=[self.charset().get()] + accept-charset=[self.charset().as_deref()] { (self.children().render(cx).await) } diff --git a/src/base/component/form/fieldset.rs b/src/base/component/form/fieldset.rs index 1deac27b..1cca3bee 100644 --- a/src/base/component/form/fieldset.rs +++ b/src/base/component/form/fieldset.rs @@ -27,9 +27,9 @@ pub struct Fieldset { /// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente. props: Props, /// Devuelve la leyenda del `fieldset`. - legend: Attr, + legend: Lc, /// Devuelve la descripción del `fieldset`. - description: Attr, + description: Lc, /// Devuelve si el `fieldset` está deshabilitado. disabled: bool, /// Devuelve la lista de componentes del `fieldset`. @@ -84,17 +84,17 @@ impl Fieldset { self } - /// Establece o elimina la leyenda del `fieldset` (basta pasar `None` para quitarla). + /// Establece la leyenda del `fieldset` (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_legend(mut self, legend: impl Into>) -> Self { - self.legend.alter_opt(legend.into()); + pub fn with_legend(mut self, legend: Lc) -> Self { + self.legend = legend; self } - /// Establece o elimina la descripción del `fieldset` (basta pasar `None` para quitarla). + /// Establece la descripción del `fieldset` (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_description(mut self, description: impl Into>) -> Self { - self.description.alter_opt(description.into()); + pub fn with_description(mut self, description: Lc) -> Self { + self.description = description; self } diff --git a/src/base/component/form/hidden.rs b/src/base/component/form/hidden.rs index 5a63f734..f5cb4384 100644 --- a/src/base/component/form/hidden.rs +++ b/src/base/component/form/hidden.rs @@ -48,8 +48,8 @@ impl Component for Hidden { Ok(html! { input type="hidden" - name=[self.name().get()] - value=[self.value().get()]; + name=[self.name().as_deref()] + value=[self.value().as_deref()]; }) } } diff --git a/src/base/component/form/input.rs b/src/base/component/form/input.rs index 62279724..da15942b 100644 --- a/src/base/component/form/input.rs +++ b/src/base/component/form/input.rs @@ -161,17 +161,19 @@ pub struct Field { /// Devuelve el valor inicial del campo. value: AttrValue, /// Devuelve la etiqueta del campo. - label: Attr, + label: Lc, /// Devuelve el texto de ayuda del campo. - help_text: Attr, + help_text: Lc, /// Devuelve la longitud mínima permitida en caracteres. - minlength: Attr, + #[getters(copy)] + minlength: Option, /// Devuelve la longitud máxima permitida en caracteres. - maxlength: Attr, + #[getters(copy)] + maxlength: Option, /// Devuelve el texto indicativo del campo. - placeholder: Attr, + placeholder: Lc, /// Devuelve la configuración de autocompletado del campo. - autocomplete: Attr, + autocomplete: Option, /// 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. @@ -183,7 +185,8 @@ pub struct Field { /// Devuelve si el campo se muestra como texto plano sin bordes ni fondo. plaintext: bool, /// Devuelve la sugerencia de teclado virtual para el campo. - inputmode: Attr, + #[getters(copy)] + inputmode: Option, } #[async_trait] @@ -199,7 +202,7 @@ impl Component for Field { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .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)); } @@ -225,9 +228,9 @@ impl Component for Field { let strict = self.kind().is_strict(); let masked = *self.kind() == Kind::StrictPassword; let autocomplete = if strict { - Some(form::Autocomplete::Off) + Some(&form::Autocomplete::Off) } else { - self.autocomplete().get() + self.autocomplete() }; Ok(html! { @@ -249,12 +252,12 @@ impl Component for Field { type=(self.kind()) id=[input_id.as_deref()] class=(input_class) - name=[self.name().get()] - value=[self.value().get()] - minlength=[self.minlength().get()] - maxlength=[self.maxlength().get()] + name=[self.name().as_deref()] + value=[self.value().as_deref()] + minlength=[self.minlength()] + maxlength=[self.maxlength()] placeholder=[self.placeholder().lookup(cx)] - inputmode=[self.inputmode().get()] + inputmode=[self.inputmode()] autocomplete=[autocomplete] spellcheck=[strict.then_some("false")] autocorrect=[strict.then_some("off")] @@ -418,41 +421,41 @@ impl Field { self } - /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). + /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); + pub fn with_label(mut self, label: Lc) -> Self { + self.label = label; self } - /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). + /// Establece el texto de ayuda del campo (usa [`Lc::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()); + pub fn with_help_text(mut self, help_text: Lc) -> Self { + self.help_text = help_text; self } /// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo). #[builder_fn] - pub fn with_minlength(mut self, minlength: Option) -> Self { - self.minlength.alter_opt(minlength); + pub fn with_minlength(mut self, minlength: impl Into>) -> Self { + self.minlength = minlength.into(); self } /// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite). #[builder_fn] - pub fn with_maxlength(mut self, maxlength: Option) -> Self { - self.maxlength.alter_opt(maxlength); + pub fn with_maxlength(mut self, maxlength: impl Into>) -> Self { + self.maxlength = maxlength.into(); self } - /// Establece o elimina el texto indicativo del campo (`None` para quitarlo). + /// Establece el texto indicativo del campo (usa [`Lc::none()`] para quitarlo). /// /// Este texto aparece en el mismo campo y desaparece en cuanto el usuario empieza a escribir. /// Al ser texto visible para el usuario se acepta [`Lc`] para poder localizarlo. #[builder_fn] - pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { - self.placeholder.alter_opt(placeholder.into()); + pub fn with_placeholder(mut self, placeholder: Lc) -> Self { + self.placeholder = placeholder; self } @@ -462,8 +465,11 @@ impl Field { /// [`Autocomplete::email()`](form::Autocomplete::email) o /// [`Autocomplete::current_password()`](form::Autocomplete::current_password)). #[builder_fn] - pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { - self.autocomplete.alter_opt(autocomplete); + pub fn with_autocomplete( + mut self, + autocomplete: impl Into>, + ) -> Self { + self.autocomplete = autocomplete.into(); self } @@ -510,8 +516,8 @@ impl Field { /// 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. #[builder_fn] - pub fn with_inputmode(mut self, inputmode: Option) -> Self { - self.inputmode.alter_opt(inputmode); + pub fn with_inputmode(mut self, inputmode: impl Into>) -> Self { + self.inputmode = inputmode.into(); self } } diff --git a/src/base/component/form/number.rs b/src/base/component/form/number.rs index 48a8f617..8e66261f 100644 --- a/src/base/component/form/number.rs +++ b/src/base/component/form/number.rs @@ -35,17 +35,21 @@ pub struct Number { /// Devuelve el nombre del campo. name: AttrName, /// Devuelve el valor inicial del campo. - value: Attr, + #[getters(copy)] + value: Option, /// Devuelve la etiqueta del campo. - label: Attr, + label: Lc, /// Devuelve el texto de ayuda del campo. - help_text: Attr, + help_text: Lc, /// Devuelve el valor mínimo permitido. - min: Attr, + #[getters(copy)] + min: Option, /// Devuelve el valor máximo permitido. - max: Attr, + #[getters(copy)] + max: Option, /// Devuelve el incremento entre valores del campo. - step: Attr, + #[getters(copy)] + step: Option, /// 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. @@ -69,7 +73,7 @@ impl Component for Number { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .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)); } @@ -100,11 +104,11 @@ impl Component for Number { 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()] + name=[self.name().as_deref()] + min=[self.min()] + max=[self.max()] + step=[self.step()] + value=[self.value()] autofocus[*self.autofocus()] readonly[*self.readonly()] required[*self.required()] @@ -146,36 +150,36 @@ impl Number { /// Establece el valor inicial del campo. #[builder_fn] - pub fn with_value(mut self, value: Option) -> Self { - self.value.alter_opt(value); + pub fn with_value(mut self, value: impl Into>) -> Self { + self.value = value.into(); self } - /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). + /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); + pub fn with_label(mut self, label: Lc) -> Self { + self.label = label; self } - /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). + /// Establece el texto de ayuda del campo (usa [`Lc::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()); + pub fn with_help_text(mut self, help_text: Lc) -> Self { + self.help_text = help_text; 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); + pub fn with_min(mut self, min: impl Into>) -> Self { + self.min = min.into(); 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); + pub fn with_max(mut self, max: impl Into>) -> Self { + self.max = max.into(); self } @@ -184,8 +188,8 @@ impl Number { /// 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); + pub fn with_step(mut self, step: impl Into>) -> Self { + self.step = step.into(); self } diff --git a/src/base/component/form/radio.rs b/src/base/component/form/radio.rs index 2cd39b54..77d4ec24 100644 --- a/src/base/component/form/radio.rs +++ b/src/base/component/form/radio.rs @@ -101,9 +101,9 @@ pub struct Field { /// Devuelve el nombre compartido por todos los botones de opción del grupo. name: AttrName, /// Devuelve la etiqueta del grupo. - label: Attr, + label: Lc, /// Devuelve el texto de ayuda del grupo. - help_text: Attr, + help_text: Lc, /// Devuelve las opciones del grupo. items: Vec, /// Devuelve si la selección de alguna opción del grupo es obligatoria. @@ -141,7 +141,7 @@ impl Component for Field { async fn prepare(&self, cx: &mut Context) -> Result { // 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(); Ok(html! { @@ -178,8 +178,8 @@ impl Component for Field { type="radio" id=(&item_id) class="form-check-input" - name=(&name) - value=[item.value().get()] + name=(name) + value=[item.value().as_deref()] checked[checked] required[*self.required()] disabled[*item.disabled() || *self.disabled()]; @@ -227,17 +227,17 @@ impl Field { self } - /// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla). + /// Establece la etiqueta visible del grupo (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); + pub fn with_label(mut self, label: Lc) -> Self { + self.label = label; self } - /// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo). + /// Establece el texto de ayuda del grupo (usa [`Lc::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()); + pub fn with_help_text(mut self, help_text: Lc) -> Self { + self.help_text = help_text; self } diff --git a/src/base/component/form/range.rs b/src/base/component/form/range.rs index bebb95bc..444a2de6 100644 --- a/src/base/component/form/range.rs +++ b/src/base/component/form/range.rs @@ -36,17 +36,21 @@ pub struct Range { /// Devuelve el nombre del campo. name: AttrName, /// Devuelve el valor inicial del campo. - value: Attr, + #[getters(copy)] + value: Option, /// Devuelve la etiqueta del campo. - label: Attr, + label: Lc, /// Devuelve el texto de ayuda del campo. - help_text: Attr, + help_text: Lc, /// Devuelve el valor mínimo permitido. - min: Attr, + #[getters(copy)] + min: Option, /// Devuelve el valor máximo permitido. - max: Attr, + #[getters(copy)] + max: Option, /// Devuelve el incremento entre valores del campo. - step: Attr, + #[getters(copy)] + step: Option, /// Devuelve si el control recibe el foco automáticamente al cargar la página. autofocus: bool, /// Devuelve si el control está deshabilitado. @@ -66,7 +70,7 @@ impl Component for Range { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .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)); }; @@ -87,11 +91,11 @@ impl Component for Range { type="range" id=[range_id.as_deref()] class="form-range" - name=[self.name().get()] - min=[self.min().get()] - max=[self.max().get()] - step=[self.step().get()] - value=[self.value().get()] + name=[self.name().as_deref()] + min=[self.min()] + max=[self.max()] + step=[self.step()] + value=[self.value()] autofocus[*self.autofocus()] disabled[*self.disabled()]; @if let Some(description) = self.help_text().lookup(cx) { @@ -134,22 +138,22 @@ impl Range { /// 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); + pub fn with_value(mut self, value: impl Into>) -> Self { + self.value = value.into(); self } - /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). + /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); + pub fn with_label(mut self, label: Lc) -> Self { + self.label = label; self } - /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). + /// Establece el texto de ayuda del campo (usa [`Lc::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()); + pub fn with_help_text(mut self, help_text: Lc) -> Self { + self.help_text = help_text; self } @@ -157,8 +161,8 @@ impl Range { /// /// Pasar `None` omite el atributo `min` y deja que el navegador aplique su valor por defecto. #[builder_fn] - pub fn with_min(mut self, min: Option) -> Self { - self.min.alter_opt(min); + pub fn with_min(mut self, min: impl Into>) -> Self { + self.min = min.into(); self } @@ -166,8 +170,8 @@ impl Range { /// /// Pasar `None` omite el atributo `max` y deja que el navegador aplique su valor por defecto. #[builder_fn] - pub fn with_max(mut self, max: Option) -> Self { - self.max.alter_opt(max); + pub fn with_max(mut self, max: impl Into>) -> Self { + self.max = max.into(); self } @@ -176,8 +180,8 @@ impl Range { /// 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); + pub fn with_step(mut self, step: impl Into>) -> Self { + self.step = step.into(); self } diff --git a/src/base/component/form/select.rs b/src/base/component/form/select.rs index 8de719df..d562916d 100644 --- a/src/base/component/form/select.rs +++ b/src/base/component/form/select.rs @@ -197,17 +197,18 @@ pub struct Field { /// Devuelve el nombre del campo. name: AttrName, /// Devuelve la etiqueta del campo. - label: Attr, + label: Lc, /// Devuelve el texto de ayuda del campo. - help_text: Attr, + help_text: Lc, /// Devuelve las entradas de la lista (elementos individuales y grupos de elementos). entries: Vec, /// Devuelve si la lista permite selección múltiple. multiple: bool, /// Devuelve el número de filas visibles de la lista de selección. - rows: Attr, + #[getters(copy)] + rows: Option, /// Devuelve la configuración de autocompletado del campo. - autocomplete: Attr, + autocomplete: Option, /// Devuelve si la lista recibe el foco automáticamente al cargar la página. autofocus: bool, /// Devuelve si la selección de un elemento es obligatoria. @@ -229,7 +230,7 @@ impl Component for Field { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .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)); } @@ -260,10 +261,10 @@ impl Component for Field { select id=[select_id.as_deref()] class="form-select" - name=[self.name().get()] + name=[self.name().as_deref()] multiple[*self.multiple()] - size=[self.rows().get()] - autocomplete=[self.autocomplete().get()] + size=[self.rows()] + autocomplete=[self.autocomplete()] autofocus[*self.autofocus()] required[*self.required()] disabled[*self.disabled()] @@ -272,7 +273,7 @@ impl Component for Field { @match entry { Entry::Item(opt) => { option - value=(opt.value().as_str().unwrap_or("")) + value=(opt.value().as_deref().unwrap_or("")) selected[*opt.selected()] disabled[*opt.disabled()] { @@ -286,7 +287,7 @@ impl Component for Field { { @for opt in group.items() { option - value=(opt.value().as_str().unwrap_or("")) + value=(opt.value().as_deref().unwrap_or("")) selected[*opt.selected()] disabled[*opt.disabled()] { @@ -333,17 +334,17 @@ impl Field { self } - /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). + /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); + pub fn with_label(mut self, label: Lc) -> Self { + self.label = label; self } - /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). + /// Establece el texto de ayuda del campo (usa [`Lc::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()); + pub fn with_help_text(mut self, help_text: Lc) -> Self { + self.help_text = help_text; self } @@ -389,8 +390,8 @@ impl Field { /// Es especialmente útil con selección múltiple para controlar el número de filas visibles sin /// necesidad de recurrir al desplazamiento. #[builder_fn] - pub fn with_rows(mut self, rows: Option) -> Self { - self.rows.alter_opt(rows); + pub fn with_rows(mut self, rows: impl Into>) -> Self { + self.rows = rows.into(); self } @@ -404,8 +405,11 @@ impl Field { /// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para /// omitir el atributo. #[builder_fn] - pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { - self.autocomplete.alter_opt(autocomplete); + pub fn with_autocomplete( + mut self, + autocomplete: impl Into>, + ) -> Self { + self.autocomplete = autocomplete.into(); self } diff --git a/src/base/component/form/textarea.rs b/src/base/component/form/textarea.rs index 5760d779..97e047ce 100644 --- a/src/base/component/form/textarea.rs +++ b/src/base/component/form/textarea.rs @@ -40,19 +40,22 @@ pub struct Textarea { /// Devuelve el valor inicial del área de texto. value: AttrValue, /// Devuelve la etiqueta del campo. - label: Attr, + label: Lc, /// Devuelve el texto de ayuda del campo. - help_text: Attr, + help_text: Lc, /// Devuelve el número de filas visibles del área de texto. - rows: Attr, + #[getters(copy)] + rows: Option, /// Devuelve la longitud mínima permitida en caracteres. - minlength: Attr, + #[getters(copy)] + minlength: Option, /// Devuelve la longitud máxima permitida en caracteres. - maxlength: Attr, + #[getters(copy)] + maxlength: Option, /// Devuelve el texto indicativo del área de texto. - placeholder: Attr, + placeholder: Lc, /// Devuelve la configuración de autocompletado del campo. - autocomplete: Attr, + autocomplete: Option, /// 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. @@ -76,7 +79,7 @@ impl Component for Textarea { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .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)); } @@ -109,18 +112,18 @@ impl Component for Textarea { textarea id=[textarea_id.as_deref()] class="form-control" - name=[self.name().get()] - rows=[self.rows().get()] - minlength=[self.minlength().get()] - maxlength=[self.maxlength().get()] + name=[self.name().as_deref()] + rows=[self.rows()] + minlength=[self.minlength()] + maxlength=[self.maxlength()] placeholder=[self.placeholder().lookup(cx)] - autocomplete=[self.autocomplete().get()] + autocomplete=[self.autocomplete()] autofocus[*self.autofocus()] readonly[*self.readonly()] required[*self.required()] disabled[*self.disabled()] { - @if let Some(value) = self.value().get() { + @if let Some(value) = self.value().as_deref() { (value) } } @@ -166,17 +169,17 @@ impl Textarea { self } - /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). + /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); + pub fn with_label(mut self, label: Lc) -> Self { + self.label = label; self } - /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). + /// Establece el texto de ayuda del campo (usa [`Lc::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()); + pub fn with_help_text(mut self, help_text: Lc) -> Self { + self.help_text = help_text; self } @@ -185,32 +188,32 @@ impl Textarea { /// Sin valor o pasando `None`, el área muestra su altura predeterminada, dos filas según el /// estándar. #[builder_fn] - pub fn with_rows(mut self, rows: Option) -> Self { - self.rows.alter_opt(rows); + pub fn with_rows(mut self, rows: impl Into>) -> Self { + self.rows = rows.into(); self } /// Establece la longitud mínima permitida en caracteres. #[builder_fn] - pub fn with_minlength(mut self, minlength: Option) -> Self { - self.minlength.alter_opt(minlength); + pub fn with_minlength(mut self, minlength: impl Into>) -> Self { + self.minlength = minlength.into(); self } /// Establece la longitud máxima permitida en caracteres. #[builder_fn] - pub fn with_maxlength(mut self, maxlength: Option) -> Self { - self.maxlength.alter_opt(maxlength); + pub fn with_maxlength(mut self, maxlength: impl Into>) -> Self { + self.maxlength = maxlength.into(); self } - /// Establece o elimina el texto indicativo del área de texto (`None` para quitarlo). + /// Establece el texto indicativo del área de texto (usa [`Lc::none()`] para quitarlo). /// /// Este texto aparece en el área de texto y desaparece en cuanto el usuario empieza a escribir. /// Al ser texto visible para el usuario se acepta [`Lc`] para poder localizarlo. #[builder_fn] - pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { - self.placeholder.alter_opt(placeholder.into()); + pub fn with_placeholder(mut self, placeholder: Lc) -> Self { + self.placeholder = placeholder; self } @@ -222,8 +225,11 @@ impl Textarea { /// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para /// omitir el atributo. #[builder_fn] - pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { - self.autocomplete.alter_opt(autocomplete); + pub fn with_autocomplete( + mut self, + autocomplete: impl Into>, + ) -> Self { + self.autocomplete = autocomplete.into(); self } diff --git a/src/base/component/image/component.rs b/src/base/component/image/component.rs index ed799886..220a079e 100644 --- a/src/base/component/image/component.rs +++ b/src/base/component/image/component.rs @@ -29,7 +29,7 @@ pub struct Image { /// Devuelve el origen de la imagen. source: image::Source, /// Devuelve el texto alternativo localizado. - alternative: Attr, + alternative: Lc, } #[async_trait] @@ -140,7 +140,7 @@ impl Image { /// decorativa. #[builder_fn] pub fn with_alternative(mut self, alt: Lc) -> Self { - self.alternative.alter_value(alt); + self.alternative = alt; self } } diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index 4a8efb54..f2c306c1 100644 --- a/src/base/component/intro.rs +++ b/src/base/component/intro.rs @@ -263,8 +263,8 @@ impl Intro { /// let intro_no_button = Intro::default().with_button(None); /// ``` #[builder_fn] - pub fn with_button(mut self, button: Option<(Lc, Route)>) -> Self { - self.button = button; + pub fn with_button(mut self, button: impl Into>) -> Self { + self.button = button.into(); self } diff --git a/src/base/component/pager.rs b/src/base/component/pager.rs index 1cf282ce..da24013e 100644 --- a/src/base/component/pager.rs +++ b/src/base/component/pager.rs @@ -203,11 +203,11 @@ impl Component for Pager { return Ok(html! {}); } 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 // `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() { route.alter_param(key, value); } diff --git a/src/base/component/table/column.rs b/src/base/component/table/column.rs index cc55ca71..ea7604ad 100644 --- a/src/base/component/table/column.rs +++ b/src/base/component/table/column.rs @@ -99,7 +99,7 @@ impl Column { html! { 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) } } } } diff --git a/src/base/component/table/props.rs b/src/base/component/table/props.rs index c6446e92..c7b3acdc 100644 --- a/src/base/component/table/props.rs +++ b/src/base/component/table/props.rs @@ -21,7 +21,7 @@ use crate::prelude::*; /// .with_prop(PropsOp::set("data-sort", "email")); /// /// 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)); /// ``` /// diff --git a/src/core/component/children.rs b/src/core/component/children.rs index 2a89f2c2..154b4945 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -45,8 +45,8 @@ impl Child { /// /// Si se proporciona `Some(component)`, se encapsula como [`Child`]; y si es `None`, se limpia. #[builder_fn] - pub fn with_component(mut self, component: Option) -> Self { - self.0 = component.map(|c| Arc::new(c) as Arc); + pub fn with_component(mut self, component: impl Into>) -> Self { + self.0 = component.into().map(|c| Arc::new(c) as Arc); self } @@ -166,8 +166,8 @@ impl Embed { /// /// Si se proporciona `Some(component)`, se encapsula como [`Embed`]; y si es `None`, se limpia. #[builder_fn] - pub fn with_component(mut self, component: Option) -> Self { - self.0 = component.map(Arc::new); + pub fn with_component(mut self, component: impl Into>) -> Self { + self.0 = component.into().map(Arc::new); self } diff --git a/src/html.rs b/src/html.rs index a2d4d2d0..d2f88c52 100644 --- a/src/html.rs +++ b/src/html.rs @@ -26,7 +26,7 @@ pub use logo::PageTopSvg; // **< HTML ATTRIBUTES >**************************************************************************** mod attr; -pub use attr::{Attr, AttrName, AttrValue}; +pub use attr::{AttrName, AttrValue}; mod props; pub use props::{Props, PropsError, PropsExtra, PropsOp}; diff --git a/src/html/attr.rs b/src/html/attr.rs index 92593da3..5ad5b3a3 100644 --- a/src/html/attr.rs +++ b/src/html/attr.rs @@ -1,133 +1,5 @@ -use crate::locale::{LangId, Lc}; use crate::{AutoDefault, builder_fn, util}; -/// Valor opcional para atributos HTML. -/// -/// `Attr` encapsula un `Option` 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` y `Attr`, o en tipos específicos como -/// [`AttrName`]. -#[derive(AutoDefault, Clone, Debug)] -pub struct Attr(Option); - -impl Attr { - /// 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 BUILDER >************************************************************************ - - /// Establece un valor opcional para el atributo. - #[builder_fn] - pub fn with_opt(mut self, opt: Option) -> 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 GETTERS >************************************************************************ - - /// Devuelve el valor (clonado), si existe. - pub fn get(&self) -> Option - 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 { - self.0 - } - - /// `true` si no hay valor. - pub fn is_empty(&self) -> bool { - self.0.is_none() - } -} - -// **< Attr >*********************************************************************************** - -/// Extiende [`Attr`] para [texto localizado](crate::locale) en atributos HTML. -/// -/// Encapsula un [`Lc`] para manejar traducciones de forma segura en atributos. -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// // Traducción por clave en las locales por defecto de PageTop. -/// let hello = Attr::::new(Lc::l("test_hello_world")); -/// -/// // Español disponible. -/// assert_eq!( -/// hello.lookup(&Locale::resolve("es-ES")), -/// Some("¡Hola mundo!".to_string()) -/// ); -/// -/// // Japonés no disponible, traduce al idioma de respaldo (`"en-US"`). -/// assert_eq!( -/// hello.lookup(&Locale::resolve("ja-JP")), -/// Some("Hello world!".to_string()) -/// ); -/// -/// // Uso típico en un atributo: -/// let title = hello.value(&Locale::resolve("es-ES")); -/// // Ejemplo: html! { a title=(title) { "Link" } } -/// ``` -impl Attr { - /// Crea una nueva instancia `Attr`. - pub fn new(value: Lc) -> Self { - Self::some(value) - } - - /// Devuelve la traducción para `language` si puede resolverse. - pub fn lookup(&self, language: &impl LangId) -> Option { - self.0.as_ref()?.lookup(language) - } - - /// Devuelve la traducción para `language` o una cadena vacía si no existe. - pub fn value(&self, language: &impl LangId) -> String { - self.lookup(language).unwrap_or_default() - } -} - -// **< Attr >******************************************************************************* - -/// Extiende [`Attr`] para cadenas de texto. -impl Attr { - /// Devuelve el texto como `&str` si existe. - pub fn as_str(&self) -> Option<&str> { - self.0.as_deref() - } -} - // **< AttrName >*********************************************************************************** /// Nombre normalizado para el atributo `name` o similar de HTML. @@ -144,13 +16,13 @@ impl Attr { /// ```rust /// # use pagetop::prelude::*; /// 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(); /// assert_eq!(empty.get(), None); /// ``` #[derive(AutoDefault, Clone, Debug)] -pub struct AttrName(Attr); +pub struct AttrName(Option); impl AttrName { /// Crea un nuevo `AttrName` normalizando el valor. @@ -163,33 +35,25 @@ impl AttrName { /// Establece un nombre nuevo normalizando el valor. #[builder_fn] pub fn with_name(mut self, name: impl AsRef) -> Self { - self.0 = match util::normalize_token(name) { - Some(name) => Attr::some(name), - None => Attr::default(), - }; + self.0 = util::normalize_token(name); self } // **< AttrName GETTERS >*********************************************************************** - /// Devuelve el nombre normalizado, si existe. - pub fn get(&self) -> Option { - self.0.get() - } - /// Devuelve el nombre normalizado (sin clonar), si existe. - pub fn as_str(&self) -> Option<&str> { - self.0.as_str() + pub fn as_deref(&self) -> Option<&str> { + self.0.as_deref() } - /// Devuelve el nombre normalizado (propiedad), si existe. - pub fn into_inner(self) -> Option { - self.0.into_inner() + /// Devuelve el nombre normalizado (clonado), si existe. + pub fn get(&self) -> Option { + self.0.clone() } /// `true` si no hay valor. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.0.is_none() } } @@ -207,13 +71,13 @@ impl AttrName { /// ```rust /// # use pagetop::prelude::*; /// 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(); /// assert_eq!(empty.get(), None); /// ``` #[derive(AutoDefault, Clone, Debug)] -pub struct AttrValue(Attr); +pub struct AttrValue(Option); impl AttrValue { /// Crea un nuevo `AttrValue` normalizando el valor. @@ -226,32 +90,24 @@ impl AttrValue { /// Establece una cadena nueva normalizando el valor. #[builder_fn] pub fn with_str(mut self, value: impl AsRef) -> Self { - self.0 = match util::non_blank(value.as_ref()) { - Some(value) => Attr::some(value.to_string()), - None => Attr::default(), - }; + self.0 = util::non_blank(value.as_ref()).map(str::to_string); self } // **< AttrValue GETTERS >********************************************************************** - /// Devuelve la cadena normalizada, si existe. - pub fn get(&self) -> Option { - self.0.get() - } - /// Devuelve la cadena normalizada (sin clonar), si existe. - pub fn as_str(&self) -> Option<&str> { - self.0.as_str() + pub fn as_deref(&self) -> Option<&str> { + self.0.as_deref() } - /// Devuelve la cadena normalizada (propiedad), si existe. - pub fn into_inner(self) -> Option { - self.0.into_inner() + /// Devuelve la cadena normalizada (clonada), si existe. + pub fn get(&self) -> Option { + self.0.clone() } /// `true` si no hay valor. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.0.is_none() } } diff --git a/src/locale/lc.rs b/src/locale/lc.rs index 93e4a84e..0299d2da 100644 --- a/src/locale/lc.rs +++ b/src/locale/lc.rs @@ -32,6 +32,7 @@ enum LcKind { /// - Un texto puro (`n()`) que no requiere traducción. /// - Una clave para traducir un texto del conjunto de traducciones predefinidas de PageTop (`l()`). /// - Una clave para traducir de un conjunto concreto de traducciones (`t()`). +/// - Ningún contenido (`none()`), para representar la ausencia en un campo `Lc` opcional. /// /// # ¿Cuál usar, `get()`, `lookup()` o `using()`? /// @@ -122,6 +123,23 @@ impl Lc { } } + /// Crea una instancia **sin contenido**: no traduce nada y no representa ningún texto. + /// + /// Equivale a [`Lc::default()`](Default::default), con un nombre más explícito. Útil para + /// representar la ausencia en un campo `Lc` opcional sin requerir `Option`: + /// [`get()`](Self::get) y [`lookup()`](Self::lookup) devuelven `None`, y + /// [`using()`](Self::using) devuelve un marcado vacío. + /// + /// ```rust + /// # use pagetop::prelude::*; + /// assert_eq!(Lc::none().get(), None); + /// ``` + pub fn none() -> Self { + Self::default() + } + + // **< Lc BUILDER >***************************************************************************** + /// Añade un argumento `{$arg}` => `value` a la traducción. pub fn with_arg(mut self, arg: impl Into, value: impl Into) -> Self { self.args.push((arg.into(), value.into())); @@ -142,6 +160,8 @@ impl Lc { self } + // **< Lc GETTERS >***************************************************************************** + /// Resuelve la traducción usando el idioma por defecto o, si no procede, el de respaldo de la /// aplicación. /// diff --git a/src/response/page.rs b/src/response/page.rs index f9b2cc67..27ca749c 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -25,8 +25,8 @@ use crate::core::component::{AssetsOp, ChildOp, ComponentRender}; use crate::core::component::{Context, ContextError, Contextual}; use crate::core::theme::{CoreRegions, RegionName, RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; -use crate::html::{Attr, Props, PropsOp}; use crate::html::{DOCTYPE, Markup, html}; +use crate::html::{Props, PropsOp}; use crate::locale::{CharacterDirection, LangId, LanguageIdentifier, Lc}; use crate::web::HttpRequest; use crate::{AutoDefault, builder_fn}; @@ -88,11 +88,11 @@ impl RegionName for ReservedRegions { #[rustfmt::skip] #[derive(AutoDefault)] pub struct Page { - title : Attr, - description : Attr, - metadata : Vec<(&'static str, &'static str)>, - properties : Vec<(&'static str, &'static str)>, - context : Context, + title : Lc, + description: Lc, + metadata : Vec<(&'static str, &'static str)>, + properties : Vec<(&'static str, &'static str)>, + context : Context, } impl Page { @@ -128,14 +128,14 @@ impl Page { /// Establece el título de la página como un valor traducible. #[builder_fn] pub fn with_title(mut self, title: Lc) -> Self { - self.title.alter_value(title); + self.title = title; self } /// Establece la descripción de la página como un valor traducible. #[builder_fn] pub fn with_description(mut self, description: Lc) -> Self { - self.description.alter_value(description); + self.description = description; self } diff --git a/tests/component_button.rs b/tests/component_button.rs new file mode 100644 index 00000000..5918d7c1 --- /dev/null +++ b/tests/component_button.rs @@ -0,0 +1,45 @@ +use pagetop::prelude::*; + +#[pagetop::test] +async fn label_is_rendered_when_set() { + let mut button = Button::submit(Lc::n("Save")); + let html = button.render(&mut Context::default()).await.into_string(); + + assert!(html.contains("Save")); +} + +#[pagetop::test] +async fn label_can_be_cleared_with_lc_none() { + let mut button = Button::submit(Lc::n("Save")).with_label(Lc::none()); + let html = button.render(&mut Context::default()).await.into_string(); + + assert!(!html.contains("Save")); + // The button itself must still render. + assert!(html.contains("