diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index a1f6245d..7b83bde2 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -143,8 +143,6 @@ 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 deleted file mode 100644 index 45763c40..00000000 --- a/extensions/pagetop-bootsier/src/theme/bs/brand.rs +++ /dev/null @@ -1,9 +0,0 @@ -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 6de0698e..c5451ea0 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_deref().unwrap_or("")) + value=(opt.value().as_str().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 e01ed9af..28fcf30a 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -5,9 +5,6 @@ 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 deleted file mode 100644 index fa7398b1..00000000 --- a/src/base/component/brand.rs +++ /dev/null @@ -1,120 +0,0 @@ -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 4ecc95d3..4316766e 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: Lc, + label: Attr, /// Devuelve el texto emergente del botón (atributo `title`). - title: Lc, + title: Attr, /// 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().as_deref()] - value=[self.value().as_deref()] + name=[self.name().get()] + value=[self.value().get()] 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, + label: Attr::some(label), ..Default::default() } } @@ -135,7 +135,7 @@ impl Button { pub fn reset(label: Lc) -> Self { Self { kind: ButtonAction::Reset, - label, + label: Attr::some(label), ..Default::default() } } @@ -147,7 +147,7 @@ impl Button { pub fn plain(label: Lc) -> Self { Self { kind: ButtonAction::Plain, - label, + label: Attr::some(label), ..Default::default() } } @@ -188,17 +188,17 @@ impl Button { self } - /// Establece la etiqueta visible del botón (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la etiqueta visible del botón (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: Lc) -> Self { - self.label = label; + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } - /// Establece el texto emergente del botón (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto emergente del botón (basta pasar `None` para quitarlo). #[builder_fn] - pub fn with_title(mut self, title: Lc) -> Self { - self.title = title; + pub fn with_title(mut self, title: impl Into>) -> Self { + self.title.alter_opt(title.into()); self } diff --git a/src/base/component/form/check.rs b/src/base/component/form/check.rs index b5fbbd07..e0021e6c 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: Lc, + label: Attr, /// Devuelve el texto de ayuda del grupo. - help_text: Lc, + help_text: Attr, /// 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().as_deref().unwrap(); + let name = self.name().get().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().as_deref()] + name=(&name) + value=[item.value().get()] checked[*item.checked()] disabled[*item.disabled() || *self.disabled()]; label class="form-check-label" for=(&item_id) { @@ -207,17 +207,17 @@ impl Field { self } - /// Establece la etiqueta visible del grupo (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: Lc) -> Self { - self.label = label; + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } - /// Establece el texto de ayuda del grupo (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo). #[builder_fn] - pub fn with_help_text(mut self, help_text: Lc) -> Self { - self.help_text = help_text; + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); self } diff --git a/src/base/component/form/checkbox.rs b/src/base/component/form/checkbox.rs index 3f173394..af721490 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: Lc, + label: Attr, /// 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().as_deref().unwrap(); + let name = self.name().get().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 la etiqueta visible del control (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la etiqueta visible del control (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: Lc) -> Self { - self.label = label; + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } diff --git a/src/base/component/form/component.rs b/src/base/component/form/component.rs index ea5082ec..bad71b8c 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().as_deref()] + accept-charset=[self.charset().get()] { (self.children().render(cx).await) } diff --git a/src/base/component/form/fieldset.rs b/src/base/component/form/fieldset.rs index 1cca3bee..1deac27b 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: Lc, + legend: Attr, /// Devuelve la descripción del `fieldset`. - description: Lc, + description: Attr, /// Devuelve si el `fieldset` está deshabilitado. disabled: bool, /// Devuelve la lista de componentes del `fieldset`. @@ -84,17 +84,17 @@ impl Fieldset { self } - /// Establece la leyenda del `fieldset` (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la leyenda del `fieldset` (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_legend(mut self, legend: Lc) -> Self { - self.legend = legend; + pub fn with_legend(mut self, legend: impl Into>) -> Self { + self.legend.alter_opt(legend.into()); self } - /// Establece la descripción del `fieldset` (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la descripción del `fieldset` (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_description(mut self, description: Lc) -> Self { - self.description = description; + pub fn with_description(mut self, description: impl Into>) -> Self { + self.description.alter_opt(description.into()); self } diff --git a/src/base/component/form/hidden.rs b/src/base/component/form/hidden.rs index f5cb4384..5a63f734 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().as_deref()] - value=[self.value().as_deref()]; + name=[self.name().get()] + value=[self.value().get()]; }) } } diff --git a/src/base/component/form/input.rs b/src/base/component/form/input.rs index da15942b..62279724 100644 --- a/src/base/component/form/input.rs +++ b/src/base/component/form/input.rs @@ -161,19 +161,17 @@ pub struct Field { /// Devuelve el valor inicial del campo. value: AttrValue, /// Devuelve la etiqueta del campo. - label: Lc, + label: Attr, /// Devuelve el texto de ayuda del campo. - help_text: Lc, + help_text: Attr, /// Devuelve la longitud mínima permitida en caracteres. - #[getters(copy)] - minlength: Option, + minlength: Attr, /// Devuelve la longitud máxima permitida en caracteres. - #[getters(copy)] - maxlength: Option, + maxlength: Attr, /// Devuelve el texto indicativo del campo. - placeholder: Lc, + placeholder: Attr, /// Devuelve la configuración de autocompletado del campo. - autocomplete: Option, + autocomplete: Attr, /// Devuelve si el campo recibe el foco automáticamente al cargar la página. autofocus: bool, /// Devuelve si el campo es de sólo lectura. @@ -185,8 +183,7 @@ 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. - #[getters(copy)] - inputmode: Option, + inputmode: Attr, } #[async_trait] @@ -202,7 +199,7 @@ impl Component for Field { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .id() - .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) { self.alter_prop(PropsOp::ensure_id(container_id)); } @@ -228,9 +225,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() + self.autocomplete().get() }; Ok(html! { @@ -252,12 +249,12 @@ impl Component for Field { type=(self.kind()) id=[input_id.as_deref()] class=(input_class) - name=[self.name().as_deref()] - value=[self.value().as_deref()] - minlength=[self.minlength()] - maxlength=[self.maxlength()] + name=[self.name().get()] + value=[self.value().get()] + minlength=[self.minlength().get()] + maxlength=[self.maxlength().get()] placeholder=[self.placeholder().lookup(cx)] - inputmode=[self.inputmode()] + inputmode=[self.inputmode().get()] autocomplete=[autocomplete] spellcheck=[strict.then_some("false")] autocorrect=[strict.then_some("off")] @@ -421,41 +418,41 @@ impl Field { self } - /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: Lc) -> Self { - self.label = label; + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } - /// Establece el texto de ayuda del campo (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). #[builder_fn] - pub fn with_help_text(mut self, help_text: Lc) -> Self { - self.help_text = help_text; + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); self } /// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo). #[builder_fn] - pub fn with_minlength(mut self, minlength: impl Into>) -> Self { - self.minlength = minlength.into(); + pub fn with_minlength(mut self, minlength: Option) -> Self { + self.minlength.alter_opt(minlength); self } /// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite). #[builder_fn] - pub fn with_maxlength(mut self, maxlength: impl Into>) -> Self { - self.maxlength = maxlength.into(); + pub fn with_maxlength(mut self, maxlength: Option) -> Self { + self.maxlength.alter_opt(maxlength); self } - /// Establece el texto indicativo del campo (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto indicativo del campo (`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: Lc) -> Self { - self.placeholder = placeholder; + pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { + self.placeholder.alter_opt(placeholder.into()); self } @@ -465,11 +462,8 @@ impl Field { /// [`Autocomplete::email()`](form::Autocomplete::email) o /// [`Autocomplete::current_password()`](form::Autocomplete::current_password)). #[builder_fn] - pub fn with_autocomplete( - mut self, - autocomplete: impl Into>, - ) -> Self { - self.autocomplete = autocomplete.into(); + pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { + self.autocomplete.alter_opt(autocomplete); self } @@ -516,8 +510,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: impl Into>) -> Self { - self.inputmode = inputmode.into(); + pub fn with_inputmode(mut self, inputmode: Option) -> Self { + self.inputmode.alter_opt(inputmode); self } } diff --git a/src/base/component/form/number.rs b/src/base/component/form/number.rs index 8e66261f..48a8f617 100644 --- a/src/base/component/form/number.rs +++ b/src/base/component/form/number.rs @@ -35,21 +35,17 @@ pub struct Number { /// Devuelve el nombre del campo. name: AttrName, /// Devuelve el valor inicial del campo. - #[getters(copy)] - value: Option, + value: Attr, /// Devuelve la etiqueta del campo. - label: Lc, + label: Attr, /// Devuelve el texto de ayuda del campo. - help_text: Lc, + help_text: Attr, /// Devuelve el valor mínimo permitido. - #[getters(copy)] - min: Option, + min: Attr, /// Devuelve el valor máximo permitido. - #[getters(copy)] - max: Option, + max: Attr, /// Devuelve el incremento entre valores del campo. - #[getters(copy)] - step: Option, + step: Attr, /// Devuelve si el campo recibe el foco automáticamente al cargar la página. autofocus: bool, /// Devuelve si el campo es de sólo lectura. @@ -73,7 +69,7 @@ impl Component for Number { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .id() - .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) { self.alter_prop(PropsOp::ensure_id(container_id)); } @@ -104,11 +100,11 @@ impl Component for Number { type="number" id=[input_id.as_deref()] class="form-control" - name=[self.name().as_deref()] - min=[self.min()] - max=[self.max()] - step=[self.step()] - value=[self.value()] + name=[self.name().get()] + min=[self.min().get()] + max=[self.max().get()] + step=[self.step().get()] + value=[self.value().get()] autofocus[*self.autofocus()] readonly[*self.readonly()] required[*self.required()] @@ -150,36 +146,36 @@ impl Number { /// Establece el valor inicial del campo. #[builder_fn] - pub fn with_value(mut self, value: impl Into>) -> Self { - self.value = value.into(); + pub fn with_value(mut self, value: Option) -> Self { + self.value.alter_opt(value); self } - /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: Lc) -> Self { - self.label = label; + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } - /// Establece el texto de ayuda del campo (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). #[builder_fn] - pub fn with_help_text(mut self, help_text: Lc) -> Self { - self.help_text = help_text; + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); self } /// Establece el valor mínimo permitido (`None` para no imponer mínimo). #[builder_fn] - pub fn with_min(mut self, min: impl Into>) -> Self { - self.min = min.into(); + pub fn with_min(mut self, min: Option) -> Self { + self.min.alter_opt(min); self } /// Establece el valor máximo permitido (`None` para no imponer máximo). #[builder_fn] - pub fn with_max(mut self, max: impl Into>) -> Self { - self.max = max.into(); + pub fn with_max(mut self, max: Option) -> Self { + self.max.alter_opt(max); self } @@ -188,8 +184,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: impl Into>) -> Self { - self.step = step.into(); + pub fn with_step(mut self, step: Option) -> Self { + self.step.alter_opt(step); self } diff --git a/src/base/component/form/radio.rs b/src/base/component/form/radio.rs index 77d4ec24..2cd39b54 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: Lc, + label: Attr, /// Devuelve el texto de ayuda del grupo. - help_text: Lc, + help_text: Attr, /// 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().as_deref().unwrap(); + let name = self.name().get().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().as_deref()] + name=(&name) + value=[item.value().get()] checked[checked] required[*self.required()] disabled[*item.disabled() || *self.disabled()]; @@ -227,17 +227,17 @@ impl Field { self } - /// Establece la etiqueta visible del grupo (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: Lc) -> Self { - self.label = label; + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } - /// Establece el texto de ayuda del grupo (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo). #[builder_fn] - pub fn with_help_text(mut self, help_text: Lc) -> Self { - self.help_text = help_text; + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); self } diff --git a/src/base/component/form/range.rs b/src/base/component/form/range.rs index 444a2de6..bebb95bc 100644 --- a/src/base/component/form/range.rs +++ b/src/base/component/form/range.rs @@ -36,21 +36,17 @@ pub struct Range { /// Devuelve el nombre del campo. name: AttrName, /// Devuelve el valor inicial del campo. - #[getters(copy)] - value: Option, + value: Attr, /// Devuelve la etiqueta del campo. - label: Lc, + label: Attr, /// Devuelve el texto de ayuda del campo. - help_text: Lc, + help_text: Attr, /// Devuelve el valor mínimo permitido. - #[getters(copy)] - min: Option, + min: Attr, /// Devuelve el valor máximo permitido. - #[getters(copy)] - max: Option, + max: Attr, /// Devuelve el incremento entre valores del campo. - #[getters(copy)] - step: Option, + step: Attr, /// Devuelve si el control recibe el foco automáticamente al cargar la página. autofocus: bool, /// Devuelve si el control está deshabilitado. @@ -70,7 +66,7 @@ impl Component for Range { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .id() - .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) { self.alter_prop(PropsOp::ensure_id(container_id)); }; @@ -91,11 +87,11 @@ impl Component for Range { type="range" id=[range_id.as_deref()] class="form-range" - name=[self.name().as_deref()] - min=[self.min()] - max=[self.max()] - step=[self.step()] - value=[self.value()] + name=[self.name().get()] + min=[self.min().get()] + max=[self.max().get()] + step=[self.step().get()] + value=[self.value().get()] autofocus[*self.autofocus()] disabled[*self.disabled()]; @if let Some(description) = self.help_text().lookup(cx) { @@ -138,22 +134,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: impl Into>) -> Self { - self.value = value.into(); + pub fn with_value(mut self, value: Option) -> Self { + self.value.alter_opt(value); self } - /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: Lc) -> Self { - self.label = label; + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } - /// Establece el texto de ayuda del campo (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). #[builder_fn] - pub fn with_help_text(mut self, help_text: Lc) -> Self { - self.help_text = help_text; + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); self } @@ -161,8 +157,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: impl Into>) -> Self { - self.min = min.into(); + pub fn with_min(mut self, min: Option) -> Self { + self.min.alter_opt(min); self } @@ -170,8 +166,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: impl Into>) -> Self { - self.max = max.into(); + pub fn with_max(mut self, max: Option) -> Self { + self.max.alter_opt(max); self } @@ -180,8 +176,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: impl Into>) -> Self { - self.step = step.into(); + pub fn with_step(mut self, step: Option) -> Self { + self.step.alter_opt(step); self } diff --git a/src/base/component/form/select.rs b/src/base/component/form/select.rs index d562916d..8de719df 100644 --- a/src/base/component/form/select.rs +++ b/src/base/component/form/select.rs @@ -197,18 +197,17 @@ pub struct Field { /// Devuelve el nombre del campo. name: AttrName, /// Devuelve la etiqueta del campo. - label: Lc, + label: Attr, /// Devuelve el texto de ayuda del campo. - help_text: Lc, + help_text: Attr, /// 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. - #[getters(copy)] - rows: Option, + rows: Attr, /// Devuelve la configuración de autocompletado del campo. - autocomplete: Option, + autocomplete: Attr, /// 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. @@ -230,7 +229,7 @@ impl Component for Field { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .id() - .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) { self.alter_prop(PropsOp::ensure_id(container_id)); } @@ -261,10 +260,10 @@ impl Component for Field { select id=[select_id.as_deref()] class="form-select" - name=[self.name().as_deref()] + name=[self.name().get()] multiple[*self.multiple()] - size=[self.rows()] - autocomplete=[self.autocomplete()] + size=[self.rows().get()] + autocomplete=[self.autocomplete().get()] autofocus[*self.autofocus()] required[*self.required()] disabled[*self.disabled()] @@ -273,7 +272,7 @@ impl Component for Field { @match entry { Entry::Item(opt) => { option - value=(opt.value().as_deref().unwrap_or("")) + value=(opt.value().as_str().unwrap_or("")) selected[*opt.selected()] disabled[*opt.disabled()] { @@ -287,7 +286,7 @@ impl Component for Field { { @for opt in group.items() { option - value=(opt.value().as_deref().unwrap_or("")) + value=(opt.value().as_str().unwrap_or("")) selected[*opt.selected()] disabled[*opt.disabled()] { @@ -334,17 +333,17 @@ impl Field { self } - /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: Lc) -> Self { - self.label = label; + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } - /// Establece el texto de ayuda del campo (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). #[builder_fn] - pub fn with_help_text(mut self, help_text: Lc) -> Self { - self.help_text = help_text; + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); self } @@ -390,8 +389,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: impl Into>) -> Self { - self.rows = rows.into(); + pub fn with_rows(mut self, rows: Option) -> Self { + self.rows.alter_opt(rows); self } @@ -405,11 +404,8 @@ 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: impl Into>, - ) -> Self { - self.autocomplete = autocomplete.into(); + pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { + self.autocomplete.alter_opt(autocomplete); self } diff --git a/src/base/component/form/textarea.rs b/src/base/component/form/textarea.rs index 97e047ce..5760d779 100644 --- a/src/base/component/form/textarea.rs +++ b/src/base/component/form/textarea.rs @@ -40,22 +40,19 @@ pub struct Textarea { /// Devuelve el valor inicial del área de texto. value: AttrValue, /// Devuelve la etiqueta del campo. - label: Lc, + label: Attr, /// Devuelve el texto de ayuda del campo. - help_text: Lc, + help_text: Attr, /// Devuelve el número de filas visibles del área de texto. - #[getters(copy)] - rows: Option, + rows: Attr, /// Devuelve la longitud mínima permitida en caracteres. - #[getters(copy)] - minlength: Option, + minlength: Attr, /// Devuelve la longitud máxima permitida en caracteres. - #[getters(copy)] - maxlength: Option, + maxlength: Attr, /// Devuelve el texto indicativo del área de texto. - placeholder: Lc, + placeholder: Attr, /// Devuelve la configuración de autocompletado del campo. - autocomplete: Option, + autocomplete: Attr, /// Devuelve si el campo recibe el foco automáticamente al cargar la página. autofocus: bool, /// Devuelve si el campo es de sólo lectura. @@ -79,7 +76,7 @@ impl Component for Textarea { fn setup(&mut self, _cx: &Context) { if let Some(container_id) = self .id() - .or_else(|| self.name().as_deref().map(|n| util::join!("edit-", n))) + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) { self.alter_prop(PropsOp::ensure_id(container_id)); } @@ -112,18 +109,18 @@ impl Component for Textarea { textarea id=[textarea_id.as_deref()] class="form-control" - name=[self.name().as_deref()] - rows=[self.rows()] - minlength=[self.minlength()] - maxlength=[self.maxlength()] + name=[self.name().get()] + rows=[self.rows().get()] + minlength=[self.minlength().get()] + maxlength=[self.maxlength().get()] placeholder=[self.placeholder().lookup(cx)] - autocomplete=[self.autocomplete()] + autocomplete=[self.autocomplete().get()] autofocus[*self.autofocus()] readonly[*self.readonly()] required[*self.required()] disabled[*self.disabled()] { - @if let Some(value) = self.value().as_deref() { + @if let Some(value) = self.value().get() { (value) } } @@ -169,17 +166,17 @@ impl Textarea { self } - /// Establece la etiqueta visible del campo (usa [`Lc::none()`] para quitarla). + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: Lc) -> Self { - self.label = label; + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } - /// Establece el texto de ayuda del campo (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). #[builder_fn] - pub fn with_help_text(mut self, help_text: Lc) -> Self { - self.help_text = help_text; + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); self } @@ -188,32 +185,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: impl Into>) -> Self { - self.rows = rows.into(); + pub fn with_rows(mut self, rows: Option) -> Self { + self.rows.alter_opt(rows); self } /// Establece la longitud mínima permitida en caracteres. #[builder_fn] - pub fn with_minlength(mut self, minlength: impl Into>) -> Self { - self.minlength = minlength.into(); + pub fn with_minlength(mut self, minlength: Option) -> Self { + self.minlength.alter_opt(minlength); self } /// Establece la longitud máxima permitida en caracteres. #[builder_fn] - pub fn with_maxlength(mut self, maxlength: impl Into>) -> Self { - self.maxlength = maxlength.into(); + pub fn with_maxlength(mut self, maxlength: Option) -> Self { + self.maxlength.alter_opt(maxlength); self } - /// Establece el texto indicativo del área de texto (usa [`Lc::none()`] para quitarlo). + /// Establece o elimina el texto indicativo del área de texto (`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: Lc) -> Self { - self.placeholder = placeholder; + pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { + self.placeholder.alter_opt(placeholder.into()); self } @@ -225,11 +222,8 @@ 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: impl Into>, - ) -> Self { - self.autocomplete = autocomplete.into(); + pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { + self.autocomplete.alter_opt(autocomplete); self } diff --git a/src/base/component/image/component.rs b/src/base/component/image/component.rs index 220a079e..ed799886 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: Lc, + alternative: Attr, } #[async_trait] @@ -140,7 +140,7 @@ impl Image { /// decorativa. #[builder_fn] pub fn with_alternative(mut self, alt: Lc) -> Self { - self.alternative = alt; + self.alternative.alter_value(alt); self } } diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index f2c306c1..4a8efb54 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: impl Into>) -> Self { - self.button = button.into(); + pub fn with_button(mut self, button: Option<(Lc, Route)>) -> Self { + self.button = button; self } diff --git a/src/base/component/pager.rs b/src/base/component/pager.rs index da24013e..1cf282ce 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_deref().unwrap_or(""); + let base_path = self.base_path().as_str().unwrap_or_default(); // 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); + let mut route = cx.route(base_path.to_owned()); 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 ea7604ad..cc55ca71 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_deref()] (link_props) { (label) } + a href=[sort.href().as_str()] (link_props) { (label) } } } } diff --git a/src/base/component/table/props.rs b/src/base/component/table/props.rs index c7b3acdc..c6446e92 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_deref(), Some("/admin/users?sort=email")); +/// assert_eq!(link.href().as_str(), 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 154b4945..2a89f2c2 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: impl Into>) -> Self { - self.0 = component.into().map(|c| Arc::new(c) as Arc); + pub fn with_component(mut self, component: Option) -> Self { + self.0 = component.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: impl Into>) -> Self { - self.0 = component.into().map(Arc::new); + pub fn with_component(mut self, component: Option) -> Self { + self.0 = component.map(Arc::new); self } diff --git a/src/html.rs b/src/html.rs index d2f88c52..a2d4d2d0 100644 --- a/src/html.rs +++ b/src/html.rs @@ -26,7 +26,7 @@ pub use logo::PageTopSvg; // **< HTML ATTRIBUTES >**************************************************************************** mod attr; -pub use attr::{AttrName, AttrValue}; +pub use attr::{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 5ad5b3a3..92593da3 100644 --- a/src/html/attr.rs +++ b/src/html/attr.rs @@ -1,5 +1,133 @@ +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. @@ -16,13 +144,13 @@ use crate::{AutoDefault, builder_fn, util}; /// ```rust /// # use pagetop::prelude::*; /// let name = AttrName::new(" DISplay name "); -/// assert_eq!(name.as_deref(), Some("display_name")); +/// assert_eq!(name.as_str(), Some("display_name")); /// /// let empty = AttrName::default(); /// assert_eq!(empty.get(), None); /// ``` #[derive(AutoDefault, Clone, Debug)] -pub struct AttrName(Option); +pub struct AttrName(Attr); impl AttrName { /// Crea un nuevo `AttrName` normalizando el valor. @@ -35,25 +163,33 @@ impl AttrName { /// Establece un nombre nuevo normalizando el valor. #[builder_fn] pub fn with_name(mut self, name: impl AsRef) -> Self { - self.0 = util::normalize_token(name); + self.0 = match util::normalize_token(name) { + Some(name) => Attr::some(name), + None => Attr::default(), + }; self } // **< AttrName GETTERS >*********************************************************************** - /// Devuelve el nombre normalizado (sin clonar), si existe. - pub fn as_deref(&self) -> Option<&str> { - self.0.as_deref() + /// Devuelve el nombre normalizado, si existe. + pub fn get(&self) -> Option { + self.0.get() } - /// Devuelve el nombre normalizado (clonado), si existe. - pub fn get(&self) -> Option { - self.0.clone() + /// Devuelve el nombre normalizado (sin clonar), si existe. + pub fn as_str(&self) -> Option<&str> { + self.0.as_str() + } + + /// Devuelve el nombre normalizado (propiedad), si existe. + pub fn into_inner(self) -> Option { + self.0.into_inner() } /// `true` si no hay valor. pub fn is_empty(&self) -> bool { - self.0.is_none() + self.0.is_empty() } } @@ -71,13 +207,13 @@ impl AttrName { /// ```rust /// # use pagetop::prelude::*; /// let s = AttrValue::new(" a new string "); -/// assert_eq!(s.as_deref(), Some("a new string")); +/// assert_eq!(s.as_str(), Some("a new string")); /// /// let empty = AttrValue::default(); /// assert_eq!(empty.get(), None); /// ``` #[derive(AutoDefault, Clone, Debug)] -pub struct AttrValue(Option); +pub struct AttrValue(Attr); impl AttrValue { /// Crea un nuevo `AttrValue` normalizando el valor. @@ -90,24 +226,32 @@ impl AttrValue { /// Establece una cadena nueva normalizando el valor. #[builder_fn] pub fn with_str(mut self, value: impl AsRef) -> Self { - self.0 = util::non_blank(value.as_ref()).map(str::to_string); + self.0 = match util::non_blank(value.as_ref()) { + Some(value) => Attr::some(value.to_string()), + None => Attr::default(), + }; self } // **< AttrValue GETTERS >********************************************************************** - /// Devuelve la cadena normalizada (sin clonar), si existe. - pub fn as_deref(&self) -> Option<&str> { - self.0.as_deref() + /// Devuelve la cadena normalizada, si existe. + pub fn get(&self) -> Option { + self.0.get() } - /// Devuelve la cadena normalizada (clonada), si existe. - pub fn get(&self) -> Option { - self.0.clone() + /// Devuelve la cadena normalizada (sin clonar), si existe. + pub fn as_str(&self) -> Option<&str> { + self.0.as_str() + } + + /// Devuelve la cadena normalizada (propiedad), si existe. + pub fn into_inner(self) -> Option { + self.0.into_inner() } /// `true` si no hay valor. pub fn is_empty(&self) -> bool { - self.0.is_none() + self.0.is_empty() } } diff --git a/src/locale/lc.rs b/src/locale/lc.rs index 0299d2da..93e4a84e 100644 --- a/src/locale/lc.rs +++ b/src/locale/lc.rs @@ -32,7 +32,6 @@ 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()`? /// @@ -123,23 +122,6 @@ 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())); @@ -160,8 +142,6 @@ 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 27ca749c..f9b2cc67 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 : Lc, - description: Lc, - metadata : Vec<(&'static str, &'static str)>, - properties : Vec<(&'static str, &'static str)>, - context : Context, + title : Attr, + description : Attr, + 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 = title; + self.title.alter_value(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 = description; + self.description.alter_value(description); self } diff --git a/tests/component_button.rs b/tests/component_button.rs deleted file mode 100644 index 5918d7c1..00000000 --- a/tests/component_button.rs +++ /dev/null @@ -1,45 +0,0 @@ -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("