1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use crate::prelude::{use_router, Target};
use crate::state::State;
use gloo_events::{EventListener, EventListenerOptions};
use web_sys::HtmlElement;
use yew::prelude::*;

/// Properties for the [`Link`] component.
#[derive(Clone, Debug, PartialEq, Properties)]
pub struct LinkProperties<T>
where
    T: Target,
{
    /// Its content, rendered inside the element.
    #[prop_or_default]
    pub children: Html,

    #[prop_or_default]
    pub id: Option<AttrValue>,

    /// The link target.
    pub target: T,

    /// A state to push, if present
    #[prop_or_default]
    pub state: State,

    #[prop_or_default]
    pub any: bool,

    #[prop_or_default]
    pub predicate: Option<Callback<T, bool>>,

    /// The element to render, default to `<a>`.
    #[prop_or_else(default::element)]
    pub element: String,

    /// Suppress rendering the "href" attribute in any case.
    #[prop_or_default]
    pub suppress_href: bool,

    /// Suppress rendering the state to the "hash" of the "href".
    #[prop_or_default]
    pub suppress_hash: bool,

    /// CSS classes which are always present.
    #[prop_or_default]
    pub class: Classes,

    /// CSS classes which are added when the target is the active route.
    #[prop_or_default]
    pub active: Classes,

    /// CSS classes which are added when the target is not the active route.
    #[prop_or_default]
    pub inactive: Classes,
}

mod default {
    pub fn element() -> String {
        "a".to_string()
    }
}

/// A link component, navigating to a [`Target`] on the `onclick` event.
#[function_component(Link)]
pub fn link<T>(props: &LinkProperties<T>) -> Html
where
    T: Target + 'static,
{
    let router = use_router::<T>().expect("Need Router or Nested component");

    let mut class = props.class.clone();

    let active = match props.any {
        true => {
            let active = router.active();
            active.is_some()
        }
        false => match &props.predicate {
            Some(predicate) => router
                .active()
                .clone()
                .map(|t| predicate.emit(t))
                .unwrap_or(false),
            None => router.is_same(&props.target),
        },
    };

    match active {
        true => class.extend(props.active.clone()),
        false => class.extend(props.inactive.clone()),
    }

    let href = match props.element.as_str() {
        "a" if !props.suppress_href => {
            Some(router.render_target_with(props.target.clone(), props.state.clone()))
        }
        _ => None,
    };

    let node_ref = use_node_ref();

    use_effect_with(
        (
            router,
            props.target.clone(),
            props.state.clone(),
            node_ref.clone(),
        ),
        |(router, target, state, node_ref)| {
            let mut listener = None;

            if let Some(element) = node_ref.cast::<HtmlElement>() {
                let router = router.clone();
                let target = target.clone();
                let state = state.clone();
                listener = Some(EventListener::new_with_options(
                    &element,
                    "click",
                    EventListenerOptions::enable_prevent_default(),
                    move |e| {
                        e.prevent_default();
                        router.push_with(target.clone(), state.clone());
                    },
                ));
            }

            move || drop(listener)
        },
    );

    html!(
        <@{props.element.clone()}
            {class}
            {href}
            ref={node_ref}
            id={props.id.clone()}
        >
            { props.children.clone() }
        </@>
    )
}