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