dioxus_element_plug/components/
table.rs1use dioxus::prelude::*;
2
3pub const TABLE: &str = "el-table";
5pub const TABLE_BORDERED: &str = "el-table--border";
6pub const TABLE_STRIPED: &str = "el-table--striped";
7pub const TABLE_HOVER: &str = "el-table--enable-row-hover";
8pub const TABLE_HEADER: &str = "el-table__header";
9pub const TABLE_BODY: &str = "el-table__body";
10pub const TABLE_ROW: &str = "el-table__row";
11pub const TABLE_CELL: &str = "el-table__cell";
12
13#[derive(Clone, Copy, PartialEq, Debug)]
15pub enum SortOrder {
16 Ascending,
17 Descending,
18 None,
19}
20
21#[derive(Clone, PartialEq)]
23pub struct TableColumn {
24 pub title: String,
26 pub key: String,
28 pub width: Option<String>,
30 pub sortable: bool,
32 pub fixed: Option<String>,
34}
35
36pub type TableData = Vec<std::collections::HashMap<String, String>>;
38
39#[derive(Props, Clone, PartialEq)]
41pub struct TableProps {
42 pub columns: Vec<TableColumn>,
44 pub data: TableData,
46 #[props(default)]
48 pub height: Option<String>,
49 #[props(default)]
51 pub max_height: Option<String>,
52 #[props(default = true)]
54 pub show_header: bool,
55 #[props(default = false)]
57 pub border: bool,
58 #[props(default = false)]
60 pub stripe: bool,
61 #[props(default = false)]
63 pub highlight_current_row: bool,
64 #[props(default = false)]
66 pub loading: bool,
67 #[props(default = "No Data".to_string())]
69 pub empty_text: String,
70 #[props(default)]
72 pub sort_key: Option<String>,
73 #[props(default = SortOrder::None)]
75 pub sort_order: SortOrder,
76 #[props(default)]
78 pub current_row_index: Option<usize>,
79 #[props(default)]
81 pub on_row_click: Option<EventHandler<usize>>,
82 #[props(default)]
84 pub on_sort_change: Option<EventHandler<(String, SortOrder)>>,
85 #[props(default)]
87 pub class: Option<String>,
88 #[props(default)]
90 pub style: Option<String>,
91}
92
93#[component]
128pub fn Table(props: TableProps) -> Element {
129 let mut class_names = vec!["el-table".to_string()];
130
131 if props.border {
132 class_names.push("el-table--border".to_string());
133 }
134 if props.stripe {
135 class_names.push("el-table--striped".to_string());
136 }
137 if props.highlight_current_row {
138 class_names.push("el-table--highlight-current-row".to_string());
139 }
140 if let Some(ref custom_class) = props.class {
141 class_names.push(custom_class.to_string());
142 }
143
144 let class_string = class_names.join(" ");
145 let style_string = props.style.as_ref().cloned().unwrap_or_default();
146
147 let active_sort_key = props.sort_key.clone().unwrap_or_default();
148 let active_sort_order = props.sort_order.clone();
149
150 let sorted_rows: Vec<(usize, std::collections::HashMap<String, String>)> = {
152 if !active_sort_key.is_empty() && active_sort_order != SortOrder::None {
153 let mut indexed: Vec<(usize, &std::collections::HashMap<String, String>)> =
154 props.data.iter().enumerate().collect();
155 let sk = active_sort_key.clone();
156 indexed.sort_by(|a, b| {
157 let va = a.1.get(&sk).map(|s| s.as_str()).unwrap_or("");
158 let vb = b.1.get(&sk).map(|s| s.as_str()).unwrap_or("");
159 match active_sort_order {
160 SortOrder::Ascending => va.cmp(vb),
161 SortOrder::Descending => vb.cmp(va),
162 SortOrder::None => std::cmp::Ordering::Equal,
163 }
164 });
165 indexed.into_iter().map(|(i, row)| (i, row.clone())).collect()
166 } else {
167 props.data.iter().enumerate().map(|(i, row)| (i, row.clone())).collect()
168 }
169 };
170
171 let header_cols: Vec<(String, String, bool, String, String, String, bool, SortOrder)> = props
173 .columns
174 .iter()
175 .map(|col| {
176 let width_style = col
177 .width
178 .as_ref()
179 .map(|w| format!("width: {};", w))
180 .unwrap_or_default();
181 let is_active = active_sort_key == col.key;
182 let asc_class = if is_active {
183 match active_sort_order {
184 SortOrder::Ascending => "sort-caret ascending is-active",
185 _ => "sort-caret ascending",
186 }
187 } else {
188 "sort-caret ascending"
189 };
190 let desc_class = if is_active {
191 match active_sort_order {
192 SortOrder::Descending => "sort-caret descending is-active",
193 _ => "sort-caret descending",
194 }
195 } else {
196 "sort-caret descending"
197 };
198 let current_order = if is_active { active_sort_order } else { SortOrder::None };
199 (
200 col.title.clone(),
201 width_style,
202 col.sortable,
203 asc_class.to_string(),
204 desc_class.to_string(),
205 col.key.clone(),
206 is_active,
207 current_order,
208 )
209 })
210 .collect();
211
212 let row_render_data: Vec<(usize, String, Vec<(String, String)>)> = {
214 let cur = props.current_row_index;
215 let stripe = props.stripe;
216 let columns_keys: Vec<String> = props.columns.iter().map(|c| c.key.clone()).collect();
217 sorted_rows
218 .iter()
219 .map(|(orig_idx, row)| {
220 let is_current = cur.map_or(false, |r| r == *orig_idx);
221 let base_class = if *orig_idx % 2 == 1 && stripe {
222 "el-table__row el-table__row--striped"
223 } else {
224 "el-table__row"
225 };
226 let row_class = if is_current {
227 format!("{} current-row", base_class)
228 } else {
229 base_class.to_string()
230 };
231 let cells: Vec<(String, String)> = columns_keys
232 .iter()
233 .map(|key| {
234 (
235 "el-table__cell".to_string(),
236 row.get(key).cloned().unwrap_or_default(),
237 )
238 })
239 .collect();
240 (*orig_idx, row_class, cells)
241 })
242 .collect()
243 };
244
245 let has_data = !props.data.is_empty();
246 let col_count = props.columns.len();
247 let empty_text = props.empty_text.clone();
248 let show_header = props.show_header;
249 let on_row_click = props.on_row_click;
250 let on_sort_change = props.on_sort_change;
251 let loading = props.loading;
252
253 rsx! {
254 div {
255 class: "el-table__wrapper",
256 style: "{style_string}",
257
258 table {
259 class: "{class_string}",
260
261 if show_header {
262 thead {
263 class: "el-table__header",
264
265 tr {
266 class: "el-table__row",
267
268 for (title, width_style, sortable, asc_class, desc_class, col_key, is_active, current_order) in header_cols.into_iter() {
269 th {
270 class: "el-table__cell",
271 style: "{width_style}",
272
273 onclick: move |_| {
274 if sortable {
275 let new_order = if is_active {
276 match current_order {
277 SortOrder::None => SortOrder::Ascending,
278 SortOrder::Ascending => SortOrder::Descending,
279 SortOrder::Descending => SortOrder::None,
280 }
281 } else {
282 SortOrder::Ascending
283 };
284 if let Some(handler) = on_sort_change.as_ref() {
285 handler.call((col_key.clone(), new_order));
286 }
287 }
288 },
289
290 div {
291 class: "cell",
292
293 if sortable {
294 span {
295 class: "el-table__column-label",
296 "{title}"
297 }
298 span {
299 class: "caret-wrapper",
300 i { class: "{asc_class}" }
301 i { class: "{desc_class}" }
302 }
303 } else {
304 "{title}"
305 }
306 }
307 }
308 }
309 }
310 }
311 }
312
313 if has_data {
314 tbody {
315 class: "el-table__body",
316
317 for (orig_idx, row_class, cells) in row_render_data.into_iter() {
318 tr {
319 class: "{row_class}",
320
321 onclick: move |_| {
322 if let Some(handler) = on_row_click.as_ref() {
323 handler.call(orig_idx);
324 }
325 },
326
327 for (cell_class, cell_value) in cells.into_iter() {
328 td {
329 class: "{cell_class}",
330 div {
331 class: "cell",
332 "{cell_value}"
333 }
334 }
335 }
336 }
337 }
338 }
339 } else {
340 tbody {
341 class: "el-table__body",
342 tr {
343 class: "el-table__empty-row",
344 td {
345 class: "el-table__cell",
346 colspan: "{col_count}",
347 div {
348 class: "el-table__empty-block",
349 div {
350 class: "el-table__empty-text",
351 "{empty_text}"
352 }
353 }
354 }
355 }
356 }
357 }
358 }
359
360 if loading {
361 div {
362 class: "el-table__loading-mask",
363 div {
364 class: "el-loading-spinner",
365 i { class: "el-icon-loading" }
366 span { "Loading..." }
367 }
368 }
369 }
370 }
371 }
372}
373
374#[derive(Props, Clone, PartialEq)]
376pub struct DataListProps {
377 pub items: Vec<String>,
379 #[props(default = false)]
381 pub loading: bool,
382 #[props(default = true)]
384 pub show_empty: bool,
385 #[props(default = "No data".to_string())]
387 pub empty_text: String,
388 #[props(default = "vertical".to_string())]
390 pub direction: String,
391 #[props(default)]
393 pub class: Option<String>,
394 #[props(default)]
396 pub style: Option<String>,
397 #[props(default)]
399 pub on_item_click: Option<EventHandler<usize>>,
400}
401
402#[component]
404pub fn DataList(props: DataListProps) -> Element {
405 let mut class_names = vec!["el-data-list".to_string()];
406
407 class_names.push(format!("el-data-list--{}", props.direction));
408
409 if props.loading {
410 class_names.push("el-data-list--loading".to_string());
411 }
412 if props.items.is_empty() {
413 class_names.push("el-data-list--empty".to_string());
414 }
415 if let Some(ref custom_class) = props.class {
416 class_names.push(custom_class.to_string());
417 }
418
419 let class_string = class_names.join(" ");
420 let style_string = props.style.as_ref().cloned().unwrap_or_default();
421
422 if props.items.is_empty() && props.show_empty {
423 return rsx! {
424 div {
425 class: "{class_string} el-data-list--empty-state",
426 style: "{style_string}",
427
428 div {
429 class: "el-empty",
430 div {
431 class: "el-empty__image",
432 i { class: "el-icon-document" }
433 }
434 div {
435 class: "el-empty__description",
436 p { "{props.empty_text}" }
437 }
438 }
439 }
440 };
441 }
442
443 rsx! {
444 div {
445 class: "{class_string}",
446 style: "{style_string}",
447
448 for (index, item) in props.items.iter().enumerate() {
449 div {
450 class: "el-data-list__item",
451
452 onclick: move |_| {
453 if let Some(handler) = props.on_item_click {
454 handler.call(index);
455 }
456 },
457
458 "{item}"
459 }
460 }
461
462 if props.loading {
463 div {
464 class: "el-data-list__loading",
465 div {
466 class: "el-loading-spinner",
467 i { class: "el-icon-loading" }
468 span { "Loading..." }
469 }
470 }
471 }
472 }
473 }
474}