patternfly_yew/hooks/
pagination.rs

1//! Hooks for implementing pagination
2
3use crate::prelude::Navigation;
4use std::ops::{Deref, DerefMut, Range};
5use std::rc::Rc;
6use yew::prelude::*;
7
8pub const DEFAULT_PER_PAGE: usize = 10;
9
10/// The current control (input settings) of the pagination
11#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12pub struct PaginationControl {
13    pub page: usize,
14    pub per_page: usize,
15}
16
17/// The current state of the pagination
18#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub struct PaginationState {
20    pub control: PaginationControl,
21    pub total: Option<usize>,
22}
23
24impl PaginationState {
25    fn change_page(mut self, page: usize) -> Self {
26        self.control.page = page;
27        self
28    }
29
30    fn change_per_page(mut self, per_page: usize) -> Self {
31        // remember the current offset
32        let current_offset = self.control.page * self.control.per_page;
33
34        self.control.per_page = per_page.max(1);
35
36        // point to the page with the same offset as before
37        self.control.page = current_offset / self.control.per_page;
38
39        self
40    }
41
42    fn change_total(mut self, total: Option<usize>) -> Self {
43        // set the new total
44        self.total = total;
45
46        // and check if we need to cap the current page
47        if let Some(total_pages) = self.total_pages() {
48            if total_pages > 0 {
49                self.control.page = self.control.page.min(total_pages - 1);
50            } else {
51                self.control.page = 0;
52            }
53        }
54
55        self
56    }
57
58    pub fn navigate(self, navigation: Navigation) -> Self {
59        let mut newpage = self.control.page;
60        match navigation {
61            Navigation::First => newpage = 0,
62            Navigation::Last => {
63                if let Some(total_pages) = self.total_pages() {
64                    newpage = total_pages.saturating_sub(1);
65                }
66            }
67            Navigation::Next => {
68                newpage = newpage.saturating_add(1);
69                if let Some(total_pages) = self.total_pages() {
70                    newpage = newpage.min(total_pages.max(1) - 1);
71                }
72            }
73            Navigation::Previous => {
74                newpage = newpage.saturating_sub(1);
75            }
76            Navigation::Page(page) => {
77                if let Some(total_pages) = self.total_pages() {
78                    if page < total_pages {
79                        newpage = page;
80                    }
81                } else {
82                    newpage = page;
83                }
84            }
85        };
86
87        self.change_page(newpage)
88    }
89
90    pub fn offset(&self) -> usize {
91        self.control.per_page * self.control.page
92    }
93
94    pub fn range(&self) -> Range<usize> {
95        let start = self.offset();
96        let mut end = start + self.control.per_page;
97        if let Some(total) = self.total {
98            end = end.min(total);
99        }
100
101        Range { start, end }
102    }
103
104    pub fn total_pages(&self) -> Option<usize> {
105        self.total
106            .map(|total| (total + self.control.per_page - 1) / self.control.per_page)
107    }
108}
109
110impl Default for PaginationControl {
111    fn default() -> Self {
112        Self {
113            page: 0,
114            per_page: DEFAULT_PER_PAGE,
115        }
116    }
117}
118
119#[derive(Debug, PartialEq, Clone)]
120pub struct UsePagination {
121    pub state: UseStateHandle<PaginationState>,
122    pub onnavigation: Callback<Navigation>,
123    pub onperpagechange: Callback<usize>,
124}
125
126impl Deref for UsePagination {
127    type Target = PaginationState;
128
129    fn deref(&self) -> &Self::Target {
130        &self.state
131    }
132}
133
134impl Deref for PaginationState {
135    type Target = PaginationControl;
136
137    fn deref(&self) -> &Self::Target {
138        &self.control
139    }
140}
141
142impl DerefMut for PaginationState {
143    fn deref_mut(&mut self) -> &mut Self::Target {
144        &mut self.control
145    }
146}
147
148/// Create a hook for managing pagination state.
149///
150/// If known, the hook takes in a total number of items to be shown, otherwise it will be an
151/// unbounded pagination control. The state will be initialized using the initializer function.
152///
153/// The hook returns a struct to manage and track pagination state. It is intended to be used
154/// in combination with the [`crate::components::pagination::SimplePagination`] component.
155///
156/// ## Example
157///
158/// Also see the quickstart project for a full example.
159///
160/// ```rust
161/// use yew::prelude::*;
162/// use patternfly_yew::prelude::*;
163///
164/// #[function_component(Example)]
165/// fn example() -> Html {
166///   let total = use_state_eq(||Some(123));
167///   let pagination = use_pagination(*total, Default::default);
168///
169///   html!(
170///     <>
171///       <SimplePagination
172///         pagination={pagination.clone()}
173///         total={*total}
174///       />
175///       // ... render content
176///       { format!("Showing items: {:?}", pagination.state.range()) }
177///       <SimplePagination
178///         pagination={pagination.clone()}
179///         total={*total}
180///         position={PaginationPosition::Bottom}
181///       />
182///     </>
183///   )
184/// }
185/// ```
186#[hook]
187pub fn use_pagination<T>(total: Option<usize>, init: T) -> UsePagination
188where
189    T: FnOnce() -> PaginationControl,
190{
191    let state = use_state_eq(|| PaginationState {
192        control: init(),
193        total,
194    });
195
196    use_effect_with((total, state.clone()), move |(total, state)| {
197        state.set((**state).change_total(*total));
198    });
199
200    let onnavigation = use_callback(state.clone(), |nav: Navigation, state| {
201        state.set((**state).navigate(nav))
202    });
203
204    let onperpagechange = use_callback(state.clone(), |per_page, state| {
205        state.set((**state).change_per_page(per_page))
206    });
207
208    UsePagination {
209        state,
210        onnavigation,
211        onperpagechange,
212    }
213}
214
215/// Apply pagination state to a set of data.
216///
217/// Ideally, pagination is applied on the source of the data. Like a database query. However,
218/// sometimes it can be convenient to even paginate a fully loaded dataset.
219///
220/// This hook takes a full dataset and returns the currently selected page. It will update
221/// whenever the entries or pagination control state changes.
222#[hook]
223fn use_apply_pagination<T>(entries: Rc<Vec<T>>, control: PaginationControl) -> Rc<Vec<T>>
224where
225    T: Clone + PartialEq + 'static,
226{
227    use_memo((entries, control), |(entries, control)| {
228        let offset = control.per_page * control.page;
229        let limit = control.per_page;
230        entries
231            .iter()
232            // apply pagination window
233            .skip(offset)
234            .take(limit)
235            .cloned()
236            .collect::<Vec<_>>()
237    })
238}
239
240#[cfg(test)]
241mod test {
242
243    use super::*;
244
245    fn state(page: usize, per_page: usize, total: Option<usize>) -> PaginationState {
246        PaginationState {
247            control: PaginationControl { per_page, page },
248            total,
249        }
250    }
251
252    #[test]
253    fn test_navigate() {
254        let state = state(0, 10, Some(23));
255        assert_eq!(state.total_pages(), Some(3));
256        assert_eq!(state.control.page, 0);
257        assert_eq!(state.offset(), 0);
258        assert_eq!(state.range(), 0..10);
259
260        let state = state.navigate(Navigation::First);
261        assert_eq!(state.total_pages(), Some(3));
262        assert_eq!(state.control.page, 0);
263        assert_eq!(state.offset(), 0);
264        assert_eq!(state.range(), 0..10);
265
266        let state = state.navigate(Navigation::Last);
267        assert_eq!(state.total_pages(), Some(3));
268        assert_eq!(state.control.page, 2);
269        assert_eq!(state.offset(), 20);
270        assert_eq!(state.range(), 20..23);
271
272        let state = state.navigate(Navigation::Previous);
273        assert_eq!(state.total_pages(), Some(3));
274        assert_eq!(state.control.page, 1);
275        assert_eq!(state.offset(), 10);
276        assert_eq!(state.range(), 10..20);
277
278        let state = state.navigate(Navigation::Previous);
279        assert_eq!(state.total_pages(), Some(3));
280        assert_eq!(state.control.page, 0);
281        assert_eq!(state.offset(), 0);
282        assert_eq!(state.range(), 0..10);
283    }
284
285    /// ensure that it's not possible to navigate before the first page
286    #[test]
287    fn test_underflow() {
288        let state = state(0, 10, Some(23));
289
290        let state = state.navigate(Navigation::Previous);
291        assert_eq!(state.total_pages(), Some(3));
292        assert_eq!(state.control.page, 0);
293        assert_eq!(state.offset(), 0);
294        assert_eq!(state.range(), 0..10);
295    }
296
297    /// ensure start "next" stops with the last page
298    #[test]
299    fn test_overflow_1() {
300        let state = state(0, 10, Some(23));
301
302        let state = state.navigate(Navigation::Last);
303        assert_eq!(state.total_pages(), Some(3));
304        assert_eq!(state.control.page, 2);
305        assert_eq!(state.offset(), 20);
306        assert_eq!(state.range(), 20..23);
307
308        let state = state.navigate(Navigation::Next);
309        assert_eq!(state.total_pages(), Some(3));
310        assert_eq!(state.control.page, 2);
311        assert_eq!(state.offset(), 20);
312        assert_eq!(state.range(), 20..23);
313    }
314
315    /// ensure that navigating beyond the last page doesn't work
316    #[test]
317    fn test_overflow_2() {
318        let state = state(0, 10, Some(23));
319        assert_eq!(state.total_pages(), Some(3));
320
321        let state = state.navigate(Navigation::Page(5));
322        assert_eq!(state.total_pages(), Some(3));
323        assert_eq!(state.control.page, 0);
324        assert_eq!(state.offset(), 0);
325        assert_eq!(state.range(), 0..10);
326    }
327
328    #[test]
329    fn test_change_page_size() {
330        let state = state(0, 10, Some(23));
331        assert_eq!(state.total_pages(), Some(3));
332        assert_eq!(state.control.page, 0);
333        assert_eq!(state.offset(), 0);
334        assert_eq!(state.range(), 0..10);
335
336        let state = state.navigate(Navigation::Next);
337        assert_eq!(state.total_pages(), Some(3));
338        assert_eq!(state.control.page, 1);
339        assert_eq!(state.offset(), 10);
340        assert_eq!(state.range(), 10..20);
341
342        let state = state.change_per_page(5);
343        assert_eq!(state.total_pages(), Some(5));
344        assert_eq!(state.control.page, 2);
345        assert_eq!(state.offset(), 10);
346        assert_eq!(state.range(), 10..15);
347    }
348
349    #[test]
350    fn test_change_none() {
351        let state = state(0, 10, None);
352        assert_eq!(state.total_pages(), None);
353        assert_eq!(state.control.page, 0);
354        assert_eq!(state.offset(), 0);
355        assert_eq!(state.range(), 0..10);
356    }
357
358    #[test]
359    fn test_change_empty() {
360        let state = state(0, 10, Some(0));
361        assert_eq!(state.total_pages(), Some(0));
362        assert_eq!(state.control.page, 0);
363        assert_eq!(state.offset(), 0);
364        assert_eq!(state.range(), 0..0);
365    }
366
367    #[test]
368    fn test_total_pages() {
369        for i in 0..100 {
370            let state = state(0, 10, Some(i));
371            assert_eq!(
372                state.total_pages(),
373                Some((i as f64 / 10f64).ceil() as usize)
374            );
375        }
376    }
377}