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(¶ms.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}