diff --git a/src/base/component.rs b/src/base/component.rs index 687d2995..6bb4e8b9 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -34,7 +34,3 @@ pub use pager::{Pager, PagerAlign, PagerVisibility}; mod poweredby; pub use poweredby::PoweredBy; - -pub mod table; -#[doc(inline)] -pub use table::Table; diff --git a/src/base/component/form/check.rs b/src/base/component/form/check.rs index e0021e6c..c6c66321 100644 --- a/src/base/component/form/check.rs +++ b/src/base/component/form/check.rs @@ -42,14 +42,12 @@ impl Item { // **< Item BUILDER >*************************************************************************** /// Establece si la casilla debe aparecer marcada por defecto. - #[builder_fn] pub fn with_checked(mut self, checked: bool) -> Self { self.checked = checked; self } /// Establece si la casilla está deshabilitada. - #[builder_fn] pub fn with_disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self diff --git a/src/base/component/form/radio.rs b/src/base/component/form/radio.rs index 2cd39b54..50451c61 100644 --- a/src/base/component/form/radio.rs +++ b/src/base/component/form/radio.rs @@ -46,14 +46,12 @@ impl Item { /// /// Si varias opciones del grupo tienen `checked` activo, sólo la primera se renderizará como /// seleccionada; las demás se ignorarán. - #[builder_fn] pub fn with_checked(mut self, checked: bool) -> Self { self.checked = checked; self } /// Establece si la opción está inicialmente deshabilitada. - #[builder_fn] pub fn with_disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self diff --git a/src/base/component/form/select.rs b/src/base/component/form/select.rs index 8de719df..5f73cca1 100644 --- a/src/base/component/form/select.rs +++ b/src/base/component/form/select.rs @@ -50,14 +50,12 @@ impl Item { /// En una lista de selección única, el navegador aplica la selección al último elemento marcado /// si hay más de uno; mientras que en una lista múltiple se respetan todos los elementos /// marcados. - #[builder_fn] pub fn with_selected(mut self, selected: bool) -> Self { self.selected = selected; self } /// Establece si el elemento está deshabilitado. - #[builder_fn] pub fn with_disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self @@ -102,14 +100,12 @@ impl Group { // **< Group BUILDER >************************************************************************** /// Añade un elemento al grupo. Los elementos se muestran en el orden en que se añaden. - #[builder_fn] pub fn with_item(mut self, item: Item) -> Self { self.items.push(item); self } /// Establece si el grupo de elementos está deshabilitado en bloque. - #[builder_fn] pub fn with_disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self diff --git a/src/base/component/html.rs b/src/base/component/html.rs index 7098c756..9bb74331 100644 --- a/src/base/component/html.rs +++ b/src/base/component/html.rs @@ -100,18 +100,3 @@ impl Html { (self.0)(cx) } } - -impl From for Child { - /// Convierte un texto traducible [`Lc`] en un [`Child`], envolviéndolo en un [`Html`] que lo - /// resuelve con el `cx` del ciclo de renderizado. - fn from(label: Lc) -> Self { - Child::with(Html::with(move |cx| label.using(cx))) - } -} - -impl From for ChildOp { - /// Convierte un [`Lc`] en [`ChildOp::Add`]; equivale a `ChildOp::Add(Child::from(label))`. - fn from(label: Lc) -> Self { - ChildOp::Add(Child::from(label)) - } -} diff --git a/src/base/component/table.rs b/src/base/component/table.rs deleted file mode 100644 index 725c26be..00000000 --- a/src/base/component/table.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Definiciones para representar tablas de datos ([`Table`]). - -mod props; -pub use props::SortLink; - -mod component; -pub use component::Table; - -mod column; -pub use column::Column; - -mod row; -pub use row::Row; - -mod cell; -pub use cell::Cell; diff --git a/src/base/component/table/cell.rs b/src/base/component/table/cell.rs deleted file mode 100644 index 0f49a826..00000000 --- a/src/base/component/table/cell.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::prelude::*; - -/// Representa una celda de datos (``) de una [`table::Row`]. -/// -/// El contenido es una lista [`Children`] para admitir cualquier número de componentes. -/// [`Cell::new()`] añade un componente como contenido inicial, normalmente un [`Lc`] con texto -/// literal ([`Lc::n()`]) o traducible (con [`Lc::l()`]/[`Lc::t()`]). -/// -/// Un `&str`, un `String` o un [`Lc`] se convierten directamente en `Cell`; para estos tipos no -/// sería necesario llamar a [`Cell::new()`] de forma explícita. -/// -/// ```rust,no_run -/// use pagetop::prelude::*; -/// -/// let email: table::Cell = "ana@example.com".into(); -/// let label: table::Cell = Lc::l("table-status").into(); -/// let name = table::Cell::new(Html::with(|_| html! { "Julia" })); -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Cell { - /// Devuelve identificador, clases CSS y atributos HTML de la celda (``). - props: Props, - /// Devuelve la lista de componentes hijo que genera el contenido de la celda. - children: Children, -} - -impl Cell { - /// Crea una celda a partir del componente o texto ([`Lc`]) indicado. - /// - /// Para combinar varios componentes en la misma celda basta con encadenar llamadas a - /// [`with_child()`](Cell::with_child): - /// - /// ```rust,no_run - /// # use pagetop::prelude::*; - /// let status = table::Cell::new(Lc::n("Admin ")).with_child(Badge::labeled(Lc::n("active"))); - /// ``` - pub fn new(child: impl Into) -> Self { - Self { - children: Children::with(child.into()), - ..Default::default() - } - } - - // **< Cell BUILDER >*************************************************************************** - - /// Establece el identificador único de la celda. - #[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 de la celda. - #[builder_fn] - pub fn with_prop(mut self, op: PropsOp) -> Self { - self.props.alter_prop(op); - self - } - - /// Establece el número de columnas que ocupa la celda (atributo `colspan`). - /// - /// Con `1` (el valor por defecto de HTML) elimina el atributo en vez de fijarlo. - #[builder_fn] - pub fn with_colspan(mut self, span: u8) -> Self { - self.props.alter_prop(if span == 1 { - PropsOp::remove("colspan") - } else { - PropsOp::set("colspan", span.to_string()) - }); - self - } - - /// Establece el número de filas que ocupa la celda (atributo `rowspan`). - /// - /// Con `1` (el valor por defecto de HTML) elimina el atributo en vez de fijarlo. - #[builder_fn] - pub fn with_rowspan(mut self, span: u8) -> Self { - self.props.alter_prop(if span == 1 { - PropsOp::remove("rowspan") - } else { - PropsOp::set("rowspan", span.to_string()) - }); - self - } - - /// Añade un nuevo componente a la celda o modifica la lista de componentes (`children`) con una - /// operación [`ChildOp`]. - #[builder_fn] - pub fn with_child(mut self, op: impl Into) -> Self { - self.children.alter_child(op.into()); - self - } -} - -impl From<&str> for Cell { - /// Convierte un `&str` en una celda de texto literal; equivale a `Cell::new(Lc::n(text))`. - fn from(text: &str) -> Self { - Cell::new(Lc::n(text.to_string())) - } -} - -impl From for Cell { - /// Convierte un `String` en una celda de texto literal; equivale a `Cell::new(Lc::n(text))`. - fn from(text: String) -> Self { - Cell::new(Lc::n(text)) - } -} - -impl From for Cell { - /// Convierte un [`Lc`] en una celda traducible; equivale a `Cell::new(label)`. - fn from(label: Lc) -> Self { - Cell::new(label) - } -} diff --git a/src/base/component/table/column.rs b/src/base/component/table/column.rs deleted file mode 100644 index cc55ca71..00000000 --- a/src/base/component/table/column.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::prelude::*; - -/// Cabecera de columna (``) de una [`Table`]. -/// -/// El contenido (`label`) es un [`Lc`]. Si la columna debe permitir ordenar la tabla al pulsarla, -/// añade un [`table::SortLink`] con [`with_sort()`](Self::with_sort) para que `Table` envuelva la -/// etiqueta en un enlace y añada el `aria-sort` y las clases CSS correspondientes. -/// -/// Un `&str`, un `String` o un [`Lc`] se convierten directamente en `Column`; para estos tipos no -/// sería necesario llamar a [`Column::new()`] de forma explícita. -/// -/// ```rust -/// use pagetop::locale::Lc; -/// use pagetop::base::component::table::Column; -/// -/// let column: Column = Lc::n("User").into(); -/// assert_eq!(column.label().get(), Some("User".to_string())); -/// assert!(column.sort().is_none()); -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Column { - /// Devuelve identificador, clases CSS y atributos HTML de la celda de cabecera (``). - props: Props, - /// Devuelve el contenido de la cabecera. - label: Lc, - /// Devuelve el enlace de ordenación de la columna, si es ordenable. - sort: Option, -} - -impl Column { - /// Crea una cabecera con el texto localizado indicado. - pub fn new(label: Lc) -> Self { - Self { - label, - ..Default::default() - } - } - - // **< Column BUILDER >************************************************************************* - - /// Establece el identificador único de la celda de cabecera. - #[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 de la columna. - #[builder_fn] - pub fn with_prop(mut self, op: PropsOp) -> Self { - self.props.alter_prop(op); - self - } - - /// Establece el número de columnas que ocupa la cabecera (atributo `colspan`). - /// - /// Con `1` (el valor por defecto de HTML) elimina el atributo en vez de fijarlo. - #[builder_fn] - pub fn with_colspan(mut self, span: u8) -> Self { - self.props.alter_prop(if span == 1 { - PropsOp::remove("colspan") - } else { - PropsOp::set("colspan", span.to_string()) - }); - self - } - - /// Establece el número de filas que ocupa la cabecera (atributo `rowspan`). - /// - /// Con `1` (el valor por defecto de HTML) elimina el atributo en vez de fijarlo. - #[builder_fn] - pub fn with_rowspan(mut self, span: u8) -> Self { - self.props.alter_prop(if span == 1 { - PropsOp::remove("rowspan") - } else { - PropsOp::set("rowspan", span.to_string()) - }); - self - } - - /// Convierte la columna en ordenable con el enlace indicado, o la vuelve no ordenable con - /// `None`. - #[builder_fn] - pub fn with_sort(mut self, sort: impl Into>) -> Self { - self.sort = sort.into(); - self - } - - // Traduce la etiqueta y, si la columna es ordenable, la envuelve en su enlace con `aria-sort` - // y las clases `table-sort*` ya resueltas por `table::SortLink`. Sólo lo usa `Table` al - // renderizar. - pub(super) fn render_header(&self, cx: &Context) -> Markup { - let label = self.label().using(cx); - - let Some(sort) = self.sort() else { - return html! { th (self.props()) scope="col" { (label) } }; - }; - let (aria_sort, link_props) = sort.header_attrs(); - - html! { - th (self.props()) scope="col" aria-sort=(aria_sort) { - a href=[sort.href().as_str()] (link_props) { (label) } - } - } - } -} - -impl From<&str> for Column { - /// Convierte un `&str` en una cabecera de texto literal; equivale a `Column::new(Lc::n(text))`. - fn from(text: &str) -> Self { - Column::new(Lc::n(text.to_string())) - } -} - -impl From for Column { - /// Convierte un `String` en una cabecera de texto literal; equivale a - /// `Column::new(Lc::n(text))`. - fn from(text: String) -> Self { - Column::new(Lc::n(text)) - } -} - -impl From for Column { - /// Convierte un [`Lc`] en una cabecera de texto traducible; equivale a `Column::new(label)`. - fn from(label: Lc) -> Self { - Column::new(label) - } -} diff --git a/src/base/component/table/component.rs b/src/base/component/table/component.rs deleted file mode 100644 index 76857ef4..00000000 --- a/src/base/component/table/component.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::prelude::*; - -/// Componente para representar **tablas de datos**. -/// -/// `Table` resuelve la estructura HTML de cualquier listado tabular (``, ``, -/// `` 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 `
` para admitir scroll horizontal sólo de la tabla si no -/// cabe en el ancho disponible. -/// - `.table`: clase base del elemento `
`. -/// - `.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, - /// Devuelve las filas de datos de la tabla, en orden de aparición. - rows: Vec, - /// Devuelve el mensaje mostrado cuando no hay filas, o `None` si está desactivado. - #[default(Some(Lc::l("table_empty")))] - empty: Option, -} - -#[async_trait] -impl Component for Table { - 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("table")); - } - - async fn prepare(&self, cx: &mut Context) -> Result { - 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) -> 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) -> 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 `
` y, si - /// hay columnas, ``. - /// - /// 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>) -> Self { - self.empty = empty.into(); - self - } -} diff --git a/src/base/component/table/props.rs b/src/base/component/table/props.rs deleted file mode 100644 index c6446e92..00000000 --- a/src/base/component/table/props.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::prelude::*; - -/// Enlace de ordenación para una cabecera de columna ([`Column::with_sort`]). -/// -/// Encapsula la URL de destino (`href`) y, si la tabla está actualmente ordenada por esta columna, -/// la dirección vigente (`dir`). [`Table`] usa esa información para marcar la cabecera con -/// `aria-sort` y aplicar las clases `table-sort`/`table-sort-asc`/`table-sort-desc` al enlace, de -/// modo que el tema activo pueda mostrar el indicador visual con CSS. -/// -/// `SortLink` no añade ningún atributo adicional por sí sola: el enlace ya es funcional por sí -/// mismo (navega a `href` con una petición normal). Se pueden añadir atributos adicionales para -/// opciones de interactividad usando [`with_prop()`]. -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// let link = table::SortLink::new("/admin/users?sort=email") -/// .with_id("sort-email") -/// .with_dir(SortDir::Desc) -/// .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.dir(), Some(&SortDir::Desc)); -/// ``` -/// -/// [`Table`]: super::Table -/// [`Column::with_sort`]: super::Column::with_sort -/// [`with_prop()`]: Self::with_prop -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct SortLink { - /// Devuelve los atributos adicionales del enlace. - props: Props, - /// Devuelve la URL de destino del enlace ya normalizada. - href: AttrValue, - /// Devuelve la dirección de orden vigente si la tabla está ordenada por esta columna, o - /// `None` si la columna es ordenable pero no es la que determina el orden actual. - dir: Option, -} - -impl SortLink { - /// Crea un enlace de ordenación hacia la URL indicada, sin dirección activa. - pub fn new(href: impl Into) -> Self { - Self { - href: AttrValue::new(href.into().to_string()), - ..Default::default() - } - } - - // **< SortLink BUILDER >*********************************************************************** - - /// Establece el identificador único del enlace. - #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); - self - } - - /// Establece la dirección de orden vigente, o `None` si esta columna no es la que ordena - /// actualmente la tabla. - #[builder_fn] - pub fn with_dir(mut self, dir: impl Into>) -> Self { - self.dir = dir.into(); - self - } - - /// Modifica los atributos HTML del enlace. Es el punto de extensión para añadir atributos de - /// interactividad sin que `Table` necesite conocerlos. - #[builder_fn] - pub fn with_prop(mut self, op: PropsOp) -> Self { - self.props.alter_prop(op); - self - } - - // Determina el `aria-sort` y los atributos del enlace ya combinados con las clases - // `table-sort`/`table-sort-asc`/`table-sort-desc` correspondientes al estado de orden vigente. - // Sólo lo usa `Column` para montar la cabecera completa; no forma parte de la API pública. - pub(super) fn header_attrs(&self) -> (&'static str, Props) { - let (aria_sort, sort_class) = match self.dir() { - Some(dir) => match dir { - SortDir::Asc => ("ascending", "table-sort table-sort-asc"), - SortDir::Desc => ("descending", "table-sort table-sort-desc"), - }, - None => ("none", "table-sort"), - }; - let props = self - .props() - .clone() - .with_prop(PropsOp::prepend_classes(sort_class)); - (aria_sort, props) - } -} diff --git a/src/base/component/table/row.rs b/src/base/component/table/row.rs deleted file mode 100644 index b73648fb..00000000 --- a/src/base/component/table/row.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::prelude::*; - -/// Fila de datos (``) de una [`Table`], formada por una lista de celdas ([`table::Cell`]). -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// let row = table::Row::new() -/// .with_cell("Julia") -/// .with_cell("ana@example.com"); -/// assert_eq!(row.cells().len(), 2); -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Row { - /// Devuelve identificador, clases CSS y atributos HTML de la fila (``). - props: Props, - /// Devuelve las celdas de la fila, en orden de aparición. - cells: Vec, -} - -impl Row { - /// Crea una fila vacía. - pub fn new() -> Self { - Self::default() - } - - // **< Row BUILDER >**************************************************************************** - - /// Establece el identificador único de la fila. - #[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 de la fila. - #[builder_fn] - pub fn with_prop(mut self, op: PropsOp) -> Self { - self.props.alter_prop(op); - self - } - - /// Añade una celda al final de la fila. - /// - /// Acepta directamente un `&str`, un `String` o un [`Lc`] (equivalen a `table::Cell::new(...)` - /// con el contenido indicado), o un [`table::Cell`] ya construido (por ejemplo para asignarle - /// clases o atributos propios, o para contener otros componentes). - #[builder_fn] - pub fn with_cell(mut self, cell: impl Into) -> Self { - self.cells.push(cell.into()); - self - } -} diff --git a/src/locale/en-US/base.ftl b/src/locale/en-US/base.ftl index 03e1aa76..d11c8cef 100644 --- a/src/locale/en-US/base.ftl +++ b/src/locale/en-US/base.ftl @@ -31,6 +31,3 @@ pager_next_aria_label = Next page pager_goto_label = Jump to page pager_goto_button = Go pager_summary = Showing { $first }-{ $last } of { $total } - -# Table component. -table_empty = No data to display diff --git a/src/locale/es-ES/base.ftl b/src/locale/es-ES/base.ftl index 4c26244f..fa0d4890 100644 --- a/src/locale/es-ES/base.ftl +++ b/src/locale/es-ES/base.ftl @@ -31,6 +31,3 @@ pager_next_aria_label = Página siguiente pager_goto_label = Saltar a la página pager_goto_button = Ir pager_summary = Mostrando { $first }-{ $last } de { $total } - -# Table component. -table_empty = No hay datos que mostrar diff --git a/tests/component_table.rs b/tests/component_table.rs deleted file mode 100644 index 35083540..00000000 --- a/tests/component_table.rs +++ /dev/null @@ -1,140 +0,0 @@ -use pagetop::prelude::*; - -#[pagetop::test] -async fn table_without_columns_or_rows_renders_empty_shell() { - let mut table = Table::new().with_empty(None); - - let html = table.render(&mut Context::default()).await.into_string(); - - assert_eq!( - html, - r#"
"# - ); -} - -#[pagetop::test] -async fn table_without_rows_uses_default_empty_message() { - let mut table = Table::new(); - - let html = table.render(&mut Context::default()).await.into_string(); - - assert!(html.contains(r#"No data to display"#)); -} - -#[pagetop::test] -async fn table_with_columns_and_no_rows_shows_empty_message() { - let mut table = Table::new() - .with_column(Lc::n("User")) - .with_column(Lc::n("Email")) - .with_empty(Lc::n("No users to show.")); - - let html = table.render(&mut Context::default()).await.into_string(); - - assert!(html.contains("")); - assert!(html.contains("User")); - assert!(html.contains("Email")); - assert!(html.contains(r#"No users to show."#)); -} - -#[pagetop::test] -async fn table_with_columns_and_no_rows_and_no_empty_message_omits_tbody() { - let mut table = Table::new().with_column(Lc::n("User")).with_empty(None); - - let html = table.render(&mut Context::default()).await.into_string(); - - assert!(html.contains("")); - assert!(!html.contains("")); -} - -#[pagetop::test] -async fn table_renders_rows_and_cells_in_order() { - let mut table = Table::new() - .with_column(Lc::n("User")) - .with_column(Lc::n("Email")); - - table.alter_row( - table::Row::new() - .with_cell(table::Cell::new(Html::with(|_| html! { "alice" }))) - .with_cell(table::Cell::new(Html::with( - |_| html! { "alice@example.com" }, - ))), - ); - table.alter_row( - table::Row::new() - .with_cell(table::Cell::new(Html::with(|_| html! { "bob" }))) - .with_cell(table::Cell::new(Html::with( - |_| html! { "bob@example.com" }, - ))), - ); - - let html = table.render(&mut Context::default()).await.into_string(); - - assert!(!html.contains("table-empty")); - let alice = html - .find("alice@example.com") - .expect("Expected alice's row"); - let bob = html.find("bob@example.com").expect("Expected bob's row"); - assert!(alice < bob, "Rows should keep insertion order"); -} - -#[pagetop::test] -async fn sortable_column_without_active_direction_marks_aria_sort_none() { - let mut table = Table::new().with_column( - table::Column::new(Lc::n("User")).with_sort(table::SortLink::new("/users?sort=username")), - ); - - let html = table.render(&mut Context::default()).await.into_string(); - - assert!(html.contains(r#"aria-sort="none""#)); - assert!(html.contains(r#"User"#)); - assert!(!html.contains("table-sort-asc")); - assert!(!html.contains("table-sort-desc")); -} - -#[pagetop::test] -async fn sortable_column_with_active_direction_marks_aria_sort_and_css_class() { - let mut table = Table::new().with_column( - table::Column::new(Lc::n("User")) - .with_sort(table::SortLink::new("/users?sort=username").with_dir(SortDir::Desc)), - ); - - let html = table.render(&mut Context::default()).await.into_string(); - - assert!(html.contains(r#"aria-sort="descending""#)); - assert!(html.contains("table-sort table-sort-desc")); -} - -#[pagetop::test] -async fn sort_link_props_are_ready_for_htmx_without_hardcoding_it() { - let mut table = Table::new().with_column( - table::Column::new(Lc::n("User")).with_sort( - table::SortLink::new("/users?sort=username") - .with_prop(PropsOp::set("hx-get", "/users?sort=username")) - .with_prop(PropsOp::set("hx-target", "#user-table")), - ), - ); - - let html = table.render(&mut Context::default()).await.into_string(); - - assert!(html.contains(r##"hx-get="/users?sort=username""##)); - assert!(html.contains(r##"hx-target="#user-table""##)); -} - -#[pagetop::test] -async fn row_and_cell_props_flow_through_to_attributes() { - let mut table = Table::new().with_column(Lc::n("User")); - - table.alter_row( - table::Row::new() - .with_prop(PropsOp::set("hx-target", "#row-1")) - .with_cell( - table::Cell::new(Html::with(|_| html! { "alice" })) - .with_prop(PropsOp::add_classes("is-admin")), - ), - ); - - let html = table.render(&mut Context::default()).await.into_string(); - - assert!(html.contains(r##""##)); - assert!(html.contains(r##"alice"##)); -}