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}