Skip to main content

dioxus_bootstrap_css/
button.rs

1use dioxus::prelude::*;
2
3use crate::types::{Color, Size};
4
5/// Bootstrap Button component.
6///
7/// Renders a `<button>` by default. When `href` is set, renders an `<a>` element
8/// instead (Bootstrap link-button pattern).
9///
10/// Accepts all standard HTML attributes via `extends = GlobalAttributes`.
11/// This means `title`, `data-bs-toggle`, `aria-label`, `id`, etc. all work.
12///
13/// # Bootstrap HTML → Dioxus
14///
15/// | HTML | Dioxus |
16/// |---|---|
17/// | `<button class="btn btn-primary">` | `Button { color: Color::Primary, "Text" }` |
18/// | `<button class="btn btn-outline-danger btn-sm">` | `Button { color: Color::Danger, outline: true, size: Size::Sm, "Text" }` |
19/// | `<button class="btn btn-success btn-lg" disabled>` | `Button { color: Color::Success, size: Size::Lg, disabled: true, "Text" }` |
20/// | `<a class="btn btn-primary" href="/page">` | `Button { color: Color::Primary, href: "/page", "Link" }` |
21/// | `<a class="btn btn-sm" href="f.json" target="_blank" download="f.json">` | `Button { size: Size::Sm, href: "f.json", target: "_blank", download: "f.json", "DL" }` |
22/// | `<button class="btn btn-primary" title="Tip">` | `Button { color: Color::Primary, title: "Tip", "Text" }` |
23///
24/// ```rust,no_run
25/// rsx! {
26///     Button { color: Color::Primary, "Click me" }
27///     Button { color: Color::Danger, outline: true, size: Size::Sm, "Delete" }
28///     Button { color: Color::Success, disabled: true, "Saved" }
29///     Button { color: Color::Warning, onclick: move |_| { /* handler */ }, "Action" }
30///     // Link button — renders <a> instead of <button>:
31///     Button { color: Color::Primary, href: "/page", "Go to Page" }
32///     // HTML attributes work directly:
33///     Button { color: Color::Secondary, title: "Tooltip text", "Hover me" }
34///     Button { color: Color::Primary, "data-bs-toggle": "modal", "Open Modal" }
35/// }
36/// ```
37#[derive(Clone, PartialEq, Props)]
38pub struct ButtonProps {
39    /// Button color variant.
40    #[props(default)]
41    pub color: Color,
42    /// Use outline style instead of filled.
43    #[props(default)]
44    pub outline: bool,
45    /// Button size.
46    #[props(default)]
47    pub size: Size,
48    /// Whether the button is disabled.
49    #[props(default)]
50    pub disabled: bool,
51    /// When set, renders an `<a>` element instead of `<button>` (link-button pattern).
52    #[props(default)]
53    pub href: Option<String>,
54    /// Link target (e.g., `"_blank"`). Only used when `href` is set.
55    #[props(default)]
56    pub target: Option<String>,
57    /// Download filename. Only used when `href` is set.
58    #[props(default)]
59    pub download: Option<String>,
60    /// HTML button type attribute (ignored when `href` is set).
61    #[props(default = "button".to_string())]
62    pub r#type: String,
63    /// Click event handler.
64    #[props(default)]
65    pub onclick: Option<EventHandler<MouseEvent>>,
66    /// Active (pressed) state.
67    #[props(default)]
68    pub active: bool,
69    /// Additional CSS classes.
70    #[props(default)]
71    pub class: String,
72    /// Any additional HTML attributes (title, data-bs-toggle, aria-*, id, etc.)
73    #[props(extends = GlobalAttributes)]
74    attributes: Vec<Attribute>,
75    /// Child elements.
76    pub children: Element,
77}
78
79#[component]
80pub fn Button(props: ButtonProps) -> Element {
81    let style = if props.outline { "btn-outline" } else { "btn" };
82    let color = props.color;
83    let color_class = format!("{style}-{color}");
84
85    let size_class = match props.size {
86        Size::Md => String::new(),
87        s => format!(" btn-{s}"),
88    };
89
90    let active_class = if props.active { " active" } else { "" };
91
92    let full_class = if props.class.is_empty() {
93        format!("btn {color_class}{size_class}{active_class}")
94    } else {
95        format!(
96            "btn {color_class}{size_class}{active_class} {}",
97            props.class
98        )
99    };
100
101    if let Some(href) = &props.href {
102        // Link-button: render <a> with role="button"
103        let disabled_class = if props.disabled { " disabled" } else { "" };
104        let link_class = format!("{full_class}{disabled_class}");
105        let target = props.target.clone();
106        let download = props.download.clone();
107        rsx! {
108            a {
109                class: "{link_class}",
110                href: "{href}",
111                role: "button",
112                target: target,
113                download: download,
114                onclick: move |evt| {
115                    if let Some(handler) = &props.onclick {
116                        handler.call(evt);
117                    }
118                },
119                ..props.attributes,
120                {props.children}
121            }
122        }
123    } else {
124        rsx! {
125            button {
126                class: "{full_class}",
127                r#type: "{props.r#type}",
128                disabled: props.disabled,
129                onclick: move |evt| {
130                    if let Some(handler) = &props.onclick {
131                        handler.call(evt);
132                    }
133                },
134                ..props.attributes,
135                {props.children}
136            }
137        }
138    }
139}
140
141/// Bootstrap ButtonGroup component.
142///
143/// ```rust,no_run
144/// rsx! {
145///     ButtonGroup {
146///         Button { color: Color::Primary, "Left" }
147///         Button { color: Color::Primary, "Middle" }
148///         Button { color: Color::Primary, "Right" }
149///     }
150/// }
151/// ```
152#[derive(Clone, PartialEq, Props)]
153pub struct ButtonGroupProps {
154    /// Button group size.
155    #[props(default)]
156    pub size: Size,
157    /// Additional CSS classes.
158    #[props(default)]
159    pub class: String,
160    /// Child elements (buttons).
161    pub children: Element,
162}
163
164#[component]
165pub fn ButtonGroup(props: ButtonGroupProps) -> Element {
166    let size_class = match props.size {
167        Size::Md => String::new(),
168        s => format!(" btn-group-{s}"),
169    };
170
171    let full_class = if props.class.is_empty() {
172        format!("btn-group{size_class}")
173    } else {
174        format!("btn-group{size_class} {}", props.class)
175    };
176
177    rsx! {
178        div {
179            class: "{full_class}",
180            role: "group",
181            {props.children}
182        }
183    }
184}
185
186/// Bootstrap ButtonToolbar — groups multiple ButtonGroups.
187///
188/// ```rust,no_run
189/// rsx! {
190///     ButtonToolbar {
191///         ButtonGroup {
192///             Button { color: Color::Primary, "1" }
193///             Button { color: Color::Primary, "2" }
194///         }
195///         ButtonGroup {
196///             Button { color: Color::Secondary, "A" }
197///         }
198///     }
199/// }
200/// ```
201#[derive(Clone, PartialEq, Props)]
202pub struct ButtonToolbarProps {
203    /// Additional CSS classes.
204    #[props(default)]
205    pub class: String,
206    /// Child elements (ButtonGroups).
207    pub children: Element,
208}
209
210#[component]
211pub fn ButtonToolbar(props: ButtonToolbarProps) -> Element {
212    let full_class = if props.class.is_empty() {
213        "btn-toolbar".to_string()
214    } else {
215        format!("btn-toolbar {}", props.class)
216    };
217
218    rsx! {
219        div {
220            class: "{full_class}",
221            role: "toolbar",
222            {props.children}
223        }
224    }
225}