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}