ironhtml_bootstrap/
navbar.rs

1//! Bootstrap navbar components.
2//!
3//! Provides type-safe Bootstrap navbars matching the
4//! [Bootstrap navbar documentation](https://getbootstrap.com/docs/5.3/components/navbar/).
5//!
6//! ## Example
7//!
8//! ```rust
9//! use ironhtml_bootstrap::navbar::{navbar, nav_item};
10//! use ironhtml_bootstrap::NavbarExpand;
11//! use ironhtml_elements::Li;
12//!
13//! // Create a navbar with brand and nav items
14//! let nav = navbar("MyApp", NavbarExpand::Lg, "main-nav", |nav| {
15//!     nav.child::<Li, _>(|_| nav_item("/", "Home", true))
16//!        .child::<Li, _>(|_| nav_item("/about", "About", false))
17//!        .child::<Li, _>(|_| nav_item("/contact", "Contact", false))
18//! });
19//!
20//! let html = nav.render();
21//! assert!(html.contains("navbar"));
22//! assert!(html.contains("navbar-brand"));
23//! assert!(html.contains("nav-item"));
24//! ```
25
26use crate::NavbarExpand;
27use ironhtml::typed::Element;
28use ironhtml_elements::{Button, Div, Li, Nav, Span, Ul, A};
29
30extern crate alloc;
31use alloc::format;
32
33/// Create a Bootstrap navbar.
34///
35/// Generates the standard responsive navbar structure from Bootstrap docs.
36///
37/// ## Example
38///
39/// ```rust
40/// use ironhtml_bootstrap::{navbar::navbar, NavbarExpand};
41///
42/// let nav = navbar("Brand", NavbarExpand::Lg, "navbarNav", |nav| {
43///     nav // add nav items here
44/// });
45/// assert!(nav.render().contains("navbar-brand"));
46/// ```
47#[must_use]
48pub fn navbar<F>(brand: &str, expand: NavbarExpand, id: &str, f: F) -> Element<Nav>
49where
50    F: FnOnce(Element<Ul>) -> Element<Ul>,
51{
52    let class = format!("navbar {} bg-body-tertiary", expand.as_class());
53    let target = format!("#{id}");
54
55    Element::<Nav>::new()
56        .class(&class)
57        .child::<Div, _>(|container| {
58            container
59                .class("container-fluid")
60                .child::<A, _>(|a| a.class("navbar-brand").attr("href", "#").text(brand))
61                .child::<Button, _>(|btn| {
62                    btn.class("navbar-toggler")
63                        .attr("type", "button")
64                        .attr("data-bs-toggle", "collapse")
65                        .attr("data-bs-target", &target)
66                        .attr("aria-controls", id)
67                        .attr("aria-expanded", "false")
68                        .attr("aria-label", "Toggle navigation")
69                        .child::<Span, _>(|s| s.class("navbar-toggler-icon"))
70                })
71                .child::<Div, _>(|collapse| {
72                    collapse
73                        .class("collapse navbar-collapse")
74                        .id(id)
75                        .child::<Ul, _>(|ul| f(ul.class("navbar-nav me-auto mb-2 mb-lg-0")))
76                })
77        })
78}
79
80/// Create a navbar with dark theme.
81#[must_use]
82pub fn navbar_dark<F>(brand: &str, expand: NavbarExpand, id: &str, f: F) -> Element<Nav>
83where
84    F: FnOnce(Element<Ul>) -> Element<Ul>,
85{
86    let class = format!("navbar {} bg-dark", expand.as_class());
87    let target = format!("#{id}");
88
89    Element::<Nav>::new()
90        .class(&class)
91        .attr("data-bs-theme", "dark")
92        .child::<Div, _>(|container| {
93            container
94                .class("container-fluid")
95                .child::<A, _>(|a| a.class("navbar-brand").attr("href", "#").text(brand))
96                .child::<Button, _>(|btn| {
97                    btn.class("navbar-toggler")
98                        .attr("type", "button")
99                        .attr("data-bs-toggle", "collapse")
100                        .attr("data-bs-target", &target)
101                        .attr("aria-controls", id)
102                        .attr("aria-expanded", "false")
103                        .attr("aria-label", "Toggle navigation")
104                        .child::<Span, _>(|s| s.class("navbar-toggler-icon"))
105                })
106                .child::<Div, _>(|collapse| {
107                    collapse
108                        .class("collapse navbar-collapse")
109                        .id(id)
110                        .child::<Ul, _>(|ul| f(ul.class("navbar-nav me-auto mb-2 mb-lg-0")))
111                })
112        })
113}
114
115/// Create a nav item.
116///
117/// Generates: `<li class="nav-item"><a class="nav-link" href="...">{text}</a></li>`
118#[must_use]
119pub fn nav_item(href: &str, text: &str, active: bool) -> Element<Li> {
120    let link_class = if active {
121        "nav-link active"
122    } else {
123        "nav-link"
124    };
125
126    if active {
127        Element::<Li>::new().class("nav-item").child::<A, _>(|a| {
128            a.class(link_class)
129                .attr("aria-current", "page")
130                .attr("href", href)
131                .text(text)
132        })
133    } else {
134        Element::<Li>::new()
135            .class("nav-item")
136            .child::<A, _>(|a| a.class(link_class).attr("href", href).text(text))
137    }
138}
139
140/// Create a disabled nav item.
141#[must_use]
142pub fn nav_item_disabled(text: &str) -> Element<Li> {
143    Element::<Li>::new().class("nav-item").child::<A, _>(|a| {
144        a.class("nav-link disabled")
145            .attr("aria-disabled", "true")
146            .text(text)
147    })
148}
149
150/// Wrapper to add nav items to a navbar.
151///
152/// This trait allows adding nav items to a ul element.
153pub trait NavItemExt {
154    /// Add a nav item to this navbar.
155    #[must_use]
156    fn child(self, item: Element<Li>) -> Self;
157}
158
159impl NavItemExt for Element<Ul> {
160    fn child(self, item: Element<Li>) -> Self {
161        self.child::<Li, _>(|_| item)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_navbar() {
171        let nav = navbar("Brand", NavbarExpand::Lg, "nav", |ul| ul);
172        let html = nav.render();
173        assert!(html.contains("navbar"));
174        assert!(html.contains("navbar-expand-lg"));
175        assert!(html.contains("navbar-brand"));
176        assert!(html.contains("navbar-toggler"));
177        assert!(html.contains("collapse navbar-collapse"));
178    }
179
180    #[test]
181    fn test_nav_item() {
182        let item = nav_item("/home", "Home", true);
183        let html = item.render();
184        assert!(html.contains("nav-item"));
185        assert!(html.contains("nav-link active"));
186        assert!(html.contains(r#"aria-current="page"#));
187    }
188
189    #[test]
190    fn test_nav_item_inactive() {
191        let item = nav_item("/about", "About", false);
192        let html = item.render();
193        assert!(html.contains("nav-item"));
194        assert!(html.contains("nav-link"));
195        assert!(!html.contains("active"));
196    }
197}