Skip to main content

dioxus_bootstrap_css/
pagination.rs

1use dioxus::prelude::*;
2
3use crate::types::Size;
4
5/// Bootstrap Pagination component — signal-driven, no JavaScript.
6///
7/// Automatically generates page numbers with ellipsis, prev/next buttons,
8/// and highlights the active page.
9///
10/// # Bootstrap HTML → Dioxus
11///
12/// ```html
13/// <!-- Bootstrap HTML (manual) -->
14/// <nav><ul class="pagination pagination-sm">
15///   <li class="page-item"><button class="page-link">‹</button></li>
16///   <li class="page-item active"><button class="page-link">1</button></li>
17///   <li class="page-item"><button class="page-link">2</button></li>
18///   <li class="page-item"><button class="page-link">›</button></li>
19/// </ul></nav>
20/// ```
21///
22/// ```rust,no_run
23/// # use dioxus::prelude::*;
24/// # use dioxus_bootstrap_css::prelude::*;
25/// # fn _doctest() -> Element {
26/// // Dioxus equivalent — fully automatic
27/// let page = use_signal(|| 1usize);
28/// rsx! {
29///     Pagination { current: page, total: 20, window: 2, size: Size::Sm }
30/// }
31/// # }
32/// ```
33///
34/// # Props
35///
36/// - `current` — `Signal<usize>` for current page (1-based)
37/// - `total` — total number of pages
38/// - `window` — number of page links around current (default: 2)
39/// - `size` — `Size::Sm`, `Md`, `Lg`
40/// - `show_prev_next` — show prev/next buttons (default: true)
41#[derive(Clone, PartialEq, Props)]
42pub struct PaginationProps {
43    /// Signal controlling the current page (1-based).
44    pub current: Signal<usize>,
45    /// Total number of pages.
46    pub total: usize,
47    /// Number of page links to show around the current page.
48    #[props(default = 2)]
49    pub window: usize,
50    /// Pagination size.
51    #[props(default)]
52    pub size: Size,
53    /// Show previous/next buttons.
54    #[props(default = true)]
55    pub show_prev_next: bool,
56    /// Additional CSS classes.
57    #[props(default)]
58    pub class: String,
59}
60
61#[component]
62pub fn Pagination(props: PaginationProps) -> Element {
63    let current = *props.current.read();
64    let mut page_signal = props.current;
65    let total = props.total;
66
67    if total == 0 {
68        return rsx! {};
69    }
70
71    let size_class = match props.size {
72        Size::Md => String::new(),
73        s => format!(" pagination-{s}"),
74    };
75
76    let full_class = if props.class.is_empty() {
77        format!("pagination{size_class}")
78    } else {
79        format!("pagination{size_class} {}", props.class)
80    };
81
82    // Calculate visible page range
83    let start = if current > props.window {
84        current - props.window
85    } else {
86        1
87    };
88    let end = if current + props.window <= total {
89        current + props.window
90    } else {
91        total
92    };
93
94    rsx! {
95        nav { "aria-label": "Page navigation",
96            ul { class: "{full_class}",
97                // Previous
98                if props.show_prev_next {
99                    li { class: if current <= 1 { "page-item disabled" } else { "page-item" },
100                        button {
101                            class: "page-link",
102                            disabled: current <= 1,
103                            onclick: move |_| {
104                                if current > 1 {
105                                    page_signal.set(current - 1);
106                                }
107                            },
108                            "aria-label": "Previous",
109                            span { "aria-hidden": "true", "\u{2039}" }
110                        }
111                    }
112                }
113
114                // First page + ellipsis
115                if start > 1 {
116                    li { class: "page-item",
117                        button {
118                            class: "page-link",
119                            onclick: move |_| page_signal.set(1),
120                            "1"
121                        }
122                    }
123                    if start > 2 {
124                        li { class: "page-item disabled",
125                            span { class: "page-link", "\u{2026}" }
126                        }
127                    }
128                }
129
130                // Page numbers
131                for p in start..=end {
132                    li {
133                        class: if p == current { "page-item active" } else { "page-item" },
134                        button {
135                            class: "page-link",
136                            "aria-current": if p == current { "page" } else { "" },
137                            onclick: move |_| page_signal.set(p),
138                            "{p}"
139                        }
140                    }
141                }
142
143                // Last page + ellipsis
144                if end < total {
145                    if end < total - 1 {
146                        li { class: "page-item disabled",
147                            span { class: "page-link", "\u{2026}" }
148                        }
149                    }
150                    li { class: "page-item",
151                        button {
152                            class: "page-link",
153                            onclick: move |_| page_signal.set(total),
154                            "{total}"
155                        }
156                    }
157                }
158
159                // Next
160                if props.show_prev_next {
161                    li { class: if current >= total { "page-item disabled" } else { "page-item" },
162                        button {
163                            class: "page-link",
164                            disabled: current >= total,
165                            onclick: move |_| {
166                                if current < total {
167                                    page_signal.set(current + 1);
168                                }
169                            },
170                            "aria-label": "Next",
171                            span { "aria-hidden": "true", "\u{203A}" }
172                        }
173                    }
174                }
175            }
176        }
177    }
178}