leptos_pagination/hooks/
pagination.rs

1use std::fmt::Debug;
2
3use default_struct_builder::DefaultBuilder;
4use leptos::prelude::*;
5use leptos_windowing::{
6    InternalLoader, ItemWindow,
7    hook::{UseLoadOnDemandResult, use_load_on_demand},
8};
9use reactive_stores::Store;
10
11use crate::{PaginationState, PaginationStateStoreFields};
12
13/// Hook for the pagination logic.
14///
15/// This handles loading items on-demand from the data source and caching them.
16///
17/// It returns an [`ItemWindow`] that is in effect a signal of the items to display.
18///
19/// ## Usage
20///
21/// ```
22/// # use std::ops::Range;
23/// #
24/// # use leptos_pagination::{use_pagination, use_pagination_controls, UsePaginationOptions, UsePaginationControlsOptions, PaginationState, MemoryLoader};
25/// #
26/// let state = PaginationState::new_store();
27///
28/// pub struct ExampleItem {
29///     num: usize,
30/// }
31///
32/// // Implement Loader for example data
33/// pub struct ExampleLoader;
34///
35/// impl MemoryLoader for ExampleLoader {
36///     type Item = ExampleItem;
37///     type Query = ();
38///
39///     fn load_items(&self, range: Range<usize>, query: &Self::Query) -> Vec<Self::Item> {
40///         // Generate example data
41///         range.map(|i| ExampleItem { num: i }).collect()
42///     }
43///
44///     fn item_count(&self, query: &Self::Query) -> usize {
45///         // Let's say we have 1000 items in total
46///         1000
47///     }
48/// }
49///
50/// // See PaginatedFor for how to build a pagination component with the returned window from this hook.
51/// let window = use_pagination(
52///     state,
53///     ExampleLoader,
54///     (),
55///     20, // items per page
56///     UsePaginationOptions::default(),
57/// );
58///
59/// // Use this to control the pagination
60/// let pagination_controls = use_pagination_controls(state, UsePaginationControlsOptions::default());
61/// ```
62///
63/// ## Paramters
64///
65/// - `state`: The pagination state. Used to communicate between the pagination controls and this component.
66/// - `loader`: The loader used to load items from the data source.
67/// - `item_count_per_page`: The number of items to display per page.
68/// - `options`: Additional options for the pagination logic.
69#[must_use]
70pub fn use_pagination<T, L, Q, M>(
71    state: Store<PaginationState>,
72    loader: L,
73    query: impl Into<Signal<Q>>,
74    item_count_per_page: impl Into<Signal<usize>>,
75    options: UsePaginationOptions,
76) -> ItemWindow<T>
77where
78    T: Send + Sync + 'static,
79    L: InternalLoader<M, Item = T, Query = Q> + 'static,
80    L::Error: Send + Sync,
81    Q: Send + Sync + 'static,
82{
83    let UsePaginationOptions {
84        overscan_page_count,
85    } = options;
86
87    let item_count_per_page = item_count_per_page.into();
88
89    let item_count = RwSignal::new(None::<usize>);
90
91    Effect::new(move || {
92        if let Some(item_count) = item_count.get() {
93            state
94                .page_count()
95                .set(Some(item_count.div_ceil(item_count_per_page.get())));
96        }
97    });
98
99    let start_index_to_load = Signal::derive(move || {
100        let current_page = state.current_page().get();
101        current_page.saturating_sub(overscan_page_count) * item_count_per_page.get()
102    });
103
104    let end_index_to_load = Signal::derive(move || {
105        let current_page = state.current_page().get();
106        (current_page + overscan_page_count) * item_count_per_page.get()
107    });
108
109    let range_to_load = Memo::new(move |_| {
110        let start_index = start_index_to_load.get();
111        let end_index = end_index_to_load.get();
112
113        start_index..end_index
114    });
115
116    let range_to_display = Memo::new(move |_| {
117        let item_count_per_page = item_count_per_page.get();
118        let start_index = state.current_page().get() * item_count_per_page;
119        let end_index = start_index + item_count_per_page;
120
121        start_index..end_index
122    });
123
124    let UseLoadOnDemandResult {
125        item_count_result,
126        item_window,
127    } = use_load_on_demand(range_to_load, range_to_display, loader, query);
128
129    Effect::new(move || {
130        match &*item_count_result.read() {
131            Ok(None) => {
132                *state.page_count_error().write() =
133                    Some("Data source didn't provide an item/page count".to_string())
134            }
135            Ok(Some(count)) => {
136                // This sets the page_count. See effect above.
137                item_count.set(Some(*count));
138                *state.page_count_error().write() = None;
139            }
140            Err(err) => {
141                *state.page_count_error().write() =
142                    Some(format!("Error fetching item/page count: {err:?}"))
143            }
144        }
145    });
146
147    item_window
148}
149
150#[derive(Debug, Clone, DefaultBuilder)]
151pub struct UsePaginationOptions {
152    /// How many pages to load before and after the current page.
153    ///
154    /// A value of 1 means that the current page as well as the one before and after will be loaded.
155    /// Defaults to 1.
156    overscan_page_count: usize,
157}
158
159impl Default for UsePaginationOptions {
160    fn default() -> Self {
161        Self {
162            overscan_page_count: 1,
163        }
164    }
165}