Skip to main content

dioxus_element_plug/components/
transfer.rs

1use dioxus::prelude::*;
2
3/// Transfer data item
4#[derive(Clone, PartialEq)]
5pub struct TransferItem {
6    pub key: String,
7    pub label: String,
8    pub disabled: bool,
9}
10
11impl TransferItem {
12    pub fn new(key: &str, label: &str) -> Self {
13        Self {
14            key: key.to_string(),
15            label: label.to_string(),
16            disabled: false,
17        }
18    }
19
20    pub fn disabled(mut self, disabled: bool) -> Self {
21        self.disabled = disabled;
22        self
23    }
24}
25
26/// Transfer props
27#[derive(Props, Clone, PartialEq)]
28pub struct TransferProps {
29    /// All available data items
30    #[props(default)]
31    pub data: Vec<TransferItem>,
32
33    /// Keys currently in the right panel
34    #[props(default)]
35    pub model_value: Vec<String>,
36
37    /// Whether the transfer is filterable
38    #[props(default = false)]
39    pub filterable: bool,
40
41    /// Panel titles [left_title, right_title]
42    #[props(default)]
43    pub titles: Vec<String>,
44
45    /// Filter placeholder text
46    #[props(default = "Enter keyword".to_string())]
47    pub filter_placeholder: String,
48
49    /// Empty state text
50    #[props(default = "No Data".to_string())]
51    pub empty_text: String,
52
53    /// Change handler - receives the right panel keys
54    #[props(default)]
55    pub on_change: Option<EventHandler<Vec<String>>>,
56
57    /// Left panel selection change handler
58    #[props(default)]
59    pub on_left_check: Option<EventHandler<Vec<String>>>,
60
61    /// Right panel selection change handler
62    #[props(default)]
63    pub on_right_check: Option<EventHandler<Vec<String>>>,
64
65    #[props(default)]
66    pub class: Option<String>,
67
68    #[props(default)]
69    pub style: Option<String>,
70}
71
72/// Transfer component for moving items between two panels
73///
74/// This component provides a dual-panel transfer list where users can move
75/// items between left (available) and right (selected) panels.
76///
77/// ## Example
78///
79/// ```rust,ignore
80/// use dioxus_element_plug::components::transfer::{Transfer, TransferItem};
81///
82/// let data = vec![
83///     TransferItem::new("1", "Option 1"),
84///     TransferItem::new("2", "Option 2"),
85///     TransferItem::new("3", "Option 3"),
86/// ];
87///
88/// rsx! {
89///     Transfer {
90///         data: data,
91///         model_value: vec!["1".to_string()],
92///         titles: vec!["Available".to_string(), "Selected".to_string()],
93///     }
94/// }
95/// ```
96#[component]
97pub fn Transfer(props: TransferProps) -> Element {
98    let mut class_names = vec!["el-transfer".to_string()];
99    if let Some(ref c) = props.class {
100        class_names.push(c.clone());
101    }
102
103    // Split data into left and right panels
104    let right_keys: std::collections::HashSet<&String> = props.model_value.iter().collect();
105
106    let left_items: Vec<(String, String, bool)> = props
107        .data
108        .iter()
109        .filter(|item| !right_keys.contains(&item.key))
110        .map(|item| (item.key.clone(), item.label.clone(), item.disabled))
111        .collect();
112
113    let right_items: Vec<(String, String, bool)> = props
114        .data
115        .iter()
116        .filter(|item| right_keys.contains(&item.key))
117        .map(|item| (item.key.clone(), item.label.clone(), item.disabled))
118        .collect();
119
120    let left_title = props.titles.first().cloned().unwrap_or_else(|| "List 1".to_string());
121    let right_title = props.titles.get(1).cloned().unwrap_or_else(|| "List 2".to_string());
122
123    let left_count = left_items.len();
124    let right_count = right_items.len();
125    let left_empty = left_items.is_empty();
126    let right_empty = right_items.is_empty();
127    let empty_text = props.empty_text.clone();
128    let filterable = props.filterable;
129    let filter_placeholder = props.filter_placeholder.clone();
130
131    let on_change = props.on_change;
132
133    rsx! {
134        div {
135            class: "{class_names.join(\" \")}",
136            style: props.style.clone().unwrap_or_default(),
137
138            // Left panel
139            div {
140                class: "el-transfer-panel",
141
142                p {
143                    class: "el-transfer-panel__header",
144                    span {
145                        class: "el-checkbox__label",
146                        "{left_title}"
147                    }
148                    span {
149                        class: "el-transfer-panel__header-count",
150                        "{left_count} items"
151                    }
152                }
153
154                if filterable {
155                    div {
156                        class: "el-transfer-panel__filter",
157                        input {
158                            class: "el-input__inner",
159                            r#type: "text",
160                            placeholder: "{filter_placeholder}",
161                        }
162                    }
163                }
164
165                div {
166                    class: "el-transfer-panel__body",
167
168                    if left_empty {
169                        p {
170                            class: "el-transfer-panel__empty",
171                            "{empty_text}"
172                        }
173                    } else {
174                        for (key, label, disabled) in left_items.into_iter() {
175                            label {
176                                class: "el-transfer-panel__item",
177                                input {
178                                    r#type: "checkbox",
179                                    disabled: disabled,
180                                    value: "{key}",
181                                }
182                                span {
183                                    class: "el-checkbox__label",
184                                    "{label}"
185                                }
186                            }
187                        }
188                    }
189                }
190            }
191
192            // Transfer buttons
193            div {
194                class: "el-transfer__buttons",
195
196                button {
197                    class: "el-button el-transfer__button el-button--primary",
198                    r#type: "button",
199                    onclick: move |_| {
200                        // Move selected items from left to right
201                        // For simplicity, move all non-disabled items
202                        let new_right: Vec<String> = props.data.iter()
203                            .filter(|item| !item.disabled)
204                            .map(|item| item.key.clone())
205                            .collect();
206                        if let Some(handler) = on_change.as_ref() {
207                            handler.call(new_right);
208                        }
209                    },
210                    disabled: left_count == 0,
211                    i { class: "el-icon-arrow-right" }
212                }
213
214                button {
215                    class: "el-button el-transfer__button el-button--primary",
216                    r#type: "button",
217                    onclick: move |_| {
218                        // Move all items back to left (clear right panel)
219                        if let Some(handler) = on_change.as_ref() {
220                            handler.call(vec![]);
221                        }
222                    },
223                    disabled: right_count == 0,
224                    i { class: "el-icon-arrow-left" }
225                }
226            }
227
228            // Right panel
229            div {
230                class: "el-transfer-panel",
231
232                p {
233                    class: "el-transfer-panel__header",
234                    span {
235                        class: "el-checkbox__label",
236                        "{right_title}"
237                    }
238                    span {
239                        class: "el-transfer-panel__header-count",
240                        "{right_count} items"
241                    }
242                }
243
244                if filterable {
245                    div {
246                        class: "el-transfer-panel__filter",
247                        input {
248                            class: "el-input__inner",
249                            r#type: "text",
250                            placeholder: "{filter_placeholder}",
251                        }
252                    }
253                }
254
255                div {
256                    class: "el-transfer-panel__body",
257
258                    if right_empty {
259                        p {
260                            class: "el-transfer-panel__empty",
261                            "{empty_text}"
262                        }
263                    } else {
264                        for (key, label, disabled) in right_items.into_iter() {
265                            label {
266                                class: "el-transfer-panel__item",
267                                input {
268                                    r#type: "checkbox",
269                                    disabled: disabled,
270                                    value: "{key}",
271                                    checked: true,
272                                }
273                                span {
274                                    class: "el-checkbox__label",
275                                    "{label}"
276                                }
277                            }
278                        }
279                    }
280                }
281            }
282        }
283    }
284}