ironhtml_bootstrap/
list_group.rs

1//! Bootstrap list group components.
2//!
3//! Provides type-safe Bootstrap list groups matching the
4//! [Bootstrap list group documentation](https://getbootstrap.com/docs/5.3/components/list-group/).
5
6use crate::Color;
7use ironhtml::typed::Element;
8use ironhtml_elements::{Button, Div, Li, Ul, A};
9
10extern crate alloc;
11use alloc::string::String;
12
13/// Create a basic list group.
14///
15/// ## Example
16///
17/// ```rust
18/// use ironhtml_bootstrap::list_group::list_group;
19///
20/// let items = vec!["Item 1", "Item 2", "Item 3"];
21/// let list = list_group(&items);
22/// assert!(list.render().contains("list-group"));
23/// ```
24#[must_use]
25pub fn list_group(items: &[&str]) -> Element<Ul> {
26    items
27        .iter()
28        .fold(Element::<Ul>::new().class("list-group"), |ul, item| {
29            ul.child::<Li, _>(|li| li.class("list-group-item").text(*item))
30        })
31}
32
33/// Create a flush list group (no borders, square corners).
34#[must_use]
35pub fn list_group_flush(items: &[&str]) -> Element<Ul> {
36    items.iter().fold(
37        Element::<Ul>::new().class("list-group list-group-flush"),
38        |ul, item| ul.child::<Li, _>(|li| li.class("list-group-item").text(*item)),
39    )
40}
41
42/// A list group link item.
43pub struct ListGroupLink {
44    pub text: String,
45    pub href: String,
46    pub active: bool,
47    pub disabled: bool,
48    pub color: Option<Color>,
49}
50
51impl ListGroupLink {
52    /// Create a new list group link.
53    #[must_use]
54    pub fn new(text: impl Into<String>, href: impl Into<String>) -> Self {
55        Self {
56            text: text.into(),
57            href: href.into(),
58            active: false,
59            disabled: false,
60            color: None,
61        }
62    }
63
64    /// Set this link as active.
65    #[must_use]
66    pub const fn active(mut self) -> Self {
67        self.active = true;
68        self
69    }
70
71    /// Set this link as disabled.
72    #[must_use]
73    pub const fn disabled(mut self) -> Self {
74        self.disabled = true;
75        self
76    }
77
78    /// Set a contextual color.
79    #[must_use]
80    pub const fn color(mut self, color: Color) -> Self {
81        self.color = Some(color);
82        self
83    }
84}
85
86/// Create a list group with links.
87///
88/// ## Example
89///
90/// ```rust
91/// use ironhtml_bootstrap::list_group::{list_group_links, ListGroupLink};
92///
93/// let items = vec![
94///     ListGroupLink::new("Home", "#").active(),
95///     ListGroupLink::new("Profile", "#"),
96///     ListGroupLink::new("Settings", "#"),
97///     ListGroupLink::new("Disabled", "#").disabled(),
98/// ];
99///
100/// let list = list_group_links(&items);
101/// assert!(list.render().contains("list-group-item-action"));
102/// ```
103#[must_use]
104pub fn list_group_links(items: &[ListGroupLink]) -> Element<Div> {
105    items
106        .iter()
107        .fold(Element::<Div>::new().class("list-group"), |div, item| {
108            div.child::<A, _>(|_| list_group_link_item(item))
109        })
110}
111
112/// Create a single list group link item.
113fn list_group_link_item(item: &ListGroupLink) -> Element<A> {
114    let mut class = String::from("list-group-item list-group-item-action");
115
116    if item.active {
117        class.push_str(" active");
118    }
119    if item.disabled {
120        class.push_str(" disabled");
121    }
122    if let Some(color) = item.color {
123        use core::fmt::Write;
124        let _ = write!(class, " list-group-item-{}", color.as_str());
125    }
126
127    let mut elem = Element::<A>::new()
128        .attr("href", &item.href)
129        .class(&class)
130        .text(&item.text);
131
132    if item.active {
133        elem = elem.attr("aria-current", "true");
134    }
135    if item.disabled {
136        elem = elem.attr("aria-disabled", "true");
137    }
138
139    elem
140}
141
142/// Create a list group with buttons.
143#[must_use]
144pub fn list_group_buttons(items: &[(String, bool, bool)]) -> Element<Div> {
145    items.iter().fold(
146        Element::<Div>::new().class("list-group"),
147        |div, (text, active, disabled)| {
148            div.child::<Button, _>(|btn| {
149                let mut class = String::from("list-group-item list-group-item-action");
150                if *active {
151                    class.push_str(" active");
152                }
153
154                let mut btn = btn.attr("type", "button").class(&class).text(text);
155
156                if *active {
157                    btn = btn.attr("aria-current", "true");
158                }
159                if *disabled {
160                    btn = btn.bool_attr("disabled");
161                }
162                btn
163            })
164        },
165    )
166}
167
168/// Create a numbered list group.
169#[must_use]
170pub fn list_group_numbered(items: &[&str]) -> Element<Ol> {
171    items.iter().fold(
172        Element::<Ol>::new().class("list-group list-group-numbered"),
173        |ol, item| ol.child::<Li, _>(|li| li.class("list-group-item").text(*item)),
174    )
175}
176
177/// Create a horizontal list group.
178#[must_use]
179pub fn list_group_horizontal(items: &[&str]) -> Element<Ul> {
180    items.iter().fold(
181        Element::<Ul>::new().class("list-group list-group-horizontal"),
182        |ul, item| ul.child::<Li, _>(|li| li.class("list-group-item").text(*item)),
183    )
184}
185
186use ironhtml_elements::Ol;
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use alloc::vec;
192
193    #[test]
194    fn test_list_group() {
195        let items = vec!["One", "Two", "Three"];
196        let list = list_group(&items);
197        let html = list.render();
198        assert!(html.contains("list-group"));
199        assert!(html.contains("list-group-item"));
200    }
201
202    #[test]
203    fn test_list_group_links() {
204        let items = vec![
205            ListGroupLink::new("Active", "#").active(),
206            ListGroupLink::new("Normal", "#"),
207            ListGroupLink::new("Disabled", "#").disabled(),
208        ];
209        let list = list_group_links(&items);
210        let html = list.render();
211        assert!(html.contains("list-group-item-action"));
212        assert!(html.contains("active"));
213        assert!(html.contains("disabled"));
214    }
215
216    #[test]
217    fn test_list_group_colored() {
218        let items = vec![ListGroupLink::new("Success", "#").color(Color::Success)];
219        let list = list_group_links(&items);
220        assert!(list.render().contains("list-group-item-success"));
221    }
222}