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}