yew_nested_router/components/
link.rs

1use crate::prelude::{use_router, Target};
2use crate::state::State;
3use gloo_events::{EventListener, EventListenerOptions};
4use web_sys::HtmlElement;
5use yew::prelude::*;
6
7/// Properties for the [`Link`] component.
8#[derive(Clone, Debug, PartialEq, Properties)]
9pub struct LinkProperties<T>
10where
11    T: Target,
12{
13    /// Its content, rendered inside the element.
14    #[prop_or_default]
15    pub children: Html,
16
17    #[prop_or_default]
18    pub id: Option<AttrValue>,
19
20    /// The link target route.
21    pub to: T,
22
23    /// A state to push, if present
24    #[prop_or_default]
25    pub state: State,
26
27    #[prop_or_default]
28    pub any: bool,
29
30    #[prop_or_default]
31    pub predicate: Option<Callback<T, bool>>,
32
33    /// The element to render, default to `<a>`.
34    #[prop_or_else(default::element)]
35    pub element: String,
36
37    /// Suppress rendering the "href" attribute in any case.
38    #[prop_or_default]
39    pub suppress_href: bool,
40
41    /// Suppress rendering the state to the "hash" of the "href".
42    #[prop_or_default]
43    pub suppress_hash: bool,
44
45    /// CSS classes which are always present.
46    #[prop_or_default]
47    pub class: Classes,
48
49    /// CSS classes which are added when the target is the active route.
50    #[prop_or_default]
51    pub active: Classes,
52
53    /// CSS classes which are added when the target is not the active route.
54    #[prop_or_default]
55    pub inactive: Classes,
56}
57
58mod default {
59    pub fn element() -> String {
60        "a".to_string()
61    }
62}
63
64/// A link component, navigating to a [`Target`] on the `onclick` event.
65#[function_component(Link)]
66pub fn link<T>(props: &LinkProperties<T>) -> Html
67where
68    T: Target + 'static,
69{
70    let router = use_router::<T>().expect("Need Router or Nested component");
71
72    let mut class = props.class.clone();
73
74    let active = match props.any {
75        true => {
76            let active = router.active();
77            active.is_some()
78        }
79        false => match &props.predicate {
80            Some(predicate) => router
81                .active()
82                .clone()
83                .map(|t| predicate.emit(t))
84                .unwrap_or(false),
85            None => router.is_same(&props.to),
86        },
87    };
88
89    match active {
90        true => class.extend(props.active.clone()),
91        false => class.extend(props.inactive.clone()),
92    }
93
94    let href = match props.element.as_str() {
95        "a" if !props.suppress_href => {
96            Some(router.render_target_with(props.to.clone(), props.state.clone()))
97        }
98        _ => None,
99    };
100
101    let node_ref = use_node_ref();
102
103    use_effect_with(
104        (
105            router,
106            props.to.clone(),
107            props.state.clone(),
108            node_ref.clone(),
109        ),
110        |(router, to, state, node_ref)| {
111            let mut listener = None;
112
113            if let Some(element) = node_ref.cast::<HtmlElement>() {
114                let router = router.clone();
115                let to = to.clone();
116                let state = state.clone();
117                listener = Some(EventListener::new_with_options(
118                    &element,
119                    "click",
120                    EventListenerOptions::enable_prevent_default(),
121                    move |e| {
122                        e.prevent_default();
123                        router.push_with(to.clone(), state.clone());
124                    },
125                ));
126            }
127
128            move || drop(listener)
129        },
130    );
131
132    html!(
133        <@{props.element.clone()}
134            {class}
135            {href}
136            ref={node_ref}
137            id={props.id.clone()}
138        >
139            { props.children.clone() }
140        </@>
141    )
142}