From 84ebdc1b926e54bcf3bab7057c4cb0bebb9baf3b Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Wed, 6 Mar 2024 13:41:40 -0500 Subject: [PATCH] get basic routing working --- examples/router/src/lib.rs | 78 +++++++--- examples/router/src/main.rs | 4 - reactive_graph/src/owner.rs | 12 ++ routing/Cargo.toml | 4 + routing/src/router.rs | 233 ++++++++++++++++++++++++++--- routing_utils/src/lib.rs | 18 ++- routing_utils/src/nested/mod.rs | 34 +++-- routing_utils/src/nested/tuples.rs | 108 +++++++++---- tachys/src/view/either.rs | 2 +- 9 files changed, 408 insertions(+), 85 deletions(-) diff --git a/examples/router/src/lib.rs b/examples/router/src/lib.rs index e0fb2d2b3..be8fb9d66 100644 --- a/examples/router/src/lib.rs +++ b/examples/router/src/lib.rs @@ -12,7 +12,7 @@ use leptos::{ use log::{debug, info}; use routing::{ location::{BrowserUrl, Location}, - NestedRoute, Router, Routes, StaticSegment, + NestedRoute, ParamSegment, Router, Routes, StaticSegment, }; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -28,6 +28,25 @@ pub fn RouterExample() -> impl IntoView { let router = Router::new( BrowserUrl::new().unwrap(), Routes::new(( + NestedRoute { + segments: StaticSegment(""), + children: ( + NestedRoute { + segments: ParamSegment("id"), + children: (), + data: (), + view: Contact, + }, + NestedRoute { + segments: StaticSegment(""), + children: (), + data: (), + view: || "Select a contact.", + }, + ), + data: (), + view: ContactList, + }, NestedRoute { segments: StaticSegment("settings"), children: (), @@ -122,19 +141,28 @@ pub fn ContactRoutes() -> impl IntoView { } }*/ -/* + #[component] pub fn ContactList() -> impl IntoView { - log::debug!("rendering "); + info!("rendering "); // contexts are passed down through the route tree provide_context(ExampleContext(42)); - on_cleanup(|| { + Owner::on_cleanup(|| { info!("cleaning up "); }); - let location = use_location(); + view! { +
+

"Contacts"

+
  • 1
  • +
  • 2
  • +
  • 3
  • +
    + } + + /*let location = use_location(); let contacts = create_resource(move || location.search.get(), get_contacts); let contacts = move || { contacts.get().map(|contacts| { @@ -162,14 +190,14 @@ pub fn ContactList() -> impl IntoView { intro="fadeIn" /> - } -}*/ + }*/ +} /* #[derive(Params, PartialEq, Clone, Debug)] pub struct ContactParams { // Params isn't implemented for usize, only Option id: Option, -} +}*/ #[component] pub fn Contact() -> impl IntoView { @@ -180,11 +208,17 @@ pub fn Contact() -> impl IntoView { use_context::() ); - on_cleanup(|| { + Owner::on_cleanup(|| { info!("cleaning up "); }); - let params = use_params::(); + view! { +
    +

    "Contact"

    +
    + } + + /* let params = use_params::(); let contact = create_resource( move || { params @@ -229,8 +263,8 @@ pub fn Contact() -> impl IntoView { {contact_display} - } -}*/ + }*/ +} #[component] pub fn About() -> impl IntoView { @@ -271,16 +305,14 @@ pub fn Settings() -> impl IntoView { }); view! { - <> -

    "Settings"

    -
    -
    - "Name" - - -
    -
    "This page is just a placeholder."
    -
    - +

    "Settings"

    +
    +
    + "Name" + + +
    +
    "This page is just a placeholder."
    +
    } } diff --git a/examples/router/src/main.rs b/examples/router/src/main.rs index 7ba4d5a4e..d97235770 100644 --- a/examples/router/src/main.rs +++ b/examples/router/src/main.rs @@ -6,13 +6,9 @@ use tracing_subscriber_wasm::MakeConsoleWriter; pub fn main() { fmt() .with_writer( - // To avoide trace events in the browser from showing their - // JS backtrace, which is very annoying, in my opinion MakeConsoleWriter::default() .map_trace_level_to(tracing::Level::DEBUG), ) - // For some reason, if we don't do this in the browser, we get - // a runtime error. .without_time() .init(); console_error_panic_hook::set_once(); diff --git a/reactive_graph/src/owner.rs b/reactive_graph/src/owner.rs index c25d824df..751f932cd 100644 --- a/reactive_graph/src/owner.rs +++ b/reactive_graph/src/owner.rs @@ -38,6 +38,18 @@ impl Owner { } } + pub fn child(&self) -> Self { + let parent = Some(Arc::downgrade(&self.inner)); + Self { + inner: Arc::new(RwLock::new(OwnerInner { + parent, + nodes: Default::default(), + contexts: Default::default(), + cleanups: Default::default(), + })), + } + } + pub fn with(&self, fun: impl FnOnce() -> T) -> T { let prev = { OWNER.with(|o| { diff --git a/routing/Cargo.toml b/routing/Cargo.toml index 0864ecf2f..e6e596103 100644 --- a/routing/Cargo.toml +++ b/routing/Cargo.toml @@ -13,6 +13,7 @@ url = "2" js-sys = { version = "0.3" } wasm-bindgen = { version = "0.2" } tracing = { version = "0.1", optional = true } +paste = "1.0.14" [dependencies.web-sys] version = "0.3" @@ -41,3 +42,6 @@ features = [ "RequestMode", "Response", ] + +[features] +tracing = ["dep:tracing", "routing_utils/tracing"] diff --git a/routing/src/router.rs b/routing/src/router.rs index 35c77704d..ca080ca62 100644 --- a/routing/src/router.rs +++ b/routing/src/router.rs @@ -1,13 +1,14 @@ -use crate::{generate_route_list::RouteList, location::Location}; +use crate::{generate_route_list::RouteList, location::Location, Params}; use core::marker::PhantomData; -use either_of::Either; +use either_of::*; use reactive_graph::{ computed::ArcMemo, effect::RenderEffect, + owner::Owner, traits::{Read, Track}, }; use routing_utils::{ - MatchInterface, MatchNestedRoutes, PossibleRouteMatch, Routes, + MatchInterface, MatchNestedRoutes, PossibleRouteMatch, RouteMatchId, Routes, }; use std::borrow::Cow; use tachys::{ @@ -16,8 +17,8 @@ use tachys::{ renderer::Renderer, ssr::StreamBuilder, view::{ - add_attr::AddAnyAttr, either::EitherState, Position, PositionState, - Render, RenderHtml, + add_attr::AddAnyAttr, either::EitherState, Mountable, Position, + PositionState, Render, RenderHtml, }, }; @@ -125,7 +126,11 @@ where Rndr: Renderer + 'static, { type State = RenderEffect< - EitherState>::State, Rndr>, + EitherState< + NestedRouteState, + >::State, + Rndr, + >, >; type FallibleState = (); @@ -136,27 +141,47 @@ where let url = url.clone(); move |_| url.read().path().to_string() }); - let search_parans = ArcMemo::new({ + let search_params = ArcMemo::new({ let url = url.clone(); move |_| url.read().search_params().clone() }); - RenderEffect::new(move |prev| { - tachys::dom::log(&format!("recalculating route")); + let outer_owner = + Owner::current().expect("creating Router, but no Owner was found"); + + RenderEffect::new(move |prev: Option>| { let path = path.read(); - let new_view = match self.routes.match_route(&*path) { - Some(matched) => { - let view = matched.to_view(); - let view = view.choose(); - Either::Left(view) - } - _ => Either::Right((self.fallback)()), - }; + let new_match = self.routes.match_route(&path); if let Some(mut prev) = prev { - new_view.rebuild(&mut prev); + if let Some(new_match) = new_match { + match &mut prev.state { + Either::Left(prev) => { + nested_rebuild(&outer_owner, prev, new_match); + } + Either::Right(_) => { + Either::<_, Fallback>::Left(NestedRouteView::new( + &outer_owner, + new_match, + )) + .rebuild(&mut prev); + } + } + } else { + Either::, _>::Right((self + .fallback)( + )) + .rebuild(&mut prev); + } prev } else { - new_view.build() + match new_match { + Some(matched) => Either::Left(NestedRouteView::new( + &outer_owner, + matched, + )), + _ => Either::Right((self.fallback)()), + } + .build() } }) } @@ -175,6 +200,142 @@ where } } +fn nested_rebuild<'a, NewMatch, R>( + outer_owner: &Owner, + current: &mut NestedRouteState< + <::Output as Render>::State, + >, + new: NewMatch, +) where + NewMatch: MatchInterface<'a>, + NewMatch::View: ChooseView, + ::Output: Render, + R: Renderer, +{ + // if the new match is a different branch of the nested route tree from the current one, we can + // just rebuild the view starting here: everything underneath it will change + if new.as_id() != current.id { + // TODO provide params + matched via context? + let new_view = NestedRouteView::new(&outer_owner, new); + let prev_owner = std::mem::replace(&mut current.owner, new_view.owner); + current.id = new_view.id; + current.params = new_view.params; + current.matched = new_view.matched; + current + .owner + .with(|| new_view.view.rebuild(&mut current.view)); + + // TODO is this the right place to drop the old Owner? + drop(prev_owner); + } else { + tracing::warn!("TODO: replace"); + // otherwise, we should recurse to the children of the current view, and the new match + //nested_rebuild(current.as_child_mut(), new.as_child()) + } + + // update params, in case they're different + // TODO +} + +pub struct NestedRouteView { + id: RouteMatchId, + owner: Owner, + params: Params, + matched: String, + view: View, +} + +impl NestedRouteView { + pub fn new<'a, Matcher>(outer_owner: &Owner, matched: Matcher) -> Self + where + Matcher: MatchInterface<'a>, + Matcher::View: ChooseView, + { + NestedRouteView { + id: matched.as_id(), + owner: outer_owner.child(), + params: matched + .to_params() + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + matched: matched.as_matched().to_string(), + view: matched.to_view().choose(), + } + } +} + +impl Render for NestedRouteView +where + View: Render, + R: Renderer, +{ + type State = NestedRouteState; + type FallibleState = (); + + fn build(self) -> Self::State { + let NestedRouteView { + id, + owner, + params, + matched, + view, + } = self; + NestedRouteState { + id, + owner, + params, + matched, + view: view.build(), + } + } + + fn rebuild(self, state: &mut Self::State) { + todo!() + } + + fn try_build(self) -> tachys::error::Result { + todo!() + } + + fn try_rebuild( + self, + state: &mut Self::FallibleState, + ) -> tachys::error::Result<()> { + todo!() + } +} + +pub struct NestedRouteState { + id: RouteMatchId, + owner: Owner, + params: Params, + matched: String, + view: ViewState, +} + +impl Mountable for NestedRouteState +where + ViewState: Mountable, + R: Renderer, +{ + fn unmount(&mut self) { + self.view.unmount(); + } + + fn mount(&mut self, parent: &R::Element, marker: Option<&R::Node>) { + self.view.mount(parent, marker); + } + + fn insert_before_this( + &self, + parent: &R::Element, + child: &mut dyn Mountable, + ) -> bool { + self.view.insert_before_this(parent, child) + } +} + impl RenderHtml for Router where @@ -239,6 +400,40 @@ where self } } + +macro_rules! tuples { + ($either:ident => $($ty:ident),*) => { + paste::paste! { + impl<$($ty, [],)*> ChooseView for $either<$([],)*> + where + $([]: Fn() -> $ty,)* + { + type Output = $either<$($ty,)*>; + + fn choose(self) -> Self::Output { + match self { + $($either::$ty(f) => $either::$ty(f()),)* + } + } + } + } + } +} + +tuples!(EitherOf3 => A, B, C); +tuples!(EitherOf4 => A, B, C, D); +tuples!(EitherOf5 => A, B, C, D, E); +tuples!(EitherOf6 => A, B, C, D, E, F); +tuples!(EitherOf7 => A, B, C, D, E, F, G); +tuples!(EitherOf8 => A, B, C, D, E, F, G, H); +tuples!(EitherOf9 => A, B, C, D, E, F, G, H, I); +tuples!(EitherOf10 => A, B, C, D, E, F, G, H, I, J); +tuples!(EitherOf11 => A, B, C, D, E, F, G, H, I, J, K); +tuples!(EitherOf12 => A, B, C, D, E, F, G, H, I, J, K, L); +tuples!(EitherOf13 => A, B, C, D, E, F, G, H, I, J, K, L, M); +tuples!(EitherOf14 => A, B, C, D, E, F, G, H, I, J, K, L, M, N); +tuples!(EitherOf15 => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); +tuples!(EitherOf16 => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); /* impl RenderHtml for Router diff --git a/routing_utils/src/lib.rs b/routing_utils/src/lib.rs index c68d9a0f3..7c0b96c72 100644 --- a/routing_utils/src/lib.rs +++ b/routing_utils/src/lib.rs @@ -60,12 +60,16 @@ where }; let (matched, remaining) = self.children.match_nested(path); + #[cfg(feature = "tracing")] + tracing::info!("matched = {:?}", matched.is_some()); let matched = matched?; if !remaining.is_empty() { + #[cfg(feature = "tracing")] + tracing::info!("did not match because remaining was {remaining:?}"); None } else { - Some(matched) + Some(matched.1) } } @@ -79,11 +83,18 @@ where } } +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct RouteMatchId(pub(crate) u8); + pub trait MatchInterface<'a> { type Params: IntoIterator; type Child; type View; + fn as_id(&self) -> RouteMatchId; + + fn as_matched(&self) -> &str; + fn to_params(&self) -> Self::Params; fn to_child(&'a self) -> Self::Child; @@ -95,7 +106,10 @@ pub trait MatchNestedRoutes<'a> { type Data; type Match: MatchInterface<'a>; - fn match_nested(&'a self, path: &'a str) -> (Option, &'a str); + fn match_nested( + &'a self, + path: &'a str, + ) -> (Option<(RouteMatchId, Self::Match)>, &'a str); fn generate_routes( &self, diff --git a/routing_utils/src/nested/mod.rs b/routing_utils/src/nested/mod.rs index 7e335b2d3..f8cd70c70 100644 --- a/routing_utils/src/nested/mod.rs +++ b/routing_utils/src/nested/mod.rs @@ -1,7 +1,7 @@ use super::{ MatchInterface, MatchNestedRoutes, PartialPathMatch, PossibleRouteMatch, }; -use crate::PathSegment; +use crate::{PathSegment, RouteMatchId}; use alloc::vec::Vec; use core::{fmt, iter}; @@ -17,6 +17,7 @@ pub struct NestedRoute { #[derive(PartialEq, Eq)] pub struct NestedMatch<'a, ParamsIter, Child, View> { + id: RouteMatchId, /// The portion of the full path matched only by this nested route. matched: &'a str, /// The map of params matched only by this nested route. @@ -51,6 +52,14 @@ where type Child = &'a Child; type View = &'a View; + fn as_id(&self) -> RouteMatchId { + self.id + } + + fn as_matched(&self) -> &str { + self.matched + } + fn to_params(&self) -> Self::Params { self.params.clone() } @@ -87,7 +96,10 @@ where <>::Params as IntoIterator>::IntoIter, >, Children::Match, View>; - fn match_nested(&'a self, path: &'a str) -> (Option, &'a str) { + fn match_nested( + &'a self, + path: &'a str, + ) -> (Option<(RouteMatchId, Self::Match)>, &'a str) { self.segments .test(path) .and_then( @@ -98,17 +110,21 @@ where }| { let (inner, remaining) = self.children.match_nested(remaining); - let inner = inner?; + let (id, inner) = inner?; let params = params.into_iter(); if remaining.is_empty() { Some(( - Some(NestedMatch { - matched, - params: params.chain(inner.to_params()), - child: inner, - view: &self.view, - }), + Some(( + id, + NestedMatch { + id, + matched, + params: params.chain(inner.to_params()), + child: inner, + view: &self.view, + }, + )), remaining, )) } else { diff --git a/routing_utils/src/nested/tuples.rs b/routing_utils/src/nested/tuples.rs index 6cdb29992..cc46803f3 100644 --- a/routing_utils/src/nested/tuples.rs +++ b/routing_utils/src/nested/tuples.rs @@ -1,4 +1,4 @@ -use crate::{MatchInterface, MatchNestedRoutes, PathSegment}; +use crate::{MatchInterface, MatchNestedRoutes, PathSegment, RouteMatchId}; use alloc::vec::Vec; use core::iter; use either_of::*; @@ -8,6 +8,14 @@ impl<'a> MatchInterface<'a> for () { type Child = (); type View = (); + fn as_id(&self) -> RouteMatchId { + RouteMatchId(0) + } + + fn as_matched(&self) -> &str { + "" + } + fn to_params(&self) -> Self::Params { iter::empty() } @@ -21,8 +29,11 @@ impl<'a> MatchNestedRoutes<'a> for () { type Data = (); type Match = (); - fn match_nested(&self, path: &'a str) -> (Option, &'a str) { - (Some(()), path) + fn match_nested( + &self, + path: &'a str, + ) -> (Option<(RouteMatchId, Self::Match)>, &'a str) { + (Some((RouteMatchId(0), ())), path) } fn generate_routes( @@ -40,6 +51,14 @@ where type Child = A::Child; type View = A::View; + fn as_id(&self) -> RouteMatchId { + RouteMatchId(0) + } + + fn as_matched(&self) -> &str { + self.0.as_matched() + } + fn to_params(&self) -> Self::Params { self.0.to_params() } @@ -60,7 +79,10 @@ where type Data = A::Data; type Match = A::Match; - fn match_nested(&'a self, path: &'a str) -> (Option, &'a str) { + fn match_nested( + &'a self, + path: &'a str, + ) -> (Option<(RouteMatchId, Self::Match)>, &'a str) { self.0.match_nested(path) } @@ -83,6 +105,20 @@ where type Child = Either; type View = Either; + fn as_id(&self) -> RouteMatchId { + match self { + Either::Left(_) => RouteMatchId(0), + Either::Right(_) => RouteMatchId(1), + } + } + + fn as_matched(&self) -> &str { + match self { + Either::Left(i) => i.as_matched(), + Either::Right(i) => i.as_matched(), + } + } + fn to_params(&self) -> Self::Params { match self { Either::Left(i) => Either::Left(i.to_params().into_iter()), @@ -113,14 +149,17 @@ where type Data = (A::Data, B::Data); type Match = Either; - fn match_nested(&'a self, path: &'a str) -> (Option, &'a str) { + fn match_nested( + &'a self, + path: &'a str, + ) -> (Option<(RouteMatchId, Self::Match)>, &'a str) { #[allow(non_snake_case)] let (A, B) = &self; - if let (Some(matched), remaining) = A.match_nested(path) { - return (Some(Either::Left(matched)), remaining); + if let (Some((id, matched)), remaining) = A.match_nested(path) { + return (Some((id, Either::Left(matched))), remaining); } - if let (Some(matched), remaining) = B.match_nested(path) { - return (Some(Either::Right(matched)), remaining); + if let (Some((id, matched)), remaining) = B.match_nested(path) { + return (Some((id, Either::Right(matched))), remaining); } (None, path) } @@ -152,7 +191,7 @@ macro_rules! chain_generated { } macro_rules! tuples { - ($either:ident => $($ty:ident),*) => { + ($either:ident => $($ty:ident = $count:expr),*) => { impl<'a, $($ty,)*> MatchInterface<'a> for $either <$($ty,)*> where $($ty: MatchInterface<'a>),*, @@ -165,6 +204,18 @@ macro_rules! tuples { type Child = $either<$($ty::Child,)*>; type View = $either<$($ty::View,)*>; + fn as_id(&self) -> RouteMatchId { + match self { + $($either::$ty(_) => RouteMatchId($count),)* + } + } + + fn as_matched(&self) -> &str { + match self { + $($either::$ty(i) => i.as_matched(),)* + } + } + fn to_params(&self) -> Self::Params { match self { $($either::$ty(i) => $either::$ty(i.to_params().into_iter()),)* @@ -192,12 +243,15 @@ macro_rules! tuples { type Data = ($($ty::Data,)*); type Match = $either<$($ty::Match,)*>; - fn match_nested(&'a self, path: &'a str) -> (Option, &'a str) { + fn match_nested(&'a self, path: &'a str) -> (Option<(RouteMatchId, Self::Match)>, &'a str) { #[allow(non_snake_case)] let ($($ty,)*) = &self; - $(if let (Some(matched), remaining) = $ty.match_nested(path) { - return (Some($either::$ty(matched)), remaining); + let mut id = 0; + $(if let (Some((_, matched)), remaining) = $ty.match_nested(path) { + return (Some((RouteMatchId(id), $either::$ty(matched))), remaining); + } else { + id += 1; })* (None, path) } @@ -215,17 +269,17 @@ macro_rules! tuples { } } -tuples!(EitherOf3 => A, B, C); -tuples!(EitherOf4 => A, B, C, D); -tuples!(EitherOf5 => A, B, C, D, E); -tuples!(EitherOf6 => A, B, C, D, E, F); -tuples!(EitherOf7 => A, B, C, D, E, F, G); -tuples!(EitherOf8 => A, B, C, D, E, F, G, H); -tuples!(EitherOf9 => A, B, C, D, E, F, G, H, I); -tuples!(EitherOf10 => A, B, C, D, E, F, G, H, I, J); -tuples!(EitherOf11 => A, B, C, D, E, F, G, H, I, J, K); -tuples!(EitherOf12 => A, B, C, D, E, F, G, H, I, J, K, L); -tuples!(EitherOf13 => A, B, C, D, E, F, G, H, I, J, K, L, M); -tuples!(EitherOf14 => A, B, C, D, E, F, G, H, I, J, K, L, M, N); -tuples!(EitherOf15 => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); -tuples!(EitherOf16 => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); +tuples!(EitherOf3 => A = 0, B = 1, C = 2); +tuples!(EitherOf4 => A = 0, B = 1, C = 2, D = 3); +tuples!(EitherOf5 => A = 0, B = 1, C = 2, D = 3, E = 4); +tuples!(EitherOf6 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5); +tuples!(EitherOf7 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6); +tuples!(EitherOf8 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7); +tuples!(EitherOf9 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8); +tuples!(EitherOf10 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9); +tuples!(EitherOf11 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10); +tuples!(EitherOf12 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11); +tuples!(EitherOf13 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12); +tuples!(EitherOf14 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13); +tuples!(EitherOf15 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14); +tuples!(EitherOf16 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15); diff --git a/tachys/src/view/either.rs b/tachys/src/view/either.rs index 9951b687a..dacd20f9e 100644 --- a/tachys/src/view/either.rs +++ b/tachys/src/view/either.rs @@ -16,7 +16,7 @@ where B: Mountable, Rndr: Renderer, { - state: Either, + pub state: Either, marker: Rndr::Placeholder, }