Skip to main content

euv_ui/component/router/view/
impl.rs

1use super::*;
2
3/// Implementation of route configuration construction.
4impl EuvRouteConfig {
5    /// Creates a new route configuration.
6    ///
7    /// # Arguments
8    ///
9    /// - `&'static str` - The route path.
10    /// - `F: Fn() -> VirtualNode + 'static` - The component function.
11    ///
12    /// # Returns
13    ///
14    /// - `EuvRouteConfig` - The route configuration.
15    pub fn new<F>(path: &'static str, component: F) -> Self
16    where
17        F: Fn() -> VirtualNode + 'static,
18    {
19        Self {
20            path,
21            component: Rc::new(component),
22        }
23    }
24}
25
26/// Implementation of router navigation and viewport utilities.
27impl Router {
28    /// Reads the current hash-based route from the browser URL.
29    ///
30    /// # Returns
31    ///
32    /// - `String` - The hash fragment without the leading `#`, or `DEFAULT_ROUTE_PATH` if empty.
33    pub fn current_route() -> String {
34        let Some(window) = window() else {
35            return String::new();
36        };
37        let hash: String = window.location().hash().unwrap_or_default();
38        let route: String = hash
39            .strip_prefix(ROUTE_HASH_PREFIX)
40            .unwrap_or(&hash)
41            .to_string();
42        if route.is_empty() {
43            DEFAULT_ROUTE_PATH.to_string()
44        } else {
45            route
46        }
47    }
48
49    /// Navigates to a new hash-based route.
50    ///
51    /// Always defers the actual `location.set_hash()` call to `queueMicrotask`
52    /// to prevent synchronous `hashchange` dispatch while any caller frame is still
53    /// on the stack. This avoids wasm_bindgen's `"closure invoked recursively
54    /// or after being dropped"` error which occurs when `set_hash()` fires
55    /// `hashchange` synchronously and the handler (or the reactive update chain
56    /// it triggers) calls `navigate()` again before the original dispatch finishes.
57    ///
58    /// When the target route matches the current route, the call is a no-op —
59    /// the route signal subscriber ignores equal values (`Signal::set` short-
60    /// circuits on `PartialEq`), but the underlying `set_hash` would still push
61    /// a duplicate history entry, fire `hashchange`, and re-run the route
62    /// subscriber's re-render path. Skipping the call avoids all of that.
63    ///
64    /// Multiple rapid `navigate()` calls before the microtask fires are coalesced:
65    /// only the **last** target route wins, as earlier routes were superseded by
66    ///
67    /// # Arguments
68    ///
69    /// - `R: AsRef<str>` - The target route path.
70    pub fn navigate<R>(route: R)
71    where
72        R: AsRef<str>,
73    {
74        let route_string: String = route.as_ref().to_string();
75        // Cheap early-return: a navigate() to the route the user is already on
76        // would otherwise queue a microtask, mutate `location.hash`, dispatch
77        // `hashchange`, and run the route subscriber's patch path — for no
78        // observable effect. Strips the optional `#anchor` suffix so callers
79        // can compare against the URL hash form without normalizing themselves.
80        let current: String = Self::current_route();
81        let target_path: &str = match route_string.find('#') {
82            Some(idx) => &route_string[..idx],
83            None => &route_string,
84        };
85        if current == target_path {
86            return;
87        }
88        DEFERRED_NAVIGATION.with(|cell: &Cell<Option<String>>| cell.set(Some(route_string)));
89        let deferred_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
90            let target_route: Option<String> =
91                DEFERRED_NAVIGATION.with(|cell: &Cell<Option<String>>| cell.take());
92            if let Some(route_value) = target_route {
93                let Some(nav_window) = web_sys::window() else {
94                    return;
95                };
96                let nav_location: Location = nav_window.location();
97                let nav_new_hash: String = format!("{ROUTE_HASH_PREFIX}{route_value}");
98                let _: Result<(), JsValue> = nav_location.set_hash(&nav_new_hash);
99            }
100        }));
101        let Some(window) = window() else {
102            return;
103        };
104        window.queue_microtask(deferred_closure.as_ref().unchecked_ref::<Function>());
105        deferred_closure.forget();
106    }
107
108    /// Creates a link click handler that navigates to the given route.
109    ///
110    /// Calls `event.prevent_default()` to prevent the `<a>` element's
111    /// default hash navigation, then programmatically navigates via
112    /// `navigate()`. Without `preventDefault`, both the `<a href>` default
113    /// behavior and `navigate()` would fire, potentially creating duplicate
114    /// history entries and causing incorrect browser back/forward behavior.
115    ///
116    /// # Arguments
117    ///
118    /// - `R: AsRef<str>` - The target route path.
119    ///
120    /// # Returns
121    ///
122    /// - `NativeEventHandler` - An event handler for click events.
123    pub fn link_handler<R>(route: R) -> NativeEventHandler
124    where
125        R: AsRef<str>,
126    {
127        let route_string: String = route.as_ref().to_string();
128        NativeEventHandler::create("click", move |event: Event| {
129            event.prevent_default();
130            Self::navigate(&route_string);
131        })
132    }
133
134    /// Checks whether the current viewport width qualifies as a mobile device.
135    ///
136    /// Uses `MOBILE_BREAKPOINT` (768px) as the threshold.
137    ///
138    /// # Returns
139    ///
140    /// - `bool` - `true` if the viewport width is less than the mobile breakpoint.
141    pub fn is_mobile() -> bool {
142        let Some(window) = window() else {
143            return false;
144        };
145        let width: f64 = window
146            .inner_width()
147            .ok()
148            .map(|value: JsValue| Number::from(value).value_of())
149            .unwrap_or_default();
150        width < MOBILE_BREAKPOINT as f64
151    }
152}