table_rs/dioxus/
table.rs

1use dioxus::prelude::*;
2use web_sys::UrlSearchParams;
3use web_sys::wasm_bindgen::JsValue;
4
5use crate::dioxus::body::TableBody;
6use crate::dioxus::controls::PaginationControls;
7use crate::dioxus::header::TableHeader;
8use crate::dioxus::types::SortOrder;
9use crate::dioxus::types::TableProps;
10
11/// A fully featured table component with sorting, pagination, and search functionality in Dioxus.
12///
13/// This component renders an interactive HTML `<table>` with customizable columns, data,
14/// class names, and labels. It supports client-side sorting, search with URL hydration,
15/// and pagination.
16///
17/// # Props
18/// `TableProps` defines the configuration for this component:
19/// - `data`: A `Vec<HashMap<&'static str, String>>` representing row data.
20/// - `columns`: A `Vec<Column>` describing each column's ID, header text, and behavior.
21/// - `page_size`: Number of rows to display per page (default: `10`).
22/// - `loading`: When `true`, displays a loading indicator (default: `false`).
23/// - `paginate`: Enables pagination controls (default: `false`).
24/// - `search`: Enables a search input for client-side filtering (default: `false`).
25/// - `texts`: Customizable text labels for UI strings (default: `TableTexts::default()`).
26/// - `classes`: Customizable CSS class names for each table part (default: `TableClasses::default()`).
27///
28/// # Features
29/// - **Search**: Filters rows client-side using a text input; the query is persisted in the URL via `?search=`.
30/// - **Sorting**: Clickable headers allow sorting columns ascending or descending.
31/// - **Pagination**: Navigate between pages using prev/next buttons, with an indicator showing current page.
32/// - **Custom Classes**: All elements are styled via `TableClasses` for full customization.
33/// - **Text Overrides**: All UI strings (e.g., empty state, loading, buttons) can be customized using `TableTexts`.
34///
35/// # Returns
36/// Returns a `Dioxus` `Element` that renders a complete table with the above features.
37///
38/// # Example
39/// ```rust
40/// use dioxus::prelude::*;
41/// use maplit::hashmap;
42/// use table_rs::dioxus::table::Table;
43/// use table_rs::dioxus::types::Column;
44///
45///
46/// fn App() -> Element {
47///     let data = vec![
48///         hashmap! { "name" => "ferris".to_string(), "email" => "ferris@opensass.org".to_string() },
49///         hashmap! { "name" => "ferros".to_string(), "email" => "ferros@opensass.org".to_string() },
50///     ];
51///
52///     let columns = vec![
53///         Column { id: "name", header: "Name", sortable: true, ..Default::default() },
54///         Column { id: "email", header: "Email", ..Default::default() },
55///     ];
56///
57///     rsx! {
58///         Table {
59///             data: data,
60///             columns: columns,
61///             paginate: true,
62///             search: true,
63///         }
64///     }
65/// }
66/// ```
67///
68/// # See Also
69/// - [MDN `<table>` Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/table)
70#[component]
71pub fn Table(props: TableProps) -> Element {
72    let TableProps {
73        data,
74        columns,
75        page_size,
76        loading,
77        paginate,
78        search,
79        texts,
80        classes,
81    } = props;
82
83    let mut page = use_signal(|| 0_usize);
84    let mut sort_column = use_signal(|| None::<&'static str>);
85    let mut sort_order = use_signal(SortOrder::default);
86    let mut search_query = use_signal(String::new);
87
88    #[cfg(target_family = "wasm")]
89    use_effect(move || {
90        let window = web_sys::window().unwrap();
91        let location = window.location();
92        let search = location.search().unwrap_or_default();
93        let params = UrlSearchParams::new_with_str(&search).unwrap();
94        if let Some(search_val) = params.get("search") {
95            search_query.set(search_val);
96        }
97    });
98
99    #[cfg(target_family = "wasm")]
100    let update_search_param = move |query: &str| {
101        let window = web_sys::window().unwrap();
102        let href = window.location().href().unwrap();
103        let url = web_sys::Url::new(&href).unwrap();
104        let params = url.search_params();
105        params.set("search", query);
106        url.set_search(&params.to_string().as_string().unwrap_or_default());
107
108        window
109            .history()
110            .unwrap()
111            .replace_state_with_url(&JsValue::NULL, "", Some(&url.href()))
112            .unwrap();
113    };
114
115    let filtered_rows = {
116        let mut rows = data.clone();
117        if !search_query().is_empty() {
118            rows.retain(|row| {
119                columns.iter().any(|col| {
120                    row.get(col.id)
121                        .map(|v| v.to_lowercase().contains(&search_query().to_lowercase()))
122                        .unwrap_or(false)
123                })
124            });
125        }
126
127        if let Some(col_id) = sort_column() {
128            if let Some(col) = columns.iter().find(|c| c.id == col_id) {
129                rows.sort_by(|a, b| {
130                    let val = "".to_string();
131                    let a_val = a.get(col.id).unwrap_or(&val);
132                    let b_val = b.get(col.id).unwrap_or(&val);
133                    match sort_order() {
134                        SortOrder::Asc => a_val.cmp(b_val),
135                        SortOrder::Desc => b_val.cmp(a_val),
136                    }
137                });
138            }
139        }
140
141        rows
142    };
143
144    let total_pages = (filtered_rows.len() as f64 / page_size as f64).ceil() as usize;
145    let start = page() * page_size;
146    let end = ((page() + 1) * page_size).min(filtered_rows.len());
147    let page_rows = &filtered_rows[start..end];
148
149    let on_sort_column = move |id: &'static str| {
150        if Some(id) == sort_column() {
151            sort_order.set(match sort_order() {
152                SortOrder::Asc => SortOrder::Desc,
153                SortOrder::Desc => SortOrder::Asc,
154            });
155        } else {
156            sort_column.set(Some(id));
157            sort_order.set(SortOrder::Asc);
158        }
159    };
160
161    let pagination_controls = if paginate {
162        rsx! {
163            PaginationControls {
164                page: page,
165                total_pages: total_pages,
166                classes: classes.clone(),
167                texts: texts.clone(),
168            }
169        }
170    } else {
171        rsx! {}
172    };
173
174    rsx! {
175        div {
176            class: "{classes.container}",
177            if search {
178                input {
179                    class: "{classes.search_input}",
180                    r#type: "text",
181                    value: "{search_query()}",
182                    placeholder: "{texts.search_placeholder}",
183                    oninput: move |e| {
184                        let val = e.value();
185                        search_query.set(val.clone());
186                        page.set(0);
187                        #[cfg(target_family = "wasm")]
188                        update_search_param(&val);
189                    }
190                }
191            }
192            table {
193                class: "{classes.table}",
194                TableHeader {
195                    columns: columns.clone(),
196                    sort_column: sort_column,
197                    sort_order: sort_order,
198                    on_sort_column: on_sort_column,
199                    classes: classes.clone(),
200                }
201                TableBody {
202                    columns: columns.clone(),
203                    rows: page_rows.to_vec(),
204                    loading: loading,
205                    classes: classes.clone(),
206                    texts: texts.clone(),
207                }
208            }
209            {pagination_controls}
210        }
211    }
212}