Skip to main content

dioxus_bootstrap_css/
list_group.rs

1use dioxus::prelude::*;
2
3use crate::types::Color;
4
5/// Bootstrap ListGroup component.
6///
7/// # Bootstrap HTML → Dioxus
8///
9/// ```html
10/// <!-- Bootstrap HTML -->
11/// <ul class="list-group list-group-flush">
12///   <li class="list-group-item active">Active</li>
13///   <li class="list-group-item">Normal</li>
14///   <li class="list-group-item list-group-item-danger">Danger</li>
15///   <li class="list-group-item disabled">Disabled</li>
16/// </ul>
17/// ```
18///
19/// ```rust,no_run
20/// # use dioxus::prelude::*;
21/// # use dioxus_bootstrap_css::prelude::*;
22/// # fn _doctest() -> Element {
23/// # let handler = move |_: MouseEvent| {};
24/// rsx! {
25///     ListGroup { flush: true,
26///         ListGroupItem { active: true, "Active" }
27///         ListGroupItem { "Normal" }
28///         ListGroupItem { color: Color::Danger, "Danger" }
29///         ListGroupItem { disabled: true, "Disabled" }
30///     }
31///     // Clickable list group
32///     ListGroup {
33///         ListGroupItem { onclick: handler, "Click me" }
34///     }
35///     // Numbered list
36///     ListGroup { numbered: true,
37///         ListGroupItem { "First" }
38///         ListGroupItem { "Second" }
39///     }
40/// }
41/// # }
42/// ```
43#[derive(Clone, PartialEq, Props)]
44pub struct ListGroupProps {
45    /// Remove borders and rounded corners for use inside cards.
46    #[props(default)]
47    pub flush: bool,
48    /// Use numbered list style.
49    #[props(default)]
50    pub numbered: bool,
51    /// Container element. Empty (the default) renders `<ul>`, or `<ol>` when
52    /// `numbered`; `"div"` renders Bootstrap's generic `<div class="list-group">`
53    /// form, which is what a list of links or buttons requires — `<a>` and
54    /// `<button>` are not valid children of `<ul>`.
55    #[props(default)]
56    pub tag: String,
57    /// Additional CSS classes.
58    #[props(default)]
59    pub class: String,
60    /// Any additional HTML attributes.
61    #[props(extends = GlobalAttributes)]
62    attributes: Vec<Attribute>,
63    /// Child elements (ListGroupItem components).
64    pub children: Element,
65}
66
67#[component]
68pub fn ListGroup(props: ListGroupProps) -> Element {
69    let mut classes = vec!["list-group".to_string()];
70    if props.flush {
71        classes.push("list-group-flush".to_string());
72    }
73    if props.numbered {
74        classes.push("list-group-numbered".to_string());
75    }
76    if !props.class.is_empty() {
77        classes.push(props.class.clone());
78    }
79    let full_class = classes.join(" ");
80
81    // `div` first: the generic form is the one that can hold `<a>`/`<button>`
82    // children, so it must win over the numbered `<ol>` rather than be
83    // unreachable behind it.
84    if props.tag == "div" {
85        rsx! {
86            div { class: "{full_class}", ..props.attributes, {props.children} }
87        }
88    } else if props.numbered {
89        rsx! {
90            ol { class: "{full_class}", ..props.attributes, {props.children} }
91        }
92    } else {
93        rsx! {
94            ul { class: "{full_class}", ..props.attributes, {props.children} }
95        }
96    }
97}
98
99/// Bootstrap ListGroupItem component.
100#[derive(Clone, PartialEq, Props)]
101pub struct ListGroupItemProps {
102    /// Active state.
103    #[props(default)]
104    pub active: bool,
105    /// Disabled state.
106    #[props(default)]
107    pub disabled: bool,
108    /// Item color variant.
109    #[props(default)]
110    pub color: Option<Color>,
111    /// Click event handler. When set, renders as a button for interactivity.
112    #[props(default)]
113    pub onclick: Option<EventHandler<MouseEvent>>,
114    /// Item element. Empty (the default) renders `<li>`; `"div"` renders
115    /// `<div class="list-group-item">` for the generic form. Ignored when
116    /// `onclick` is set — an actionable item is always a `<button>`.
117    #[props(default)]
118    pub tag: String,
119    /// Additional CSS classes.
120    #[props(default)]
121    pub class: String,
122    /// Any additional HTML attributes.
123    #[props(extends = GlobalAttributes)]
124    attributes: Vec<Attribute>,
125    /// Child elements.
126    pub children: Element,
127}
128
129#[component]
130pub fn ListGroupItem(props: ListGroupItemProps) -> Element {
131    let mut classes = vec!["list-group-item".to_string()];
132    if props.active {
133        classes.push("active".to_string());
134    }
135    if props.disabled {
136        classes.push("disabled".to_string());
137    }
138    if let Some(ref c) = props.color {
139        classes.push(format!("list-group-item-{c}"));
140    }
141    if props.onclick.is_some() {
142        classes.push("list-group-item-action".to_string());
143    }
144    if !props.class.is_empty() {
145        classes.push(props.class.clone());
146    }
147    let full_class = classes.join(" ");
148
149    if let Some(handler) = &props.onclick {
150        let handler = *handler;
151        rsx! {
152            button {
153                class: "{full_class}",
154                r#type: "button",
155                disabled: props.disabled,
156                onclick: move |evt| handler.call(evt),
157                ..props.attributes,
158                {props.children}
159            }
160        }
161    } else if props.tag == "div" {
162        rsx! {
163            div { class: "{full_class}", ..props.attributes, {props.children} }
164        }
165    } else {
166        rsx! {
167            li { class: "{full_class}", ..props.attributes, {props.children} }
168        }
169    }
170}