leptos_pagination/hooks/
controls.rs

1use default_struct_builder::DefaultBuilder;
2use leptos::prelude::*;
3use leptos_use::math::{use_not, use_or};
4use reactive_stores::Store;
5
6use crate::{PaginationState, PaginationStateStoreFields};
7
8/// Hook for pagination page controls.
9///
10/// Depending on the current page and some configuration options,
11/// this hook returns page ranges that can be used to display pagination controls.
12///
13/// For an example, see [`use_pagination`].
14#[must_use]
15pub fn use_pagination_controls(
16    state: Store<PaginationState>,
17    options: UsePaginationControlsOptions,
18) -> PaginationControls {
19    let UsePaginationControlsOptions {
20        display_page_count,
21        margin_page_count,
22    } = options;
23
24    let page_count = Signal::derive(move || state.page_count().get().unwrap_or_default());
25
26    let additional_page_count = display_page_count / 2;
27
28    let current_page: Signal<usize> = state.current_page().into();
29    let current_range_start =
30        Signal::derive(move || current_page.get().saturating_sub(additional_page_count));
31    let current_range_end =
32        Signal::derive(move || current_page.get().saturating_add(additional_page_count));
33
34    let merge_current_with_start =
35        Memo::new(move |_| current_range_start.get() <= margin_page_count);
36    let merge_current_with_end = Memo::new(move |_| {
37        current_range_end.get() + 1 >= page_count.get().saturating_sub(margin_page_count)
38    });
39
40    let start_range_end = Signal::derive(move || {
41        if merge_current_with_start.get() {
42            current_range_end.get()
43        } else {
44            margin_page_count + 1
45        }
46    });
47
48    let end_range_start = Signal::derive(move || {
49        if merge_current_with_end.get() {
50            current_range_start.get() + 1
51        } else {
52            page_count.get().saturating_sub(margin_page_count + 1)
53        }
54    });
55
56    let merge_all = Signal::derive(move || start_range_end.get() + 1 >= end_range_start.get());
57
58    PaginationControls {
59        current_page,
60        start_range: Memo::new(move |_| {
61            let end = if merge_all.get() {
62                page_count.get()
63            } else {
64                start_range_end.get()
65            };
66
67            (0..end).collect()
68        })
69        .into(),
70        end_range: Memo::new(move |_| {
71            if merge_all.get() {
72                vec![]
73            } else {
74                let start = end_range_start.get();
75                let end = page_count.get();
76                (start..end).collect()
77            }
78        })
79        .into(),
80        current_range: Memo::new(move |_| {
81            if merge_current_with_start.get() || merge_current_with_end.get() || merge_all.get() {
82                vec![]
83            } else {
84                let start = current_page.get() - margin_page_count;
85                let end = current_page.get() + margin_page_count;
86                (start..=end).collect()
87            }
88        })
89        .into(),
90        show_separator_before: use_not(use_or(merge_current_with_start, merge_all)),
91        show_separator_after: use_not(use_or(merge_current_with_end, merge_all)),
92        page_count_error: state.page_count_error().into(),
93    }
94}
95
96/// Return type of [`use_pagination_controls`]. It provides a bunch of signals to easily build a pagination component.
97///
98/// Please note that all ranges are inclusive. This means that the start and end of each range are included in the range.
99/// Also counting starts from 0. The first page is 0.
100#[derive(Debug, Copy, Clone)]
101pub struct PaginationControls {
102    /// If the page count couldn't be determined, this signal will contain an error message.
103    pub page_count_error: Signal<Option<String>>,
104
105    pub current_page: Signal<usize>,
106
107    /// The range of pages at the start of the pagination.
108    ///
109    /// In many cases this will be `0` to `margin_page_count`.
110    ///
111    /// But if the current range is too close or overlaps then this start range with be extended so it includes the current range.
112    /// The current range will then be returned as empty.
113    ///
114    /// If there are so few pages that all the ranges (start, end, current) need to be merged into one range then they will all
115    /// be merged into this start range and they will be returned emtpy.
116    pub start_range: Signal<Vec<usize>>,
117
118    /// The range of pages at the end of the pagination.
119    ///
120    /// In many cases this will be `total_pages - margin_page_count` to `total_pages`.
121    ///
122    /// But if the current range is too close or overlaps then this end range with be extended so it includes the current range.
123    /// The current range will then be returned as empty.
124    ///
125    /// If there are so few pages that all the ranges (start, end, current) need to be merged into one range then they will all
126    /// be merged into the start range and this will be empty.
127    pub end_range: Signal<Vec<usize>>,
128
129    /// The current range of pages. This will be empty if the current range is too close to the start or the end. In this case
130    /// the range will be merged with the start or end range.
131    pub current_range: Signal<Vec<usize>>,
132
133    /// Whether to show a separator (usually an ellipsis "...") before the current range.
134    pub show_separator_before: Signal<bool>,
135
136    /// Whether to show a separator (usually an ellipsis "...") after the current range.
137    pub show_separator_after: Signal<bool>,
138}
139
140/// Options for [`use_pagination`].
141#[derive(Debug, Clone, DefaultBuilder)]
142pub struct UsePaginationControlsOptions {
143    /// How many pages to show around the current page. This number includes the current page.
144    ///
145    /// A value of 3 will display one page before and one page after the current page.
146    /// It's recommended to use odd numbers to ensure symmetry.
147    ///
148    /// Default is 5.
149    display_page_count: usize,
150
151    /// How many pages to show at the beginning and end of the pagination.
152    ///
153    /// Default is 1.
154    margin_page_count: usize,
155}
156
157impl Default for UsePaginationControlsOptions {
158    fn default() -> Self {
159        Self {
160            display_page_count: 5,
161            margin_page_count: 1,
162        }
163    }
164}