dioxus_tw_components/components/molecules/sorttable/
props.rs

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