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