yew_nav_link/
nav_link.rs

1//! Navigation link component with automatic active state detection.
2//!
3//! This module provides the [`NavLink`] component and [`nav_link`] helper
4//! function for building navigation menus in Yew applications. The component
5//! automatically detects when its target route matches the current URL and
6//! applies an `active` CSS class accordingly.
7//!
8//! # Features
9//!
10//! - **Automatic Active State**: Compares the target route against the current
11//!   route and applies the `active` class when they match.
12//! - **Type-Safe Routing**: Leverages Yew Router's [`Routable`] trait for
13//!   compile-time route validation.
14//! - **Flexible Children**: Accepts any valid Yew children, including text,
15//!   HTML elements, or other components.
16//! - **CSS Integration**: Renders with `nav-link` base class, compatible with
17//!   Bootstrap and similar CSS frameworks.
18//!
19//! # CSS Classes
20//!
21//! The component applies the following CSS classes to the rendered `<a>`
22//! element:
23//!
24//! | Class | Condition |
25//! |-------|-----------|
26//! | `nav-link` | Always applied |
27//! | `active` | Applied when the target route matches the current route |
28//!
29//! # Usage
30//!
31//! ## Component Syntax
32//!
33//! ```rust
34//! use yew::prelude::*;
35//! use yew_nav_link::NavLink;
36//! use yew_router::prelude::*;
37//!
38//! #[derive(Clone, PartialEq, Debug, Routable)]
39//! enum Route {
40//!     #[at("/")]
41//!     Home,
42//!     #[at("/about")]
43//!     About
44//! }
45//!
46//! #[component]
47//! fn Navigation() -> Html {
48//!     html! {
49//!         <nav>
50//!             <NavLink<Route> to={Route::Home}>{ "Home" }</NavLink<Route>>
51//!             <NavLink<Route> to={Route::About}>{ "About" }</NavLink<Route>>
52//!         </nav>
53//!     }
54//! }
55//! ```
56//!
57//! ## Function Syntax
58//!
59//! For simpler cases with text-only children:
60//!
61//! ```rust
62//! use yew::prelude::*;
63//! use yew_nav_link::nav_link;
64//! use yew_router::prelude::*;
65//!
66//! #[derive(Clone, PartialEq, Debug, Routable)]
67//! enum Route {
68//!     #[at("/")]
69//!     Home,
70//!     #[at("/about")]
71//!     About
72//! }
73//!
74//! #[component]
75//! fn Navigation() -> Html {
76//!     html! {
77//!         <ul class="nav">
78//!             <li>{ nav_link(Route::Home, "Home") }</li>
79//!             <li>{ nav_link(Route::About, "About") }</li>
80//!         </ul>
81//!     }
82//! }
83//! ```
84//!
85//! # Integration with CSS Frameworks
86//!
87//! The component works seamlessly with Bootstrap, Tailwind, and other CSS
88//! frameworks that use the `.nav-link` and `.active` class conventions:
89//!
90//! ```html
91//! <!-- Bootstrap Navigation -->
92//! <ul class="nav nav-pills">
93//!     <li class="nav-item">
94//!         <!-- NavLink renders: <a class="nav-link active" href="/"> -->
95//!     </li>
96//! </ul>
97//! ```
98
99use std::marker::PhantomData;
100
101use yew::prelude::*;
102use yew_router::prelude::*;
103
104/// Properties for the [`NavLink`] component.
105///
106/// # Type Parameters
107///
108/// * `R` - A type implementing [`Routable`] that defines the target route.
109#[derive(Properties, PartialEq, Debug)]
110pub struct NavLinkProps<R: Routable + PartialEq + Clone + 'static> {
111    /// Target route for navigation.
112    ///
113    /// When clicked, the application navigates to this route.
114    /// The component compares this value against the current route
115    /// to determine active state.
116    pub to: R,
117
118    /// Content rendered inside the link element.
119    ///
120    /// Accepts any valid Yew children: text, HTML elements, or components.
121    pub children: Children,
122
123    #[prop_or_default]
124    pub(crate) _marker: PhantomData<R>
125}
126
127/// Navigation link with automatic active state detection.
128///
129/// Wraps Yew Router's [`Link`] component and automatically applies the `active`
130/// CSS class when the target route matches the current URL.
131///
132/// # CSS Classes
133///
134/// - `nav-link` - Always applied
135/// - `active` - Applied when route matches current URL
136///
137/// # Type Parameters
138///
139/// * `R` - Route type implementing [`Routable`]
140///
141/// # Example
142///
143/// ```rust
144/// use yew::prelude::*;
145/// use yew_nav_link::NavLink;
146/// use yew_router::prelude::*;
147///
148/// #[derive(Clone, PartialEq, Routable)]
149/// enum Route {
150///     #[at("/")]
151///     Home,
152///     #[at("/about")]
153///     About
154/// }
155///
156/// #[component]
157/// fn Navigation() -> Html {
158///     html! {
159///         <nav>
160///             <NavLink<Route> to={Route::Home}>{ "Home" }</NavLink<Route>>
161///             <NavLink<Route> to={Route::About}>{ "About" }</NavLink<Route>>
162///         </nav>
163///     }
164/// }
165/// ```
166#[component]
167pub fn NavLink<R: Routable + PartialEq + Clone + 'static>(props: &NavLinkProps<R>) -> Html {
168    let current_route = use_route::<R>();
169    let is_active = current_route.is_some_and(|route| route == props.to);
170    let class = build_class(is_active);
171    html! {
172        <Link<R> to={props.to.clone()} classes={classes!(class)}>
173            for child in props.children.iter() {
174                { child }
175            }
176        </Link<R>>
177    }
178}
179
180/// Creates a NavLink component for the specified route with the provided
181/// children.
182///
183/// This function creates a NavLink component for Yew applications using Yew
184/// Router. It takes a route (`R`) and children text, and returns a NavLink
185/// component.
186///
187/// # Arguments
188///
189/// * `to` - The destination route for the link.
190/// * `children` - The text or other elements to be rendered within the link.
191///
192/// # Example
193///
194/// ```rust
195/// use yew::prelude::*;
196/// use yew_nav_link::{NavLink, nav_link};
197/// use yew_router::prelude::*;
198///
199/// #[derive(Clone, PartialEq, Debug, Routable)]
200/// enum HomeRoute {
201///     #[at("/")]
202///     IntroPage,
203///     #[at("/about")]
204///     About
205/// }
206///
207/// #[component]
208/// fn Menu() -> Html {
209///     html! {
210///         <ul class="nav">
211///             // Creating a NavLink for the Home route with the text "Home Page"
212///             <li class="nav-item">
213///                 { nav_link(HomeRoute::IntroPage, "Home Page") }
214///             </li>
215///             <li class="nav-item">
216///                 { nav_link(HomeRoute::About, "About") }
217///             </li>
218///         </ul>
219///     }
220/// }
221/// ```
222///
223/// # Generic Type
224///
225/// * `R` - The route type that implements the `Routable` trait.
226///
227/// # Returns
228///
229/// An HTML representation of the NavLink component.
230///
231/// # Note
232///
233/// The `to` parameter must be of a type that implements the `Routable` trait.
234pub fn nav_link<R: Routable + PartialEq + Clone + 'static>(to: R, children: &str) -> Html {
235    html! {
236        <NavLink<R> to={to}>{ Html::from(children) }</NavLink<R>>
237    }
238}
239
240/// Generates CSS class string based on active state.
241#[inline]
242fn build_class(is_active: bool) -> String {
243    if is_active {
244        "nav-link active".to_string()
245    } else {
246        "nav-link".to_string()
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[derive(Clone, PartialEq, Debug, Routable)]
255    enum TestRoute {
256        #[at("/")]
257        Home,
258        #[at("/about")]
259        About
260    }
261
262    #[test]
263    fn build_class_active() {
264        assert_eq!(build_class(true), "nav-link active");
265    }
266
267    #[test]
268    fn build_class_inactive() {
269        assert_eq!(build_class(false), "nav-link");
270    }
271
272    #[test]
273    fn props_equality_same_route() {
274        let props1: NavLinkProps<TestRoute> = NavLinkProps {
275            to:       TestRoute::Home,
276            children: Default::default(),
277            _marker:  PhantomData
278        };
279        let props2: NavLinkProps<TestRoute> = NavLinkProps {
280            to:       TestRoute::Home,
281            children: Default::default(),
282            _marker:  PhantomData
283        };
284        assert_eq!(props1, props2);
285    }
286
287    #[test]
288    fn props_equality_different_routes() {
289        let props1: NavLinkProps<TestRoute> = NavLinkProps {
290            to:       TestRoute::Home,
291            children: Default::default(),
292            _marker:  PhantomData
293        };
294        let props2: NavLinkProps<TestRoute> = NavLinkProps {
295            to:       TestRoute::About,
296            children: Default::default(),
297            _marker:  PhantomData
298        };
299        assert_ne!(props1, props2);
300    }
301
302    #[test]
303    fn props_debug_impl() {
304        let props: NavLinkProps<TestRoute> = NavLinkProps {
305            to:       TestRoute::Home,
306            children: Default::default(),
307            _marker:  PhantomData
308        };
309        let debug_str = format!("{:?}", props);
310        assert!(debug_str.contains("NavLinkProps"));
311        assert!(debug_str.contains("Home"));
312    }
313
314    #[test]
315    fn nav_link_fn_returns_html() {
316        let html = nav_link(TestRoute::Home, "Home");
317        assert!(matches!(html, Html::VComp(_)));
318    }
319
320    #[test]
321    fn nav_link_fn_different_routes() {
322        let html1 = nav_link(TestRoute::Home, "Home");
323        let html2 = nav_link(TestRoute::About, "About");
324        assert!(matches!(html1, Html::VComp(_)));
325        assert!(matches!(html2, Html::VComp(_)));
326    }
327
328    #[test]
329    fn nav_link_fn_empty_text() {
330        let html = nav_link(TestRoute::Home, "");
331        assert!(matches!(html, Html::VComp(_)));
332    }
333
334    #[test]
335    fn nav_link_fn_long_text() {
336        let html = nav_link(TestRoute::Home, "This is a very long navigation link text");
337        assert!(matches!(html, Html::VComp(_)));
338    }
339
340    #[test]
341    fn route_equality() {
342        assert_eq!(TestRoute::Home, TestRoute::Home);
343        assert_ne!(TestRoute::Home, TestRoute::About);
344    }
345}