1use crate::base;
2use crate::history::{History, HistoryListener};
3use crate::scope::{NavigationTarget, ScopeContext};
4use crate::state::State;
5use crate::target::Target;
6use gloo_utils::window;
7use std::borrow::Cow;
8use std::fmt::Debug;
9use std::rc::Rc;
10use web_sys::Location;
11use yew::html::IntoPropValue;
12use yew::prelude::*;
13
14#[derive(Clone, PartialEq)]
15pub struct RouterContext<T>
16where
17 T: Target,
18{
19 pub(crate) base: Rc<String>,
21 pub(crate) scope: Rc<ScopeContext<T>>,
22 pub active_target: Option<T>,
24}
25
26impl<T> RouterContext<T>
27where
28 T: Target,
29{
30 pub fn push(&self, target: T) {
32 self.scope.push(target);
33 }
34 pub fn replace(&self, target: T) {
36 self.scope.replace(target);
37 }
38
39 pub fn push_with(&self, target: T, state: State) {
41 self.scope.push_with(target, state.0);
42 }
43 pub fn replace_with(&self, target: T, state: State) {
45 self.scope.replace_with(target, state.0);
46 }
47
48 pub fn render_target(&self, target: T) -> String {
52 self.scope.collect(target)
53 }
54
55 pub fn render_target_with(&self, target: T, state: impl IntoPropValue<State>) -> String {
60 let mut result = self.scope.collect(target);
61
62 let state = state.into_prop_value().0;
63 if state.is_null() || state.is_undefined() {
64 } else if let Some(value) = state.as_string() {
66 result.push('#');
67 result.push_str(&value);
68 } else if let Some(value) = js_sys::JSON::stringify(&state)
69 .ok()
70 .and_then(|s| s.as_string())
71 {
72 result.push('#');
73 result.push_str(&value);
74 }
75
76 result
77 }
78
79 pub fn is_same(&self, target: &T) -> bool {
81 match &self.active_target {
82 Some(current) => current == target,
83 None => false,
84 }
85 }
86
87 pub fn is_active(&self, target: &T, predicate: Option<&Callback<T, bool>>) -> bool {
102 match predicate {
103 Some(predicate) => self
104 .active_target
105 .clone()
106 .map(|target| predicate.emit(target))
107 .unwrap_or_default(),
108 None => self.is_same(target),
109 }
110 }
111
112 pub fn active(&self) -> &Option<T> {
115 &self.active_target
116 }
117}
118
119#[derive(Clone, Debug, PartialEq, Properties)]
121pub struct RouterProps<T>
122where
123 T: Target,
124{
125 #[prop_or_default]
127 pub children: Children,
128
129 #[prop_or_default]
131 pub default: Option<T>,
132
133 #[prop_or_default]
154 pub base: Option<String>,
155}
156
157#[derive(Debug, Copy, Clone, Eq, PartialEq)]
158pub enum StackOperation {
159 Push,
160 Replace,
161}
162
163#[derive(Debug)]
164#[doc(hidden)]
165pub enum Msg<T: Target> {
166 RouteChanged,
170 ChangeTarget(NavigationTarget<T>, StackOperation),
172}
173
174pub struct Router<T: Target> {
176 _listener: HistoryListener,
177 target: Option<T>,
178
179 scope: Rc<ScopeContext<T>>,
180 router: RouterContext<T>,
181
182 base: Rc<String>,
183}
184
185impl<T> Component for Router<T>
186where
187 T: Target + 'static,
188{
189 type Message = Msg<T>;
190 type Properties = RouterProps<T>;
191
192 fn create(ctx: &Context<Self>) -> Self {
193 let cb = ctx.link().callback(|_| Msg::RouteChanged);
194
195 let base = Rc::new(
196 ctx.props()
197 .base
198 .clone()
199 .or_else(base::eval_base)
200 .unwrap_or_default(),
201 );
202
203 let target = Self::parse_location(&base, window().location())
204 .or_else(|| ctx.props().default.clone());
205
206 let listener = History::listener(move || {
207 cb.emit(window().location());
208 });
209
210 let (scope, router) = Self::build_context(base.clone(), &target, ctx);
211
212 Self {
213 _listener: listener,
214 target,
215 scope,
216 router,
217 base,
218 }
219 }
220
221 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
222 match msg {
223 Msg::RouteChanged => {
224 let target = Self::parse_location(&self.base, window().location())
225 .or_else(|| ctx.props().default.clone());
226 if target != self.target {
227 self.target = target;
228 self.sync_context(ctx);
229 return true;
230 }
231 }
232 Msg::ChangeTarget(target, operation) => {
233 let route = Self::render_target(&self.base, &target.target);
234 let _ = match operation {
235 StackOperation::Push => History::push_state(target.state, &route),
236 StackOperation::Replace => History::replace_state(target.state, &route),
237 };
238 ctx.link().send_message(Msg::RouteChanged)
239 }
240 }
241
242 false
243 }
244
245 fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
246 self.sync_context(ctx);
247 true
248 }
249
250 fn view(&self, ctx: &Context<Self>) -> Html {
251 let scope = self.scope.clone();
252 let router = self.router.clone();
253
254 html! (
255 <ContextProvider<ScopeContext<T>> context={(*scope).clone()}>
256 <ContextProvider<RouterContext<T >> context={router}>
257 { for ctx.props().children.iter() }
258 </ContextProvider<RouterContext<T >>>
259 </ContextProvider<ScopeContext<T>>>
260 )
261 }
262}
263
264impl<T: Target> Router<T> {
265 fn render_target(base: &str, target: &T) -> String {
266 let path = target
267 .render_path()
268 .into_iter()
269 .map(|segment| urlencoding::encode(&segment).to_string())
270 .collect::<Vec<_>>()
271 .join("/");
272
273 format!("{base}/{path}",)
274 }
275
276 fn parse_location(base: &str, location: Location) -> Option<T> {
277 let path = location.pathname().unwrap_or_default();
279 if !path.starts_with(base) {
281 return None;
282 }
283 let (_, path) = path.split_at(base.len());
285 let path: Result<Vec<Cow<str>>, _> = path
289 .split('/')
290 .skip(1)
291 .map(urlencoding::decode)
293 .collect();
294
295 let path = match &path {
297 Ok(path) => path.iter().map(|s| s.as_ref()).collect::<Vec<_>>(),
298 Err(_) => return None,
299 };
300
301 T::parse_path(&path)
303 }
304
305 fn sync_context(&mut self, ctx: &Context<Self>) {
306 let (scope, router) = Self::build_context(self.base.clone(), &self.target, ctx);
307 self.scope = scope;
308 self.router = router;
309 }
310
311 fn build_context(
312 base: Rc<String>,
313 target: &Option<T>,
314 ctx: &Context<Self>,
315 ) -> (Rc<ScopeContext<T>>, RouterContext<T>) {
316 let scope = Rc::new(ScopeContext {
317 upwards: ctx
318 .link()
319 .callback(|(target, operation)| Msg::ChangeTarget(target, operation)),
320 collect: {
321 let base = base.clone();
322 Callback::from(move |target| Self::render_target(&base, &target))
323 },
324 });
325
326 let router = RouterContext {
327 base,
328 scope: scope.clone(),
329 active_target: target.clone(),
330 };
331
332 (scope, router)
333 }
334}
335
336#[hook]
337pub fn use_router<T>() -> Option<RouterContext<T>>
342where
343 T: Target + 'static,
344{
345 use_context()
346}