,
}
impl StaticFilesBundle {
- /// Prepara el conjunto de recursos con los archivos de un directorio. Opcionalmente se puede
- /// aplicar un filtro para seleccionar un subconjunto de los archivos.
+ /// Crea el paquete de recursos con los archivos del directorio indicado.
///
/// # Argumentos
///
- /// * `dir` - Directorio que contiene los archivos.
- /// * `filter` - Una función opcional para aceptar o no un archivo según su ruta.
+ /// * `dir` - Ruta al directorio con los archivos a incluir, normalmente `static/` o un
+ /// directorio dentro de este.
+ /// * `filter` - Función opcional para seleccionar qué archivos incluir en el paquete.
///
/// # Ejemplo
///
@@ -145,124 +177,227 @@ impl StaticFilesBundle {
/// use std::path::Path;
///
/// fn main() -> std::io::Result<()> {
- /// fn only_images(path: &Path) -> bool {
- /// matches!(
- /// path.extension().and_then(|ext| ext.to_str()),
- /// Some("jpg" | "png" | "gif")
- /// )
- /// }
- ///
/// StaticFilesBundle::from_dir("./static", Some(only_images))
/// .with_name("images")
/// .build()
/// }
+ ///
+ /// fn only_images(path: &Path) -> bool {
+ /// matches!(
+ /// path.extension().and_then(|ext| ext.to_str()),
+ /// Some("jpg" | "png" | "gif")
+ /// )
+ /// }
/// ```
pub fn from_dir(dir: P, filter: Option bool>) -> Self
where
P: AsRef,
{
- let dir_path = dir.as_ref();
- let dir_str = dir_path.to_str().unwrap_or_else(|| {
- panic!(
- "Resource directory path is not valid UTF-8: {}",
- dir_path.display()
- );
- });
-
- let mut resource_dir = resource_dir(dir_str);
-
- // Aplica el filtro si está definido.
- if let Some(f) = filter {
- resource_dir.with_filter(f);
+ Self {
+ dir: dir.as_ref().to_path_buf(),
+ filter,
+ name: None,
}
-
- // Identifica el directorio temporal de recursos.
- StaticFilesBundle { resource_dir }
}
- /// Prepara un recurso CSS minimizado a partir de la compilación de un archivo SCSS (que puede a
- /// su vez importar otros archivos SCSS).
+ /// Asigna un nombre al paquete de recursos.
///
- /// # Argumentos
+ /// El nombre debe ser un identificador Rust válido que se convertirá en nombre del módulo y de
+ /// la función del archivo `.rs` generado en `OUT_DIR`. Si no se llama a este método, el nombre
+ /// por defecto será `"bundle"`.
///
- /// * `path` - Archivo SCSS a compilar.
- /// * `target_name` - Nombre para el archivo CSS.
+ /// Este nombre es el que hay que declarar en
+ /// [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html)
+ /// para configurar la ruta del servicio:
///
- /// # Ejemplo
- ///
- /// ```rust,no_run
- /// use pagetop_build::StaticFilesBundle;
- ///
- /// fn main() -> std::io::Result<()> {
- /// StaticFilesBundle::from_scss("./bootstrap/scss/main.scss", "bootstrap.min.css")
- /// .with_name("bootstrap_css")
- /// .build()
- /// }
+ /// ```rust,ignore
+ /// serve_static_files!(router, ["./static/css", app_css] => "/public/css");
+ /// // ^^^^^^^
+ /// // debe coincidir con .with_name("app_css")
/// ```
- pub fn from_scss(path: P, target_name: &str) -> Self
- where
- P: AsRef,
- {
- // Crea un directorio temporal único para el archivo CSS (basado en su nombre, para que
- // varias llamadas a from_scss en el mismo build.rs no se pisen).
- let out_dir = std::env::var("OUT_DIR").unwrap();
- let safe_name = target_name.replace(['.', '-'], "_");
- let temp_dir = Path::new(&out_dir).join(format!("from_scss_{safe_name}"));
-
- // Limpia el directorio temporal de ejecuciones previas, si existe.
- if temp_dir.exists() {
- remove_dir_all(&temp_dir).unwrap_or_else(|e| {
- panic!(
- "Failed to clean temporary directory `{}`: {e}",
- temp_dir.display()
- );
- });
- }
- create_dir_all(&temp_dir).unwrap_or_else(|e| {
- panic!(
- "Failed to create temporary directory `{}`: {e}",
- temp_dir.display()
- );
- });
-
- // Compila SCSS a CSS.
- let css_content = from_path(
- path.as_ref(),
- &Options::default().style(OutputStyle::Compressed),
- )
- .unwrap_or_else(|e| {
- panic!(
- "Failed to compile SCSS file `{}`: {e}",
- path.as_ref().display(),
- )
- });
-
- // Guarda el archivo CSS compilado en el directorio temporal.
- let css_path = temp_dir.join(target_name);
- File::create(&css_path)
- .unwrap_or_else(|_| panic!("Failed to create CSS file `{}`", css_path.display()))
- .write_all(css_content.as_bytes())
- .unwrap_or_else(|_| panic!("Failed to write CSS content to `{}`", css_path.display()));
-
- // Identifica el directorio temporal de recursos.
- StaticFilesBundle {
- resource_dir: resource_dir(temp_dir.to_str().unwrap()),
- }
- }
-
- /// Asigna un nombre al conjunto de recursos.
pub fn with_name(mut self, name: impl AsRef) -> Self {
- let name = name.as_ref();
- let out_dir = std::env::var("OUT_DIR").unwrap();
- let filename = Path::new(&out_dir).join(format!("{name}.rs"));
- self.resource_dir.with_generated_filename(filename);
- self.resource_dir.with_module_name(format!("bundle_{name}"));
- self.resource_dir.with_generated_fn(name);
+ self.name = Some(name.as_ref().to_string());
self
}
- /// Contruye finalmente el conjunto de recursos para incluir en el binario de la aplicación.
+ /// Genera el archivo `.rs` en `OUT_DIR` para incluir los recursos del directorio en el binario.
pub fn build(self) -> std::io::Result<()> {
- self.resource_dir.build()
+ let out_dir = std::env::var("OUT_DIR").unwrap();
+ let name = self.name.as_deref().unwrap_or("bundle");
+
+ let mut rd = resource_dir(&self.dir);
+ if let Some(f) = self.filter {
+ rd.with_filter(f);
+ }
+
+ let generated_filename = PathBuf::from(&out_dir).join(format!("{name}.rs"));
+ rd.with_generated_filename(generated_filename);
+ rd.with_module_name(format!("bundle_{name}"));
+ rd.with_generated_fn(name);
+ rd.build()
}
}
+
+// **< compile_scss / copy_dir / copy_file / copy_file_replacing / minify_js >**********************
+
+/// Compila un archivo SCSS a CSS minificado y lo escribe en la ruta de destino.
+///
+/// Crea el directorio padre del destino si no existe.
+///
+/// # Ejemplo
+///
+/// ```rust,no_run
+/// fn main() -> std::io::Result<()> {
+/// pagetop_build::compile_scss("assets/main.scss", "static/css/main.min.css")
+/// }
+/// ```
+pub fn compile_scss(src: P, dst: Q) -> io::Result<()>
+where
+ P: AsRef,
+ Q: AsRef,
+{
+ let src = src.as_ref();
+ let dst = dst.as_ref();
+
+ if let Some(parent) = dst.parent() {
+ create_dir_all(parent)?;
+ }
+
+ let options = Options::default().style(OutputStyle::Compressed);
+ let css = from_path(src, &options)
+ .map_err(|e| io::Error::other(format!("failed to compile `{}`: {e}", src.display())))?;
+ File::create(dst)?.write_all(css.as_bytes())
+}
+
+/// Copia recursivamente el contenido de un directorio a otro destino.
+///
+/// Crea el directorio destino y todos los subdirectorios necesarios.
+///
+/// # Ejemplo
+///
+/// ```rust,no_run
+/// fn main() -> std::io::Result<()> {
+/// pagetop_build::copy_dir("assets", "static")
+/// }
+/// ```
+pub fn copy_dir(src: P, dst: Q) -> io::Result<()>
+where
+ P: AsRef,
+ Q: AsRef,
+{
+ let src = src.as_ref();
+ let dst = dst.as_ref();
+ create_dir_all(dst)?;
+ for entry in read_dir(src)? {
+ let entry = entry?;
+ let src_path = entry.path();
+ let dst_path = dst.join(entry.file_name());
+ if src_path.is_dir() {
+ copy_dir(&src_path, &dst_path)?;
+ } else {
+ fs_copy(&src_path, &dst_path)?;
+ }
+ }
+ Ok(())
+}
+
+/// Copia un archivo a su destino.
+///
+/// Crea el directorio padre del destino si no existe.
+///
+/// # Ejemplo
+///
+/// ```rust,no_run
+/// fn main() -> std::io::Result<()> {
+/// pagetop_build::copy_file("assets/fonts/icon.woff2", "static/fonts/icon.woff2")
+/// }
+/// ```
+pub fn copy_file(src: P, dst: Q) -> io::Result<()>
+where
+ P: AsRef,
+ Q: AsRef,
+{
+ let src = src.as_ref();
+ let dst = dst.as_ref();
+
+ if let Some(parent) = dst.parent() {
+ create_dir_all(parent)?;
+ }
+
+ fs_copy(src, dst)?;
+ Ok(())
+}
+
+/// Copia un archivo a su destino con una lista de sustituciones de texto en su contenido.
+///
+/// El archivo fuente se lee como texto UTF-8; no debe usarse con archivos binarios. Las
+/// sustituciones de texto se aplican en orden y de forma encadenada: el resultado de cada
+/// sustitución puede ser entrada de la siguiente.
+///
+/// Crea el directorio padre del destino si no existe.
+///
+/// # Ejemplo
+///
+/// ```rust,no_run
+/// fn main() -> std::io::Result<()> {
+/// pagetop_build::copy_file_replacing(
+/// "assets/adminlte.min.js",
+/// "static/js/myapp.min.js",
+/// &[("adminlte.min.js.map", "myapp.min.js.map")],
+/// )
+/// }
+/// ```
+pub fn copy_file_replacing(src: P, dst: Q, replacements: &[(&str, &str)]) -> io::Result<()>
+where
+ P: AsRef,
+ Q: AsRef,
+{
+ let src = src.as_ref();
+ let dst = dst.as_ref();
+
+ if let Some(parent) = dst.parent() {
+ create_dir_all(parent)?;
+ }
+
+ let content = std::fs::read_to_string(src)?;
+ let patched = replacements
+ .iter()
+ .fold(content, |acc, (old, new)| acc.replace(old, new));
+ File::create(dst)?.write_all(patched.as_bytes())
+}
+
+/// Minifica un archivo JavaScript y lo escribe en la ruta de destino.
+///
+/// El archivo se procesa en modo de ámbito global (`TopLevelMode::Global`), adecuado para scripts
+/// sin `import`/`export`. Los archivos con sintaxis de módulo ES deben procesarse con
+/// `TopLevelMode::Module`, que el *crate* subyacente (`minify-js`) también soporta pero esta
+/// función no expone actualmente.
+///
+/// Crea el directorio padre del destino si no existe.
+///
+/// # Ejemplo
+///
+/// ```rust,no_run
+/// fn main() -> std::io::Result<()> {
+/// pagetop_build::minify_js("assets/shell.js", "static/js/shell.min.js")
+/// }
+/// ```
+pub fn minify_js(src: P, dst: Q) -> io::Result<()>
+where
+ P: AsRef,
+ Q: AsRef,
+{
+ let src = src.as_ref();
+ let dst = dst.as_ref();
+
+ if let Some(parent) = dst.parent() {
+ create_dir_all(parent)?;
+ }
+
+ let source = std::fs::read(src)?;
+ let session = Session::new();
+ let mut output = Vec::new();
+ minify(&session, TopLevelMode::Global, &source, &mut output)
+ .map_err(|e| io::Error::other(format!("failed to minify `{}`: {e:?}", src.display())))?;
+ File::create(dst)?.write_all(&output)
+}
diff --git a/helpers/pagetop-macros/README.md b/helpers/pagetop-macros/README.md
index 9b0174a6..599f81bb 100644
--- a/helpers/pagetop-macros/README.md
+++ b/helpers/pagetop-macros/README.md
@@ -11,14 +11,13 @@
-## 🧭 Sobre PageTop
+## Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
configurables, basadas en HTML, CSS y JavaScript.
-
-## 📚 Créditos
+## Créditos
Este *crate* incluye entre sus macros una adaptación de
[maud-macros](https://crates.io/crates/maud_macros)
@@ -29,15 +28,13 @@ Este *crate* incluye entre sus macros una adaptación de
necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de
cada proyecto PageTop.
-
-## 🚧 Advertencia
+## Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
-
-## 📜 Licencia
+## Licencia
El código está disponible bajo una doble licencia:
diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs
index 63349aa0..4557196e 100644
--- a/helpers/pagetop-macros/src/lib.rs
+++ b/helpers/pagetop-macros/src/lib.rs
@@ -31,7 +31,7 @@ cada proyecto PageTop.
*/
#![doc(
- html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
+ html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico"
)]
mod maud;
@@ -126,7 +126,7 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
///
/// Si defines un método `with_` como este:
///
-/// ```rust
+/// ```rust,no_run
/// # use pagetop_macros::builder_fn;
/// # struct Example {value: Option};
/// # impl Example {
@@ -140,7 +140,7 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
///
/// la macro reescribirá el método `with_` y generará un nuevo método `alter_`:
///
-/// ```rust
+/// ```rust,no_run
/// # struct Example {value: Option};
/// # impl Example {
/// #[inline]
diff --git a/helpers/pagetop-minimal/README.md b/helpers/pagetop-minimal/README.md
index 1f8ec148..b7a17bc0 100644
--- a/helpers/pagetop-minimal/README.md
+++ b/helpers/pagetop-minimal/README.md
@@ -11,21 +11,19 @@
-## 🧭 Sobre PageTop
+## Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
configurables, basadas en HTML, CSS y JavaScript.
-
-## 🗺️ Descripción general
+## Descripción general
Este *crate* proporciona un conjunto básico de macros que se integran en las utilidades de PageTop
para optimizar operaciones habituales relacionadas con la composición estructurada de texto, la
concatenación de cadenas y el uso rápido de colecciones clave-valor.
-
-## 📚 Créditos
+## Créditos
Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del
*crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay).
@@ -39,15 +37,13 @@ La macro para generar identificadores dinámicos **`paste!`** se reexporta del *
[pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular
`paste!` de [David Tolnay](https://crates.io/users/dtolnay).
-
-## 🚧 Advertencia
+## Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
-
-## 📜 Licencia
+## Licencia
El código está disponible bajo una doble licencia:
diff --git a/helpers/pagetop-minimal/src/lib.rs b/helpers/pagetop-minimal/src/lib.rs
index 3b8c9036..eab70c84 100644
--- a/helpers/pagetop-minimal/src/lib.rs
+++ b/helpers/pagetop-minimal/src/lib.rs
@@ -40,7 +40,7 @@ La macro para generar identificadores dinámicos **`paste!`** se reexporta del *
*/
#![doc(
- html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
+ html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico"
)]
#[doc(hidden)]
diff --git a/helpers/pagetop-statics/README.md b/helpers/pagetop-statics/README.md
index 3184f095..92541096 100644
--- a/helpers/pagetop-statics/README.md
+++ b/helpers/pagetop-statics/README.md
@@ -31,15 +31,13 @@ Para ello, adapta el código de [static-files](https://crates.io/crates/static_f
se integra en PageTop para evitar que cada proyecto tenga que declarar `static-files` manualmente
como dependencia en su `Cargo.toml`.
-
-## 🚧 Advertencia
+## Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
-
-## 📜 Licencia
+## Licencia
El código está disponible bajo una doble licencia:
diff --git a/helpers/pagetop-statics/src/lib.rs b/helpers/pagetop-statics/src/lib.rs
index d72176c6..426991d5 100644
--- a/helpers/pagetop-statics/src/lib.rs
+++ b/helpers/pagetop-statics/src/lib.rs
@@ -35,12 +35,14 @@ como dependencia en su `Cargo.toml`.
#![doc(test(no_crate_inject))]
#![doc(
- html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
+ html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico"
)]
#![allow(clippy::needless_doctest_main)]
/// Resource definition and single module based generation.
pub mod resource;
+
+#[doc(inline)]
pub use resource::Resource as StaticFile;
mod resource_dir;
diff --git a/src/base/action/component/after_render_component.rs b/src/base/action/component/after_render_component.rs
index 0778e9c8..08eefdb5 100644
--- a/src/base/action/component/after_render_component.rs
+++ b/src/base/action/component/after_render_component.rs
@@ -6,7 +6,7 @@ use super::FnActionWithComponent;
pub struct AfterRender {
f: FnActionWithComponent,
referer_type_id: Option,
- referer_id: AttrId,
+ referer_id: Option,
weight: Weight,
}
@@ -19,7 +19,7 @@ impl ActionDispatcher for AfterRender {
/// Devuelve el identificador del componente.
fn referer_id(&self) -> Option {
- self.referer_id.get()
+ self.referer_id.clone()
}
/// Devuelve el peso para definir el orden de ejecución.
@@ -34,7 +34,7 @@ impl AfterRender {
AfterRender {
f,
referer_type_id: Some(UniqueId::of::()),
- referer_id: AttrId::default(),
+ referer_id: None,
weight: 0,
}
}
@@ -42,7 +42,8 @@ impl AfterRender {
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
/// `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self {
- self.referer_id.alter_id(id);
+ let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_");
+ self.referer_id = if id.is_empty() { None } else { Some(id) };
self
}
diff --git a/src/base/action/component/before_render_component.rs b/src/base/action/component/before_render_component.rs
index 051a3dd6..e91589a2 100644
--- a/src/base/action/component/before_render_component.rs
+++ b/src/base/action/component/before_render_component.rs
@@ -6,7 +6,7 @@ use super::FnActionWithComponent;
pub struct BeforeRender {
f: FnActionWithComponent,
referer_type_id: Option,
- referer_id: AttrId,
+ referer_id: Option,
weight: Weight,
}
@@ -19,7 +19,7 @@ impl ActionDispatcher for BeforeRender {
/// Devuelve el identificador del componente.
fn referer_id(&self) -> Option {
- self.referer_id.get()
+ self.referer_id.clone()
}
/// Devuelve el peso para definir el orden de ejecución.
@@ -34,7 +34,7 @@ impl BeforeRender {
BeforeRender {
f,
referer_type_id: Some(UniqueId::of::()),
- referer_id: AttrId::default(),
+ referer_id: None,
weight: 0,
}
}
@@ -42,7 +42,8 @@ impl BeforeRender {
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
/// `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self {
- self.referer_id.alter_id(id);
+ let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_");
+ self.referer_id = if id.is_empty() { None } else { Some(id) };
self
}
diff --git a/src/base/action/component/transform_markup_component.rs b/src/base/action/component/transform_markup_component.rs
index 3e3a81f5..bed2e192 100644
--- a/src/base/action/component/transform_markup_component.rs
+++ b/src/base/action/component/transform_markup_component.rs
@@ -6,7 +6,7 @@ use super::FnActionTransformMarkup;
pub struct TransformMarkup {
f: FnActionTransformMarkup,
referer_type_id: Option,
- referer_id: AttrId,
+ referer_id: Option,
weight: Weight,
}
@@ -19,7 +19,7 @@ impl ActionDispatcher for TransformMarkup {
/// Devuelve el identificador del componente.
fn referer_id(&self) -> Option {
- self.referer_id.get()
+ self.referer_id.clone()
}
/// Devuelve el peso para definir el orden de ejecución.
@@ -34,7 +34,7 @@ impl TransformMarkup {
TransformMarkup {
f,
referer_type_id: Some(UniqueId::of::()),
- referer_id: AttrId::default(),
+ referer_id: None,
weight: 0,
}
}
@@ -42,7 +42,8 @@ impl TransformMarkup {
/// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el
/// componente `C` con identificador `id`.
pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self {
- self.referer_id.alter_id(id);
+ let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_");
+ self.referer_id = if id.is_empty() { None } else { Some(id) };
self
}
diff --git a/src/base/component.rs b/src/base/component.rs
index 7ea596d3..5c9fee3d 100644
--- a/src/base/component.rs
+++ b/src/base/component.rs
@@ -1,11 +1,18 @@
//! Componentes nativos proporcionados por PageTop.
-mod html;
-pub use html::Html;
-
mod block;
pub use block::Block;
+mod button;
+pub use button::{Button, ButtonAction};
+
+pub mod form;
+#[doc(inline)]
+pub use form::Form;
+
+mod html;
+pub use html::Html;
+
mod intro;
pub use intro::{Intro, IntroOpening};
diff --git a/src/base/component/block.rs b/src/base/component/block.rs
index 01f10d42..2e56c5b0 100644
--- a/src/base/component/block.rs
+++ b/src/base/component/block.rs
@@ -6,10 +6,8 @@ use crate::prelude::*;
/// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*).
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Block {
- #[getters(skip)]
- id: AttrId,
- /// Devuelve las clases CSS asociadas al bloque.
- classes: Classes,
+ /// Devuelve identificador, clases CSS y atributos HTML del componente.
+ props: Props,
/// Devuelve el título del bloque.
title: L10n,
/// Devuelve la lista de componentes hijo del bloque.
@@ -22,11 +20,15 @@ impl Component for Block {
}
fn id(&self) -> Option {
- self.id.get()
+ self.props.get_id()
}
- fn setup(&mut self, _cx: &Context) {
- self.alter_classes(ClassesOp::Prepend, "block");
+ fn setup(&mut self, cx: &Context) {
+ // Asegura que el bloque tiene un identificador único.
+ self.alter_prop(PropsOp::ensure_id(cx.build_id::(1)));
+
+ // Todos los bloques tienen la clase CSS `block` por defecto.
+ self.alter_prop(PropsOp::prepend_classes("block"));
}
fn prepare(&self, cx: &mut Context) -> Result {
@@ -36,14 +38,12 @@ impl Component for Block {
return Ok(html! {});
}
- let id = cx.required_id::(self.id(), 1);
-
Ok(html! {
- div id=(&id) class=[self.classes().get()] {
+ div (self.props()) {
@if let Some(title) = self.title().lookup(cx) {
- h2 class="block__title" { span { (title) } }
+ h2 class="block-title" { span { (title) } }
}
- div class="block__body" { (block_body) }
+ div class="block-body" { (block_body) }
}
})
}
@@ -52,17 +52,17 @@ impl Component for Block {
impl Block {
// **< Block BUILDER >**************************************************************************
- /// Establece el identificador único (`id`) del bloque.
+ /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
#[builder_fn]
- pub fn with_id(mut self, id: impl AsRef) -> Self {
- self.id.alter_id(id);
+ pub fn with_id(mut self, id: impl Into) -> Self {
+ self.props.alter_id(id);
self
}
- /// Modifica la lista de clases CSS aplicadas al bloque.
+ /// Modifica identificador, clases CSS o atributos HTML del componente.
#[builder_fn]
- pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self {
- self.classes.alter_classes(op, classes);
+ pub fn with_prop(mut self, op: PropsOp) -> Self {
+ self.props.alter_prop(op);
self
}
diff --git a/src/base/component/button.rs b/src/base/component/button.rs
new file mode 100644
index 00000000..8d2f0fa7
--- /dev/null
+++ b/src/base/component/button.rs
@@ -0,0 +1,207 @@
+use crate::prelude::*;
+
+use std::fmt;
+
+// **< ButtonAction >*******************************************************************************
+
+/// Comportamiento de un [`Button`] al activarse.
+#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
+pub enum ButtonAction {
+ /// Envía un formulario al servidor. Es el **tipo por defecto**.
+ #[default]
+ Submit,
+ /// Restablece todos los campos de un formulario a sus valores iniciales.
+ Reset,
+ /// Botón de propósito general, sin efecto predeterminado. Su comportamiento podría definirse
+ /// mediante JavaScript.
+ Plain,
+}
+
+impl fmt::Display for ButtonAction {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(match self {
+ ButtonAction::Submit => "submit",
+ ButtonAction::Reset => "reset",
+ ButtonAction::Plain => "button",
+ })
+ }
+}
+
+// **< Button >*************************************************************************************
+
+/// Componente para crear un **botón**.
+///
+/// Renderiza un botón con soporte para las variantes disponibles en [`ButtonAction`] (`submit`,
+/// `reset` y botón genérico).
+///
+/// El comportamiento del botón se establece al crearlo:
+///
+/// - [`Button::submit()`]: botón de envío (por defecto).
+/// - [`Button::reset()`]: botón de restablecimiento de valores.
+/// - [`Button::plain()`]: botón genérico sin comportamiento predeterminado.
+///
+/// El botón puede usarse dentro o fuera de un formulario.
+///
+/// # Ejemplo
+///
+/// ```rust,no_run
+/// use pagetop::prelude::*;
+///
+/// let save = Button::submit(L10n::n("Save"));
+/// let cancel = Button::plain(L10n::n("Cancel"));
+/// let clear = Button::reset(L10n::n("Clear"));
+/// ```
+///
+/// Cuando el botón activa el envío, el navegador incluye el par `name=value` en los datos del
+/// formulario **sólo si** tiene el atributo `name` definido. Es la forma habitual de identificar
+/// cuál de los botones de envío fue pulsado. En el servidor se deserializa como `Option`:
+///
+/// ```rust,ignore
+/// #[derive(serde::Deserialize)]
+/// struct FormData {
+/// #[serde(default)]
+/// action: Option, // p. ej., "save" o "delete"; `None` si el botón no tenía `name`.
+/// }
+/// ```
+#[derive(AutoDefault, Clone, Debug, Getters)]
+pub struct Button {
+ /// Devuelve identificador, clases CSS y atributos HTML del componente.
+ props: Props,
+ /// Devuelve el comportamiento del botón al activarse.
+ kind: ButtonAction,
+ /// Devuelve el nombre del botón.
+ name: AttrName,
+ /// Devuelve el valor del botón.
+ value: AttrValue,
+ /// Devuelve la etiqueta del botón.
+ label: 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.
+ disabled: bool,
+}
+
+impl Component for Button {
+ 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("button"));
+ }
+
+ fn prepare(&self, cx: &mut Context) -> Result {
+ Ok(html! {
+ button
+ type=(self.kind())
+ (self.props())
+ name=[self.name().get()]
+ value=[self.value().get()]
+ autofocus[*self.autofocus()]
+ disabled[*self.disabled()]
+ {
+ @if let Some(label) = self.label().lookup(cx) {
+ (label)
+ }
+ }
+ })
+ }
+}
+
+impl Button {
+ /// Crea un botón de **envío** (`type="submit"`).
+ ///
+ /// Es la acción predeterminada al pulsar un botón en la mayoría de los formularios: envía los
+ /// datos al servidor.
+ pub fn submit(label: L10n) -> Self {
+ Self {
+ kind: ButtonAction::Submit,
+ label: Attr::some(label),
+ ..Default::default()
+ }
+ }
+
+ /// Crea un botón de **restablecimiento** (`type="reset"`).
+ ///
+ /// Al pulsarlo, devuelve todos los campos del formulario a sus valores iniciales.
+ pub fn reset(label: L10n) -> Self {
+ Self {
+ kind: ButtonAction::Reset,
+ label: Attr::some(label),
+ ..Default::default()
+ }
+ }
+
+ /// Crea un **botón genérico** (`type="button"`).
+ ///
+ /// No tiene un comportamiento predeterminado sobre el formulario. Su comportamiento puede
+ /// definirse mediante JavaScript.
+ pub fn plain(label: L10n) -> Self {
+ Self {
+ kind: ButtonAction::Plain,
+ label: Attr::some(label),
+ ..Default::default()
+ }
+ }
+
+ // **< Button 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 o atributos HTML del componente.
+ #[builder_fn]
+ pub fn with_prop(mut self, op: PropsOp) -> Self {
+ self.props.alter_prop(op);
+ self
+ }
+
+ /// Establece el nombre del botón (atributo `name`).
+ ///
+ /// Cuando el formulario tiene varios botones de envío, el navegador incluye en el envío el par
+ /// `name=value` sólo del botón que activó el formulario. Permite identificar cuál fue pulsado.
+ #[builder_fn]
+ pub fn with_name(mut self, name: impl AsRef