dioxus_tw_components/components/
sorttable.rs1use crate::prelude::*;
2use dioxus::prelude::*;
3
4#[derive(Clone, PartialEq)]
5pub struct SortableRow(Vec<SortableCell>);
6impl SortableRow {
7 pub fn new(cells: Vec<SortableCell>) -> Self {
8 SortableRow(cells)
9 }
10}
11impl std::ops::Deref for SortableRow {
12 type Target = Vec<SortableCell>;
13
14 fn deref(&self) -> &Self::Target {
15 &self.0
16 }
17}
18impl std::ops::DerefMut for SortableRow {
19 fn deref_mut(&mut self) -> &mut Self::Target {
20 &mut self.0
21 }
22}
23impl ToTableData for SortableRow {
24 fn headers_to_strings() -> Vec<impl ToString> {
25 vec![""]
26 }
27
28 fn to_keytype(&self) -> Vec<&KeyType> {
29 self.iter().map(|cell| &cell.sort_by).collect()
30 }
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub struct SortableCell {
35 content: Element,
36 style: String,
37 sort_by: KeyType,
38}
39impl SortableCell {
40 pub fn new(content: Element) -> Self {
41 SortableCell {
42 content,
43 style: String::new(),
44 sort_by: KeyType::None,
45 }
46 }
47
48 pub fn sort_by(mut self, sort_by: KeyType) -> Self {
49 self.sort_by = sort_by;
50 self
51 }
52
53 pub fn style(mut self, style: impl ToString) -> Self {
54 self.style = style.to_string();
55 self
56 }
57}
58
59pub trait Sortable: ToString + Clonable {
60 fn to_sortable(&self) -> KeyType {
61 KeyType::String(self.to_string())
62 }
63}
64
65impl Clone for Box<dyn Sortable> {
66 fn clone(&self) -> Self {
67 self.clone_box()
68 }
69}
70
71pub trait Clonable {
72 fn clone_box(&self) -> Box<dyn Sortable>;
73}
74
75impl<T: Clone + Sortable + 'static> Clonable for T {
76 fn clone_box(&self) -> Box<dyn Sortable> {
77 Box::new(self.clone())
78 }
79}
80
81pub trait ToTableData {
82 fn headers_to_strings() -> Vec<impl ToString>;
83 fn to_keytype(&self) -> Vec<&KeyType>;
84}
85
86#[derive(Clone)]
88pub enum KeyType {
89 None,
90 Element(Element),
91 String(String),
92 Integer(i128),
93 UnsignedInteger(u128),
94 Object(Box<dyn Sortable>),
95}
96
97impl PartialEq for KeyType {
98 fn eq(&self, other: &Self) -> bool {
99 match (self, other) {
100 (KeyType::None, KeyType::None) => true,
101 (KeyType::String(a), KeyType::String(b)) => a == b,
102 (KeyType::Integer(a), KeyType::Integer(b)) => a == b,
103 (KeyType::UnsignedInteger(a), KeyType::UnsignedInteger(b)) => a == b,
104 (KeyType::Object(a), KeyType::Object(b)) => a.to_sortable() == b.to_sortable(),
105 _ => false,
106 }
107 }
108}
109
110impl Eq for KeyType {}
111
112impl PartialOrd for KeyType {
113 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
114 Some(self.cmp(other))
115 }
116}
117
118impl Ord for KeyType {
119 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
120 match (self, other) {
121 (KeyType::String(a), KeyType::String(b)) => a.cmp(b),
122 (KeyType::Integer(a), KeyType::Integer(b)) => b.cmp(a),
123 (KeyType::UnsignedInteger(a), KeyType::UnsignedInteger(b)) => b.cmp(a),
124 (KeyType::Object(a), KeyType::Object(b)) => a.to_sortable().cmp(&b.to_sortable()),
125 _ => std::cmp::Ordering::Equal,
126 }
127 }
128}
129
130impl std::fmt::Display for KeyType {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 match self {
133 KeyType::None => {
134 write!(f, "None")
135 }
136 KeyType::String(str) => {
137 write!(f, "{str}")
138 }
139 KeyType::Integer(nb) => {
140 write!(f, "{nb}")
141 }
142 KeyType::UnsignedInteger(nb) => {
143 write!(f, "{nb}")
144 }
145 KeyType::Object(obj) => {
146 write!(f, "{}", obj.to_string())
147 }
148 _ => write!(f, ""),
149 }
150 }
151}
152
153impl std::fmt::Debug for KeyType {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 write!(
156 f,
157 "{}",
158 match self {
159 Self::None => "None",
160 Self::Element(_) => "Element",
161 Self::String(_) => "String",
162 Self::Integer(_) => "Integer",
163 Self::UnsignedInteger(_) => "UnsignedInteger",
164 _ => "Object(_)",
165 },
166 )
167 }
168}
169
170impl From<&str> for KeyType {
171 fn from(str: &str) -> Self {
172 KeyType::String(str.to_string())
173 }
174}
175
176impl From<String> for KeyType {
177 fn from(str: String) -> Self {
178 KeyType::String(str)
179 }
180}
181
182impl From<i128> for KeyType {
183 fn from(nb: i128) -> Self {
184 KeyType::Integer(nb)
185 }
186}
187
188impl From<u128> for KeyType {
189 fn from(nb: u128) -> Self {
190 KeyType::UnsignedInteger(nb)
191 }
192}
193
194impl From<i64> for KeyType {
195 fn from(nb: i64) -> Self {
196 KeyType::Integer(nb.into())
197 }
198}
199
200impl From<u64> for KeyType {
201 fn from(nb: u64) -> Self {
202 KeyType::UnsignedInteger(nb.into())
203 }
204}
205
206impl From<i32> for KeyType {
207 fn from(nb: i32) -> Self {
208 KeyType::Integer(nb.into())
209 }
210}
211
212impl From<u32> for KeyType {
213 fn from(nb: u32) -> Self {
214 KeyType::UnsignedInteger(nb.into())
215 }
216}
217
218impl From<i16> for KeyType {
219 fn from(nb: i16) -> Self {
220 KeyType::Integer(nb.into())
221 }
222}
223
224impl From<u16> for KeyType {
225 fn from(nb: u16) -> Self {
226 KeyType::UnsignedInteger(nb.into())
227 }
228}
229
230impl From<i8> for KeyType {
231 fn from(nb: i8) -> Self {
232 KeyType::Integer(nb.into())
233 }
234}
235
236impl From<u8> for KeyType {
237 fn from(nb: u8) -> Self {
238 KeyType::UnsignedInteger(nb.into())
239 }
240}
241
242#[derive(Clone, PartialEq, Props)]
243pub struct SortTableProps {
244 #[props(extends = GlobalAttributes)]
245 attributes: Vec<Attribute>,
246
247 #[props(optional, into)]
248 header_class: Option<String>,
249
250 #[props(optional, into)]
251 row_class: Option<String>,
252
253 #[props(optional, into)]
254 cell_class: Option<String>,
255
256 #[props(optional, into)]
259 default_sort: Option<String>,
260
261 #[props(default = use_signal(|| 0), into)]
264 sorted_col_index: Signal<usize>,
265
266 headers: Vec<String>,
267
268 data: ReadSignal<Vec<SortableRow>>,
269}
270
271pub struct SortTableState {
272 headers: Vec<String>,
273 data: Vec<SortableRow>,
274 sorted_col_index: Signal<usize>,
275 sort_ascending: bool,
276}
277
278impl SortTableState {
279 pub fn new(
280 headers: Vec<String>,
281 data: Vec<SortableRow>,
282 current_sort_index: Signal<usize>,
283 ) -> Self {
284 SortTableState {
285 headers,
286 data,
287 sort_ascending: true,
288 sorted_col_index: current_sort_index,
289 }
290 }
291
292 pub fn set_sorted_col_index(&mut self, sorted_col_index: usize) {
293 self.sorted_col_index.set(sorted_col_index);
294 }
295
296 pub fn get_sorted_col_index(&self) -> usize {
297 *self.sorted_col_index.read()
298 }
299
300 pub fn reverse_data(&mut self) {
301 self.data.reverse();
302 }
303
304 pub fn toggle_sort_direction(&mut self) {
305 self.sort_ascending = !self.sort_ascending;
306 }
307
308 pub fn set_sort_direction(&mut self, ascending: bool) {
309 self.sort_ascending = ascending;
310 }
311
312 pub fn is_sort_ascending(&self) -> bool {
313 self.sort_ascending
314 }
315
316 fn is_column_sortable(&self, column_index: usize) -> bool {
317 self.data
318 .first()
319 .and_then(|row| row.get(column_index))
320 .is_some_and(|cell| cell.sort_by != KeyType::None)
321 }
322
323 pub fn set_default_sort(mut self, column_name: Option<String>) -> Self {
329 let column_index = column_name
330 .and_then(|col| self.headers.iter().position(|h| h == &col))
331 .filter(|&idx| self.is_column_sortable(idx))
332 .unwrap_or(0);
333
334 self.sorted_col_index.set(column_index);
335
336 if self.is_column_sortable(column_index) {
337 sort_table_keytype(&mut self.data, |t: &SortableRow| {
338 t.to_keytype()[column_index].clone()
339 });
340 }
341
342 self
343 }
344}
345
346fn sort_table_keytype<F>(data: &mut [SortableRow], key_extractor: F)
347where
348 F: Fn(&SortableRow) -> KeyType,
349{
350 data.sort_by_key(key_extractor);
351}
352
353#[component]
354pub fn SortTable(mut props: SortTableProps) -> Element {
355 let default_classes = "sorttable";
356 crate::setup_class_attribute(&mut props.attributes, default_classes);
357
358 let mut state = use_signal(|| {
359 SortTableState::new(
360 props.headers.clone(),
361 props.data.read().clone(),
362 props.sorted_col_index,
363 )
364 .set_default_sort(props.default_sort.clone())
365 });
366 use_effect(move || {
367 state.set(
368 SortTableState::new(
369 props.headers.clone(),
370 props.data.read().clone(),
371 props.sorted_col_index,
372 )
373 .set_default_sort(props.default_sort.clone()),
374 );
375 });
376
377 let header_class = format!(
378 "sorttable-header {}",
379 props.header_class.unwrap_or("".to_string())
380 );
381
382 rsx! {
383 table {..props.attributes,
384 TableHeader {
385 TableRow {
386 for (index , head) in state.read().headers.iter().enumerate() {
387 th {
388 class: "table-head {header_class}",
389 onclick: move |_| {
390 if !state.peek().is_column_sortable(index) {
391 return;
392 }
393 let sorted_col_index = state.read().get_sorted_col_index();
394 if sorted_col_index == index {
395 state.write().reverse_data();
396 state.write().toggle_sort_direction();
397 } else {
398 sort_table_keytype(
399 &mut state.write().data,
400 |t: &SortableRow| t.to_keytype()[index].clone(),
401 );
402 state.write().set_sort_direction(true);
403 }
404 state.write().set_sorted_col_index(index);
405 },
406 div { class: "sorttable-header-content",
407 p { {head.to_string()} }
408 if state.read().is_column_sortable(index)
409 && state.read().get_sorted_col_index() == index
410 {
411 Icon {
412 class: "sorttable-icon",
413 style: if state.read().is_sort_ascending() { "rotate: -180deg;" },
414 icon: Icons::ExpandMore,
415 }
416 }
417 }
418 }
419 }
420 }
421 }
422 TableBody {
423 for data in state.read().data.iter() {
424 TableRow { class: r#"{props.row_class.clone().unwrap_or("".to_string())}"#,
425 for field in data.iter() {
426 TableCell { class: format!("{} {}", props.cell_class.clone().unwrap_or("".to_string()), field.style),
427 {field.content.clone()}
428 }
429 }
430 }
431 }
432 }
433 }
434 }
435}