Skip to main content

dioxus_bootstrap_css/
nav.rs

1use dioxus::prelude::*;
2
3use crate::types::{Color, NavbarExpand};
4
5/// Bootstrap Navbar component.
6///
7/// ```rust
8/// let collapsed = use_signal(|| true);
9/// rsx! {
10///     Navbar { color: Color::Dark, expand: NavbarExpand::Lg,
11///         brand: rsx! { a { class: "navbar-brand", href: "#", "My App" } },
12///         NavbarCollapse { collapsed: collapsed,
13///             NavItem { NavLink { href: "/", active: true, "Home" } }
14///             NavItem { NavLink { href: "/about", "About" } }
15///         }
16///     }
17/// }
18/// ```
19#[derive(Clone, PartialEq, Props)]
20pub struct NavbarProps {
21    /// Navbar color scheme.
22    #[props(default)]
23    pub color: Option<Color>,
24    /// Responsive expand breakpoint.
25    #[props(default)]
26    pub expand: NavbarExpand,
27    /// Brand element (logo, app name).
28    #[props(default)]
29    pub brand: Option<Element>,
30    /// Additional CSS classes.
31    #[props(default)]
32    pub class: String,
33    /// Child elements (nav items, collapse, etc.).
34    pub children: Element,
35}
36
37#[component]
38pub fn Navbar(props: NavbarProps) -> Element {
39    let mut classes = vec!["navbar".to_string(), props.expand.to_string()];
40
41    if let Some(ref color) = props.color {
42        match color {
43            Color::Dark => {
44                classes.push("bg-dark".to_string());
45                classes.push("[data-bs-theme=dark]".to_string());
46            }
47            Color::Light => {
48                classes.push("bg-light".to_string());
49            }
50            c => {
51                classes.push(format!("bg-{c}"));
52            }
53        }
54    }
55
56    if !props.class.is_empty() {
57        classes.push(props.class.clone());
58    }
59
60    let full_class = classes.join(" ");
61
62    rsx! {
63        nav { class: "{full_class}",
64            div { class: "container-fluid",
65                if let Some(brand) = props.brand {
66                    {brand}
67                }
68                {props.children}
69            }
70        }
71    }
72}
73
74/// Navbar toggler button (hamburger menu) for responsive collapse.
75///
76/// ```rust
77/// rsx! {
78///     NavbarToggler { collapsed: collapsed_signal }
79/// }
80/// ```
81#[derive(Clone, PartialEq, Props)]
82pub struct NavbarTogglerProps {
83    /// Signal to toggle — will invert the value on click.
84    pub collapsed: Signal<bool>,
85    /// Additional CSS classes.
86    #[props(default)]
87    pub class: String,
88}
89
90#[component]
91pub fn NavbarToggler(props: NavbarTogglerProps) -> Element {
92    let is_collapsed = *props.collapsed.read();
93    let mut signal = props.collapsed;
94
95    let full_class = if props.class.is_empty() {
96        "navbar-toggler".to_string()
97    } else {
98        format!("navbar-toggler {}", props.class)
99    };
100
101    rsx! {
102        button {
103            class: "{full_class}",
104            r#type: "button",
105            "aria-expanded": if !is_collapsed { "true" } else { "false" },
106            "aria-label": "Toggle navigation",
107            onclick: move |_| signal.set(!is_collapsed),
108            span { class: "navbar-toggler-icon" }
109        }
110    }
111}
112
113/// Navbar collapsible content area.
114///
115/// ```rust
116/// let collapsed = use_signal(|| true);
117/// rsx! {
118///     NavbarToggler { collapsed: collapsed }
119///     NavbarCollapse { collapsed: collapsed,
120///         NavItem { NavLink { href: "/", "Home" } }
121///     }
122/// }
123/// ```
124#[derive(Clone, PartialEq, Props)]
125pub struct NavbarCollapseProps {
126    /// Signal controlling collapsed state.
127    pub collapsed: Signal<bool>,
128    /// Additional CSS classes.
129    #[props(default)]
130    pub class: String,
131    /// Child elements.
132    pub children: Element,
133}
134
135#[component]
136pub fn NavbarCollapse(props: NavbarCollapseProps) -> Element {
137    let is_collapsed = *props.collapsed.read();
138    let show = if !is_collapsed { " show" } else { "" };
139
140    let full_class = if props.class.is_empty() {
141        format!("collapse navbar-collapse{show}")
142    } else {
143        format!("collapse navbar-collapse{show} {}", props.class)
144    };
145
146    rsx! {
147        div { class: "{full_class}",
148            ul { class: "navbar-nav me-auto mb-2 mb-lg-0",
149                {props.children}
150            }
151        }
152    }
153}
154
155/// Bootstrap NavItem component.
156#[derive(Clone, PartialEq, Props)]
157pub struct NavItemProps {
158    /// Additional CSS classes.
159    #[props(default)]
160    pub class: String,
161    /// Child elements (NavLink).
162    pub children: Element,
163}
164
165#[component]
166pub fn NavItem(props: NavItemProps) -> Element {
167    let full_class = if props.class.is_empty() {
168        "nav-item".to_string()
169    } else {
170        format!("nav-item {}", props.class)
171    };
172
173    rsx! {
174        li { class: "{full_class}", {props.children} }
175    }
176}
177
178/// Bootstrap NavLink component.
179///
180/// ```rust
181/// rsx! {
182///     NavLink { href: "/dashboard", active: true, "Dashboard" }
183/// }
184/// ```
185#[derive(Clone, PartialEq, Props)]
186pub struct NavLinkProps {
187    /// Link href.
188    #[props(default = "#".to_string())]
189    pub href: String,
190    /// Active state.
191    #[props(default)]
192    pub active: bool,
193    /// Disabled state.
194    #[props(default)]
195    pub disabled: bool,
196    /// Click event handler.
197    #[props(default)]
198    pub onclick: Option<EventHandler<MouseEvent>>,
199    /// Additional CSS classes.
200    #[props(default)]
201    pub class: String,
202    /// Child elements.
203    pub children: Element,
204}
205
206#[component]
207pub fn NavLink(props: NavLinkProps) -> Element {
208    let mut classes = vec!["nav-link".to_string()];
209    if props.active {
210        classes.push("active".to_string());
211    }
212    if props.disabled {
213        classes.push("disabled".to_string());
214    }
215    if !props.class.is_empty() {
216        classes.push(props.class.clone());
217    }
218    let full_class = classes.join(" ");
219
220    rsx! {
221        a {
222            class: "{full_class}",
223            href: "{props.href}",
224            "aria-current": if props.active { "page" } else { "" },
225            onclick: move |evt| {
226                if let Some(handler) = &props.onclick {
227                    handler.call(evt);
228                }
229            },
230            {props.children}
231        }
232    }
233}