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}