1use crate::components::renderer_fn::renderer_fn;
2use crate::loaded_rows::{LoadedRows, RowState};
3use crate::selection::Selection;
4use crate::table_row::TableRow;
5use crate::{
6 ChangeEvent, ColumnSort, DefaultErrorRowRenderer, DefaultLoadingRowRenderer,
7 DefaultRowPlaceholderRenderer, DefaultTableBodyRenderer, DefaultTableHeadRenderer,
8 DefaultTableHeadRowRenderer, DefaultTableRowRenderer, DisplayStrategy, EventHandler,
9 ReloadController, RowReader, ScrollContainer, SelectionChangeEvent, SortingMode,
10 TableClassesProvider, TableDataProvider, TableHeadEvent,
11};
12use leptos::html::AnyElement;
13use leptos::leptos_dom::is_browser;
14use leptos::*;
15use leptos_use::{
16 use_debounce_fn, use_element_size_with_options, use_scroll_with_options, UseElementSizeOptions,
17 UseElementSizeReturn, UseScrollOptions, UseScrollReturn,
18};
19use std::cell::RefCell;
20use std::collections::{HashSet, VecDeque};
21use std::fmt::Debug;
22use std::marker::PhantomData;
23use std::ops::Range;
24use std::rc::Rc;
25
26const MAX_DISPLAY_ROW_COUNT: usize = 500;
27
28renderer_fn!(
29 RowRendererFn<Row>(
30 class: Signal<String>,
31 row: Row,
32 index: usize,
33 selected: Signal<bool>,
34 on_select: EventHandler<web_sys::MouseEvent>,
35 on_change: EventHandler<ChangeEvent<Row>>
36 )
37 default DefaultTableRowRenderer
38 where Row: TableRow + Clone + 'static
39);
40
41renderer_fn!(
42 RowPlaceholderRendererFn(height: Signal<f64>)
43 default DefaultRowPlaceholderRenderer
44);
45
46renderer_fn!(
47 WrapperRendererFn(view: View, class: Signal<String>)
48);
49
50renderer_fn!(
51 TbodyRendererFn(view: Fragment, class: Signal<String>, node_ref: NodeRef<AnyElement>)
52);
53
54renderer_fn!(
55 ErrorRowRendererFn(err: String, index: usize, col_count: usize)
56 default DefaultErrorRowRenderer
57);
58
59renderer_fn!(
60 LoadingRowRendererFn(class: Signal<String>, get_cell_class: Callback<usize, String>, get_cell_inner_class: Callback<usize, String>, index: usize, col_count: usize)
61 default DefaultLoadingRowRenderer
62);
63
64#[component]
66pub fn TableContent<Row, DataP, Err, ClsP>(
67 rows: DataP,
70 #[prop(optional, into)]
72 scroll_container: ScrollContainer,
73 #[prop(optional, into)]
76 on_change: EventHandler<ChangeEvent<Row>>,
77 #[prop(optional, into)]
85 selection: Selection,
86 #[prop(optional, into)]
89 on_selection_change: EventHandler<SelectionChangeEvent<Row>>,
90 #[prop(default = DefaultTableHeadRenderer.into(), into)]
93 thead_renderer: WrapperRendererFn,
94 #[prop(default = DefaultTableBodyRenderer.into(), into)]
97 tbody_renderer: TbodyRendererFn,
98 #[prop(default = DefaultTableHeadRowRenderer.into(), into)]
101 thead_row_renderer: WrapperRendererFn,
102 #[prop(optional, into)]
105 row_renderer: RowRendererFn<Row>,
106 #[prop(optional, into)]
110 loading_row_renderer: LoadingRowRendererFn,
111 #[prop(optional, into)]
115 error_row_renderer: ErrorRowRendererFn,
116 #[prop(optional, into)]
120 row_placeholder_renderer: RowPlaceholderRendererFn,
121 #[prop(optional, into)]
123 row_class: MaybeSignal<String>,
124 #[prop(optional, into)]
126 thead_class: MaybeSignal<String>,
127 #[prop(optional, into)]
129 thead_row_class: MaybeSignal<String>,
130 #[prop(optional, into)]
132 tbody_class: MaybeSignal<String>,
133 #[prop(optional, into)]
135 loading_cell_class: MaybeSignal<String>,
136 #[prop(optional, into)]
138 loading_cell_inner_class: MaybeSignal<String>,
139 #[prop(default = create_rw_signal(VecDeque::new()), into)]
143 sorting: RwSignal<VecDeque<(usize, ColumnSort)>>,
144 #[prop(optional)]
148 sorting_mode: SortingMode,
149 #[prop(optional, into)]
155 on_row_count: EventHandler<usize>,
156 #[prop(optional)]
161 reload_controller: ReloadController,
162 #[prop(optional)]
170 display_strategy: DisplayStrategy,
171 #[prop(optional)]
175 loading_row_display_limit: Option<usize>,
176 #[prop(optional)]
178 row_reader: RowReader<Row>,
179
180 #[prop(optional)] _marker: PhantomData<Err>,
181) -> impl IntoView
182where
183 Row: TableRow<ClassesProvider = ClsP> + Clone + 'static,
184 DataP: TableDataProvider<Row, Err> + 'static,
185 Err: Debug,
186 ClsP: TableClassesProvider + Copy + 'static,
187{
188 let on_change = store_value(on_change);
189 let rows = Rc::new(RefCell::new(rows));
190
191 let class_provider = ClsP::new();
192
193 let row_class = Signal::derive(move || row_class.get());
194 let loading_cell_inner_class = Signal::derive(move || loading_cell_inner_class.get());
195 let loading_cell_class = Signal::derive(move || loading_cell_class.get());
196 let thead_class = Signal::derive(move || class_provider.thead(&thead_class.get()));
197 let thead_row_class = Signal::derive(move || class_provider.thead_row(&thead_row_class.get()));
198 let tbody_class = Signal::derive(move || class_provider.tbody(&tbody_class.get()));
199
200 let loaded_rows = create_rw_signal(LoadedRows::<Row>::new());
201
202 let _ = row_reader
203 .get_loaded_rows
204 .replace(Box::new(move |index: usize| {
205 loaded_rows.with(|loaded_rows| loaded_rows[index].clone())
206 }));
207
208 let first_selected_index = create_rw_signal(None::<usize>);
209
210 let (row_count, set_row_count) = create_signal(None::<usize>);
211
212 let set_known_row_count = move |row_count: usize| {
213 set_row_count.set(Some(row_count));
214 loaded_rows.update(|loaded_rows| loaded_rows.resize(row_count));
215 on_row_count.run(row_count);
216 display_strategy.set_row_count(row_count);
217 };
218
219 let load_row_count = {
220 let rows = Rc::clone(&rows);
221 let set_known_row_count = set_known_row_count.clone();
222
223 move || {
224 spawn_local({
225 let rows = Rc::clone(&rows);
226 let set_known_row_count = set_known_row_count.clone();
227
228 async move {
229 let row_count = rows.borrow().row_count().await;
230
231 if sorting.try_with_untracked(|_| {}).is_none() {
233 return;
234 }
235
236 if let Some(row_count) = row_count {
237 set_known_row_count(row_count);
238 }
239
240 sorting.update(|_| {});
242 }
243 })
244 }
245 };
246
247 let (reload_count, set_reload_count) = create_signal(0_usize);
248 let clear = {
249 let load_row_count = load_row_count.clone();
250
251 move |clear_row_count: bool| {
252 selection.clear();
253 first_selected_index.set(None);
254
255 loaded_rows.update(|loaded_rows| {
256 loaded_rows.clear();
257 });
258
259 if clear_row_count {
260 let reload = row_count.get_untracked().is_some();
261 set_row_count.set(None);
262 if reload {
263 load_row_count();
264 }
265 }
266
267 set_reload_count.set(reload_count.get_untracked().overflowing_add(1).0);
268 }
269 };
270
271 let on_head_click = move |event: TableHeadEvent| {
272 sorting.update(move |sorting| sorting_mode.update_sorting_from_event(sorting, event));
273 };
274
275 create_effect({
276 let rows = Rc::clone(&rows);
277 let clear = clear.clone();
278
279 move |_| {
280 let sorting = sorting.get();
281 if let Ok(mut rows) = rows.try_borrow_mut() {
282 rows.set_sorting(&sorting);
283 clear(false);
284 };
285 }
286 });
287
288 create_effect({
289 let rows = Rc::clone(&rows);
290
291 move |_| {
292 reload_controller.track();
294 rows.borrow().track();
295 clear(true);
296 }
297 });
298
299 let selected_indices = match selection {
300 Selection::None => Signal::derive(|| HashSet::new()),
301 Selection::Single(selected_index) => Signal::derive(move || {
302 selected_index
303 .get()
304 .map(|i| HashSet::from([i]))
305 .unwrap_or_default()
306 }),
307 Selection::Multiple(selected_indices) => selected_indices.into(),
308 };
309
310 let UseScrollReturn { y, set_y, .. } = use_scroll_with_options(
311 scroll_container,
312 UseScrollOptions::default().throttle(100.0),
313 );
314
315 let UseElementSizeReturn { height, .. } = use_element_size_with_options(
316 scroll_container,
317 UseElementSizeOptions::default().box_(web_sys::ResizeObserverBoxOptions::ContentBox),
318 );
319
320 if is_browser()
321 && matches!(
322 display_strategy,
323 DisplayStrategy::Virtualization | DisplayStrategy::Pagination { .. }
324 )
325 {
326 load_row_count();
327 }
328
329 let (average_row_height, set_average_row_height) = create_signal(20.0);
330
331 let first_visible_row_index = if let DisplayStrategy::Pagination {
332 controller,
333 row_count,
334 } = display_strategy
335 {
336 create_memo(move |_| controller.current_page.get() * row_count)
337 } else {
338 create_memo(move |_| (y.get() / average_row_height.get()).floor() as usize)
339 };
340 let visible_row_count = match display_strategy {
341 DisplayStrategy::Pagination { row_count, .. } => Signal::derive(move || row_count),
342
343 DisplayStrategy::Virtualization | DisplayStrategy::InfiniteScroll => {
344 create_memo(move |_| {
345 ((height.get() / average_row_height.get()).ceil() as usize).max(20)
346 })
347 .into()
348 }
349 };
350
351 let (display_range, set_display_range) = create_signal(0..0);
352
353 let placeholder_height_before =
354 if matches!(display_strategy, DisplayStrategy::Pagination { .. }) {
355 Signal::derive(move || 0.0)
356 } else {
357 create_memo(move |_| display_range.get().start as f64 * average_row_height.get()).into()
358 };
359
360 let placeholder_height_after = if matches!(display_strategy, DisplayStrategy::Pagination { .. })
361 {
362 Signal::derive(move || 0.0)
363 } else {
364 create_memo(move |_| {
365 let row_count_after = if let Some(row_count) = row_count.get() {
366 (row_count.saturating_sub(display_range.get().end)) as f64
367 } else {
368 0.0
369 };
370
371 row_count_after * average_row_height.get()
372 })
373 .into()
374 };
375
376 let tbody_ref = create_node_ref::<AnyElement>();
377
378 let compute_average_row_height = use_debounce_fn(
379 move || {
380 compute_average_row_height_from_loaded(
381 tbody_ref,
382 display_range,
383 y,
384 &set_y,
385 set_average_row_height,
386 placeholder_height_before,
387 loaded_rows,
388 );
389 },
390 50.0,
391 );
392
393 create_effect(move |_| {
394 let first_visible_row_index = first_visible_row_index.get();
395 let visible_row_count = visible_row_count.get().min(MAX_DISPLAY_ROW_COUNT);
396
397 reload_count.track();
399
400 if visible_row_count == 0 {
401 return;
402 }
403
404 let mut start = first_visible_row_index.saturating_sub(visible_row_count * 2);
405
406 let mut end = start + visible_row_count * 5;
407
408 if let Some(chunk_size) = DataP::CHUNK_SIZE {
409 start /= chunk_size;
410 start *= chunk_size;
411
412 end /= chunk_size;
413 end += 1;
414 end *= chunk_size;
415 }
416
417 if let Some(row_count) = row_count.get() {
418 end = end.min(row_count);
419 }
420
421 if !matches!(display_strategy, DisplayStrategy::Pagination { .. }) {
422 end = end.min(start + MAX_DISPLAY_ROW_COUNT);
423 }
424
425 loaded_rows.update_untracked(|loaded_rows| {
426 if end > loaded_rows.len() {
427 loaded_rows.resize(end);
428 }
429 });
430
431 let range = start..end;
432
433 set_display_range.set(match display_strategy {
434 DisplayStrategy::Virtualization | DisplayStrategy::InfiniteScroll => range.clone(),
435 DisplayStrategy::Pagination { row_count, .. } => {
436 first_visible_row_index..(first_visible_row_index + row_count).min(end)
437 }
438 });
439
440 let missing_range =
441 loaded_rows.with_untracked(|loaded_rows| loaded_rows.missing_range(range.clone()));
442
443 if let Some(missing_range) = missing_range {
444 let mut end = missing_range.end;
445 if let Some(row_count) = row_count.get() {
446 end = end.min(row_count);
447
448 if end <= missing_range.start {
449 return;
450 }
451 }
452
453 loaded_rows.update(|loaded_rows| loaded_rows.write_loading(missing_range.clone()));
454
455 let mut loading_ranges = vec![];
456 if let Some(chunk_size) = DataP::CHUNK_SIZE {
457 let start = missing_range.start / chunk_size * chunk_size;
458 let mut current_range = start..start + chunk_size;
459 while current_range.end <= missing_range.end {
460 loading_ranges.push(current_range.clone());
461 current_range = current_range.end..current_range.end + chunk_size;
462 }
463 if current_range.end > missing_range.end && current_range.start < missing_range.end
465 {
466 loading_ranges.push(current_range);
467 }
468 } else {
469 loading_ranges.push(missing_range);
470 }
471
472 for missing_range in loading_ranges {
474 let compute_average_row_height = compute_average_row_height.clone();
475 spawn_local({
476 let rows = Rc::clone(&rows);
477 let set_known_row_count = set_known_row_count.clone();
478
479 async move {
480 let latest_reload_count = reload_count.get_untracked();
481
482 let result = rows
483 .borrow()
484 .get_rows(missing_range.clone())
485 .await
486 .map_err(|err| format!("{err:?}"));
487
488 if let Some(reload_count) = reload_count.try_get_untracked() {
489 if reload_count != latest_reload_count {
491 return;
492 }
493
494 if let Ok((_, loaded_range)) = &result {
495 if loaded_range.end < missing_range.end {
496 if let Some(row_count) = row_count.get_untracked() {
497 if loaded_range.end < row_count {
498 set_known_row_count(loaded_range.end);
499 }
500 } else {
501 set_known_row_count(loaded_range.end);
502 }
503 }
504 }
505 loaded_rows.update(|loaded_rows| {
506 loaded_rows.write_loaded(result, missing_range)
507 });
508
509 compute_average_row_height();
510 }
511 }
512 });
513 }
514 }
515 });
516
517 let thead_content = Row::render_head_row(sorting.into(), on_head_click).into_view();
518
519 let tbody_content = {
520 let row_renderer = row_renderer.clone();
521 let loading_row_renderer = loading_row_renderer.clone();
522 let error_row_renderer = error_row_renderer.clone();
523 let on_selection_change = on_selection_change.clone();
524
525 view! {
526 {row_placeholder_renderer.run(placeholder_height_before.into())}
527
528 <For
529 each=move || {
530 with!(
531 | loaded_rows, display_range | { let iter = loaded_rows[display_range
532 .clone()].iter().cloned().enumerate().map(| (i, row) | (i + display_range
533 .start, row)); if let Some(loading_row_display_limit) =
534 loading_row_display_limit { let mut loading_row_count = 0; iter.filter(| (_,
535 row) | { if matches!(row, RowState::Loading | RowState::Placeholder) {
536 loading_row_count += 1; loading_row_count <= loading_row_display_limit }
537 else { true } }).collect::< Vec < _ >> () } else { iter.collect::< Vec < _
538 >> () } }
539 )
540 }
541
542 key=|(idx, row)| {
543 match row {
544 RowState::Loaded(_) => idx.to_string(),
545 RowState::Error(_) => format!("error-{idx}"),
546 RowState::Loading | RowState::Placeholder => format!("loading-{idx}"),
547 }
548 }
549
550 children={
551 let row_renderer = row_renderer.clone();
552 let loading_row_renderer = loading_row_renderer.clone();
553 let error_row_renderer = error_row_renderer.clone();
554 let on_selection_change = on_selection_change.clone();
555 move |(i, row)| {
556 match row {
557 RowState::Loaded(row) => {
558 let selected_signal = Signal::derive(move || {
559 selected_indices.get().contains(&i)
560 });
561 let class_signal = Signal::derive(move || {
562 class_provider.row(i, selected_signal.get(), &row_class.get())
563 });
564 let on_select = {
565 let on_selection_change = on_selection_change.clone();
566 let row = row.clone();
567 move |evt: web_sys::MouseEvent| {
568 update_selection(evt, selection, first_selected_index, i);
569 let selection_change_event = SelectionChangeEvent {
570 row: row.clone(),
571 row_index: i,
572 selected: selected_signal.get_untracked(),
573 };
574 on_selection_change.run(selection_change_event);
575 }
576 };
577 row_renderer
578 .run(
579 class_signal,
580 row,
581 i,
582 selected_signal,
583 on_select.into(),
584 on_change.get_value(),
585 )
586 }
587 RowState::Error(err) => {
588 error_row_renderer.run(err, i, Row::COLUMN_COUNT)
589 }
590 RowState::Loading | RowState::Placeholder => {
591 loading_row_renderer
592 .run(
593 Signal::derive(move || {
594 class_provider.row(i, false, &row_class.get())
595 }),
596 Callback::new(move |col_index: usize| {
597 class_provider
598 .loading_cell(i, col_index, &loading_cell_class.get())
599 }),
600 Callback::new(move |col_index: usize| {
601 class_provider
602 .loading_cell_inner(
603 i,
604 col_index,
605 &loading_cell_inner_class.get(),
606 )
607 }),
608 i,
609 Row::COLUMN_COUNT,
610 )
611 }
612 }
613 }
614 }
615 />
616
617 {row_placeholder_renderer.run(placeholder_height_after.into())}
618 }
619 };
620
621 let tbody = tbody_renderer.run(tbody_content, tbody_class, tbody_ref);
622
623 view! {
624 {thead_renderer
625 .run(thead_row_renderer.run(thead_content, thead_row_class).into_view(), thead_class)}
626
627 {tbody}
628 }
629}
630
631fn compute_average_row_height_from_loaded<Row, ClsP>(
632 tbody_ref: NodeRef<AnyElement>,
633 display_range: ReadSignal<Range<usize>>,
634 y: Signal<f64>,
635 set_y: &impl Fn(f64),
636 set_average_row_height: WriteSignal<f64>,
637 placeholder_height_before: Signal<f64>,
638 loaded_rows: RwSignal<LoadedRows<Row>>,
639) where
640 Row: TableRow<ClassesProvider = ClsP> + Clone + 'static,
641{
642 if let Some(el) = tbody_ref.get_untracked() {
643 let el: &web_sys::Element = ⪙
644 let display_range = display_range.get_untracked();
645 if display_range.end > 0 {
646 let avg_row_height = loaded_rows.with_untracked(|loaded_rows| {
647 let mut loading_row_start_index = None;
648 let mut loading_row_end_index = None;
649
650 for i in display_range.clone() {
651 if matches!(loaded_rows[i], RowState::Loaded(_) | RowState::Loading) {
652 if loading_row_start_index.is_none() {
653 loading_row_start_index = Some(i);
654 }
655 loading_row_end_index = Some(i);
656 } else {
657 if loading_row_end_index.is_some() {
658 break;
659 }
660 }
661 }
662
663 if let (Some(loading_row_start_index), Some(loading_row_end_index)) =
664 (loading_row_start_index, loading_row_end_index)
665 {
666 if loading_row_end_index == loading_row_start_index {
667 return None;
668 }
669
670 let children = el.children();
671
672 let first_loading_row = children
674 .get_with_index((loading_row_start_index + 1 - display_range.start) as u32);
675 let last_loading_row = children
676 .get_with_index((loading_row_end_index + 1 - display_range.start) as u32);
677
678 if let (Some(first_loading_row), Some(last_loaded_row)) =
679 (first_loading_row, last_loading_row)
680 {
681 return Some(
682 (last_loaded_row.get_bounding_client_rect().top()
683 - first_loading_row.get_bounding_client_rect().top())
684 / (loading_row_end_index - loading_row_start_index) as f64,
685 );
686 }
687 }
688
689 None
690 });
691
692 if let Some(avg_row_height) = avg_row_height {
693 let prev_placeholder_height_before = placeholder_height_before.get_untracked();
694
695 set_average_row_height.set(avg_row_height);
696
697 let new_placeholder_height_before = placeholder_height_before.get_untracked();
698 set_y(
699 y.get_untracked() - prev_placeholder_height_before
700 + new_placeholder_height_before,
701 );
702 }
703 }
704 }
705}
706
707fn get_keyboard_modifiers(evt: &web_sys::MouseEvent) -> (bool, bool) {
708 let meta_pressed = evt.meta_key() || evt.ctrl_key();
709 let shift_pressed = evt.shift_key();
710 (meta_pressed, shift_pressed)
711}
712
713fn update_selection(
714 evt: web_sys::MouseEvent,
715 selection: Selection,
716 first_selected_index: RwSignal<Option<usize>>,
717 i: usize,
718) {
719 match selection {
720 Selection::None => {}
721 Selection::Single(selected_index) => {
722 if selected_index.get_untracked() == Some(i) {
723 selected_index.set(None);
724 } else {
725 selected_index.set(Some(i));
726 }
727 }
728 Selection::Multiple(selected_indices) => {
729 selected_indices.update(|selected_indices| {
730 let (meta_pressed, shift_pressed) = get_keyboard_modifiers(&evt);
731
732 if meta_pressed {
733 if selected_indices.contains(&i) {
734 selected_indices.remove(&i);
735 } else {
736 selected_indices.insert(i);
737 }
738 match selected_indices.len() {
739 0 => first_selected_index.set(None),
740 1 => {
741 first_selected_index.set(Some(i));
742 }
743 _ => {
744 }
746 }
747 } else if shift_pressed {
748 if let Some(first_selected_index) = first_selected_index.get() {
749 let min = first_selected_index.min(i);
750 let max = first_selected_index.max(i);
751 for i in min..=max {
752 selected_indices.insert(i);
753 }
754 } else {
755 selected_indices.insert(i);
756 first_selected_index.set(Some(i));
757 }
758 } else {
759 selected_indices.clear();
760 selected_indices.insert(i);
761 first_selected_index.set(Some(i));
762 }
763 });
764 }
765 }
766}