ironhtml_bootstrap/
buttons.rs

1//! Bootstrap button components.
2//!
3//! Provides type-safe Bootstrap button generation matching the
4//! [Bootstrap button documentation](https://getbootstrap.com/docs/5.3/components/buttons/).
5//!
6//! ## Example
7//!
8//! ```rust
9//! use ironhtml_bootstrap::{buttons::*, Color, Size};
10//!
11//! // Primary button
12//! let primary = btn(Color::Primary, "Click me");
13//! assert!(primary.render().contains(r#"class="btn btn-primary"#));
14//!
15//! // Outline button
16//! let outline = btn_outline(Color::Danger, "Delete");
17//! assert!(outline.render().contains(r#"class="btn btn-outline-danger"#));
18//!
19//! // Large button
20//! let large = btn_sized(Color::Success, Size::Large, "Submit");
21//! assert!(large.render().contains("btn-lg"));
22//! ```
23
24use crate::{Color, Size};
25use ironhtml::typed::Element;
26use ironhtml_elements::Button;
27
28extern crate alloc;
29use alloc::format;
30
31/// Create a Bootstrap button.
32///
33/// Generates:
34/// ```html
35/// <button type="button" class="btn btn-{color}">{text}</button>
36/// ```
37///
38/// ## Example
39///
40/// ```rust
41/// use ironhtml_bootstrap::{buttons::btn, Color};
42///
43/// let button = btn(Color::Primary, "Click me");
44/// assert_eq!(
45///     button.render(),
46///     r#"<button type="button" class="btn btn-primary">Click me</button>"#
47/// );
48/// ```
49#[must_use]
50pub fn btn(color: Color, text: &str) -> Element<Button> {
51    let class = format!("btn btn-{}", color.as_str());
52    Element::<Button>::new()
53        .attr("type", "button")
54        .class(&class)
55        .text(text)
56}
57
58/// Create an outline Bootstrap button.
59///
60/// Generates:
61/// ```html
62/// <button type="button" class="btn btn-outline-{color}">{text}</button>
63/// ```
64#[must_use]
65pub fn btn_outline(color: Color, text: &str) -> Element<Button> {
66    let class = format!("btn btn-outline-{}", color.as_str());
67    Element::<Button>::new()
68        .attr("type", "button")
69        .class(&class)
70        .text(text)
71}
72
73/// Create a sized Bootstrap button.
74///
75/// Generates:
76/// ```html
77/// <button type="button" class="btn btn-{color} btn-{size}">{text}</button>
78/// ```
79#[must_use]
80pub fn btn_sized(color: Color, size: Size, text: &str) -> Element<Button> {
81    let size_class = size.as_btn_class();
82    let class = if size_class.is_empty() {
83        format!("btn btn-{}", color.as_str())
84    } else {
85        format!("btn btn-{} {size_class}", color.as_str())
86    };
87    Element::<Button>::new()
88        .attr("type", "button")
89        .class(&class)
90        .text(text)
91}
92
93/// Create a sized outline Bootstrap button.
94#[must_use]
95pub fn btn_outline_sized(color: Color, size: Size, text: &str) -> Element<Button> {
96    let size_class = size.as_btn_class();
97    let class = if size_class.is_empty() {
98        format!("btn btn-outline-{}", color.as_str())
99    } else {
100        format!("btn btn-outline-{} {size_class}", color.as_str())
101    };
102    Element::<Button>::new()
103        .attr("type", "button")
104        .class(&class)
105        .text(text)
106}
107
108/// Create a disabled button.
109#[must_use]
110pub fn btn_disabled(color: Color, text: &str) -> Element<Button> {
111    let class = format!("btn btn-{}", color.as_str());
112    Element::<Button>::new()
113        .attr("type", "button")
114        .class(&class)
115        .bool_attr("disabled")
116        .text(text)
117}
118
119/// Create a link-styled button.
120///
121/// Generates: `<button type="button" class="btn btn-link">{text}</button>`
122#[must_use]
123pub fn btn_link(text: &str) -> Element<Button> {
124    Element::<Button>::new()
125        .attr("type", "button")
126        .class("btn btn-link")
127        .text(text)
128}
129
130/// Create a button with a loading spinner (border style).
131///
132/// ## Example
133///
134/// ```rust
135/// use ironhtml_bootstrap::{buttons::btn_loading, Color};
136///
137/// let btn = btn_loading(Color::Primary, "Loading...");
138/// assert!(btn.render().contains("spinner-border"));
139/// ```
140#[must_use]
141pub fn btn_loading(color: Color, text: &str) -> Element<Button> {
142    use ironhtml_elements::Span;
143
144    let class = format!("btn btn-{}", color.as_str());
145    Element::<Button>::new()
146        .attr("type", "button")
147        .class(&class)
148        .bool_attr("disabled")
149        .child::<Span, _>(|s| {
150            s.class("spinner-border spinner-border-sm")
151                .attr("role", "status")
152                .attr("aria-hidden", "true")
153        })
154        .text(format!(" {text}"))
155}
156
157/// Create a button with a growing loading spinner.
158///
159/// ## Example
160///
161/// ```rust
162/// use ironhtml_bootstrap::{buttons::btn_loading_grow, Color};
163///
164/// let btn = btn_loading_grow(Color::Primary, "Loading...");
165/// assert!(btn.render().contains("spinner-grow"));
166/// ```
167#[must_use]
168pub fn btn_loading_grow(color: Color, text: &str) -> Element<Button> {
169    use ironhtml_elements::Span;
170
171    let class = format!("btn btn-{}", color.as_str());
172    Element::<Button>::new()
173        .attr("type", "button")
174        .class(&class)
175        .bool_attr("disabled")
176        .child::<Span, _>(|s| {
177            s.class("spinner-grow spinner-grow-sm")
178                .attr("role", "status")
179                .attr("aria-hidden", "true")
180        })
181        .text(format!(" {text}"))
182}
183
184/// Create a block-level button (full width).
185///
186/// ## Example
187///
188/// ```rust
189/// use ironhtml_bootstrap::{buttons::btn_block, Color};
190///
191/// let btn = btn_block(Color::Primary, "Full Width");
192/// assert!(btn.render().contains("w-100"));
193/// ```
194#[must_use]
195pub fn btn_block(color: Color, text: &str) -> Element<Button> {
196    let class = format!("btn btn-{} w-100", color.as_str());
197    Element::<Button>::new()
198        .attr("type", "button")
199        .class(&class)
200        .text(text)
201}
202
203/// Create a button with an icon.
204#[must_use]
205pub fn btn_icon(color: Color, icon_class: &str, text: &str) -> Element<Button> {
206    use ironhtml_elements::I;
207
208    let class = format!("btn btn-{}", color.as_str());
209    Element::<Button>::new()
210        .attr("type", "button")
211        .class(&class)
212        .child::<I, _>(|i| i.class(icon_class))
213        .text(format!(" {text}"))
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_btn_primary() {
222        let b = btn(Color::Primary, "Click");
223        assert_eq!(
224            b.render(),
225            r#"<button type="button" class="btn btn-primary">Click</button>"#
226        );
227    }
228
229    #[test]
230    fn test_btn_outline() {
231        let b = btn_outline(Color::Danger, "Delete");
232        assert!(b.render().contains("btn-outline-danger"));
233    }
234
235    #[test]
236    fn test_btn_sized() {
237        let b = btn_sized(Color::Success, Size::Large, "Submit");
238        let html = b.render();
239        assert!(html.contains("btn-success"));
240        assert!(html.contains("btn-lg"));
241    }
242
243    #[test]
244    fn test_all_colors() {
245        for color in [
246            Color::Primary,
247            Color::Secondary,
248            Color::Success,
249            Color::Danger,
250            Color::Warning,
251            Color::Info,
252            Color::Light,
253            Color::Dark,
254        ] {
255            let b = btn(color, "Test");
256            assert!(b.render().contains(&format!("btn-{}", color.as_str())));
257        }
258    }
259}