Incluye columnas ordenables (`table::SortLink`), paso de props/atributos a filas y celdas para HTMX, y un mensaje de "sin datos" traducible con valor por defecto.
168 lines
6.2 KiB
Rust
168 lines
6.2 KiB
Rust
use crate::prelude::*;
|
|
|
|
/// Componente para representar **tablas de datos**.
|
|
///
|
|
/// `Table` resuelve la estructura HTML de cualquier listado tabular (`<table>`, `<thead>`,
|
|
/// `<tbody>` y la fila para una tabla "sin resultados") y deja el resto en manos de quien lo usa:
|
|
/// qué contiene cada celda, qué columnas son ordenables y hacia dónde apuntan sus enlaces, y la
|
|
/// paginación (que se compone aparte, normalmente junto a `Table` dentro de un mismo contenedor).
|
|
///
|
|
/// # Clases CSS
|
|
///
|
|
/// - `.table-responsive`: envuelve `<table>` para admitir scroll horizontal sólo de la tabla si no
|
|
/// cabe en el ancho disponible.
|
|
/// - `.table`: clase base del elemento `<table>`.
|
|
/// - `.table-sort`: presente en el enlace de una cabecera ordenable.
|
|
/// - `.table-sort-asc` / `.table-sort-desc`: añadidas junto a `.table-sort` cuando esa columna es
|
|
/// la que determina el orden vigente. El tema activo puede usarlas para dibujar el indicador
|
|
/// visual (flecha, icono...) mediante CSS; `Table` no incluye ningún glifo por sí misma.
|
|
/// - `.table-empty`: clase de la celda con el mensaje mostrado cuando no hay filas.
|
|
///
|
|
/// # Ejemplo
|
|
///
|
|
/// ```rust,no_run
|
|
/// use pagetop::prelude::*;
|
|
///
|
|
/// let current_sort = "username";
|
|
/// let current_dir = SortDir::Asc;
|
|
///
|
|
/// // Definición de la tabla y cabeceras.
|
|
/// let mut table = Table::new()
|
|
/// .with_column(
|
|
/// table::Column::new(Lc::n("User")).with_sort(
|
|
/// table::SortLink::new("/admin/users?sort=username")
|
|
/// .with_dir((current_sort == "username").then_some(current_dir)),
|
|
/// ),
|
|
/// )
|
|
/// .with_column(Lc::n("Email"))
|
|
/// // Con `None` en vez de un `Lc`, se desactiva el mensaje predeterminado.
|
|
/// .with_empty(Lc::n("No users to display."));
|
|
///
|
|
/// // Contenido de la tabla.
|
|
/// for (username, email) in [("julia", "julia@example.com"), ("Fran", "fran@example.com")] {
|
|
/// table.alter_row(
|
|
/// table::Row::new()
|
|
/// // Ambos campos son del mismo tipo. Uno lo asignamos en un componente `Html`.
|
|
/// .with_cell(table::Cell::new(Html::with(move |_| html! { (username) })))
|
|
/// // Y el otro como `&str`, que se convierte directamente en `Cell`.
|
|
/// .with_cell(email),
|
|
/// );
|
|
/// }
|
|
/// ```
|
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
pub struct Table {
|
|
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
|
props: Props,
|
|
/// Devuelve las columnas de la tabla, en orden de aparición.
|
|
columns: Vec<table::Column>,
|
|
/// Devuelve las filas de datos de la tabla, en orden de aparición.
|
|
rows: Vec<table::Row>,
|
|
/// Devuelve el mensaje mostrado cuando no hay filas, o `None` si está desactivado.
|
|
#[default(Some(Lc::l("table_empty")))]
|
|
empty: Option<Lc>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Component for Table {
|
|
fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
fn id(&self) -> Option<String> {
|
|
self.props.get_id()
|
|
}
|
|
|
|
fn setup(&mut self, _cx: &Context) {
|
|
self.alter_prop(PropsOp::prepend_classes("table"));
|
|
}
|
|
|
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
let colspan = self.columns().len().max(1).to_string();
|
|
|
|
Ok(html! {
|
|
div.table-responsive {
|
|
table (self.props()) {
|
|
@if !self.columns().is_empty() {
|
|
thead {
|
|
tr {
|
|
@for column in self.columns() {
|
|
(column.render_header(cx))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@if !self.rows().is_empty() {
|
|
tbody {
|
|
@for row in self.rows() {
|
|
tr (row.props()) {
|
|
@for cell in row.cells() {
|
|
td (cell.props()) { (cell.children().render(cx).await) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} @else if let Some(empty) = self
|
|
.empty()
|
|
.and_then(|e| e.lookup(cx))
|
|
.filter(|s| !s.is_empty())
|
|
{
|
|
tbody {
|
|
tr {
|
|
td.table-empty colspan=(colspan) { (empty) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Table {
|
|
// **< Table BUILDER >**************************************************************************
|
|
|
|
/// Establece el identificador único de la tabla.
|
|
#[builder_fn]
|
|
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
|
self.props.alter_id(id);
|
|
self
|
|
}
|
|
|
|
/// Modifica identificador, clases CSS o atributos HTML de la tabla.
|
|
#[builder_fn]
|
|
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
|
self.props.alter_prop(op);
|
|
self
|
|
}
|
|
|
|
/// Añade una columna al final de la cabecera.
|
|
///
|
|
/// Acepta directamente un `&str`, un `String` o un [`Lc`] (que equivalen a
|
|
/// `table::Column::new(...)` con el texto indicado), o un [`table::Column`] ya construido (por
|
|
/// ejemplo para asignarle clases, atributos propios o un enlace de ordenación con
|
|
/// `with_sort()`).
|
|
#[builder_fn]
|
|
pub fn with_column(mut self, column: impl Into<table::Column>) -> Self {
|
|
self.columns.push(column.into());
|
|
self
|
|
}
|
|
|
|
/// Añade una fila de datos al final de la tabla.
|
|
#[builder_fn]
|
|
pub fn with_row(mut self, row: table::Row) -> Self {
|
|
self.rows.push(row);
|
|
self
|
|
}
|
|
|
|
/// Sustituye el mensaje mostrado cuando no hay filas, o lo desactiva con `None`: en ese caso,
|
|
/// una tabla sin filas no muestra ninguna fila de reemplazo; sólo se renderizan `<table>` y, si
|
|
/// hay columnas, `<thead>`.
|
|
///
|
|
/// Ese mismo resultado se obtiene también si la traducción no resuelve a ningún texto (por
|
|
/// ejemplo, con `Lc::n("")`).
|
|
#[builder_fn]
|
|
pub fn with_empty(mut self, empty: impl Into<Option<Lc>>) -> Self {
|
|
self.empty = empty.into();
|
|
self
|
|
}
|
|
}
|