Skip to main content

dioxus_tw_components/components/
sorttable.rs

1use 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// Used to change the sorting type of the data (eg if a field is number we will not sort the same way as string)
87#[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    /// The default sort column (header name)
257    /// If not set, the first column will be sorted
258    #[props(optional, into)]
259    default_sort: Option<String>,
260
261    /// Provides a handle to the current sorted column index.
262    /// Can be set to 0, will be updated to Self::default_sort if provided and valid
263    #[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    /// Set the default sort column based on its name
324    ///
325    /// If None or the column is not found, the first column will be sorted
326    ///
327    /// Else, the column will be
328    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}