get basic routing working

This commit is contained in:
Greg Johnston 2024-03-06 13:41:40 -05:00
parent 9f02cc8cc1
commit 84ebdc1b92
9 changed files with 408 additions and 85 deletions

View file

@ -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 {
</Route>
}
}*/
/*
#[component]
pub fn ContactList() -> impl IntoView {
log::debug!("rendering <ContactList/>");
info!("rendering <ContactList/>");
// contexts are passed down through the route tree
provide_context(ExampleContext(42));
on_cleanup(|| {
Owner::on_cleanup(|| {
info!("cleaning up <ContactList/>");
});
let location = use_location();
view! {
<div class="contact-list">
<h1>"Contacts"</h1>
<li><a href="/1">1</a></li>
<li><a href="/2">2</a></li>
<li><a href="/3">3</a></li>
</div>
}
/*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"
/>
</div>
}
}*/
}*/
}
/*
#[derive(Params, PartialEq, Clone, Debug)]
pub struct ContactParams {
// Params isn't implemented for usize, only Option<usize>
id: Option<usize>,
}
}*/
#[component]
pub fn Contact() -> impl IntoView {
@ -180,11 +208,17 @@ pub fn Contact() -> impl IntoView {
use_context::<ExampleContext>()
);
on_cleanup(|| {
Owner::on_cleanup(|| {
info!("cleaning up <Contact/>");
});
let params = use_params::<ContactParams>();
view! {
<div class="contact">
<h2>"Contact"</h2>
</div>
}
/* let params = use_params::<ContactParams>();
let contact = create_resource(
move || {
params
@ -229,8 +263,8 @@ pub fn Contact() -> impl IntoView {
{contact_display}
</Transition>
</div>
}
}*/
}*/
}
#[component]
pub fn About() -> impl IntoView {
@ -271,7 +305,6 @@ pub fn Settings() -> impl IntoView {
});
view! {
<>
<h1>"Settings"</h1>
<form>
<fieldset>
@ -281,6 +314,5 @@ pub fn Settings() -> impl IntoView {
</fieldset>
<pre>"This page is just a placeholder."</pre>
</form>
</>
}
}

View file

@ -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();

View file

@ -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<T>(&self, fun: impl FnOnce() -> T) -> T {
let prev = {
OWNER.with(|o| {

View file

@ -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"]

View file

@ -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<View::State, <Fallback as Render<Rndr>>::State, Rndr>,
EitherState<
NestedRouteState<View::State>,
<Fallback as Render<Rndr>>::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<EitherState<_, _, _>>| {
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::<NestedRouteView<View>, _>::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<
<<NewMatch::View as ChooseView>::Output as Render<R>>::State,
>,
new: NewMatch,
) where
NewMatch: MatchInterface<'a>,
NewMatch::View: ChooseView,
<NewMatch::View as ChooseView>::Output: Render<R>,
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<View> {
id: RouteMatchId,
owner: Owner,
params: Params,
matched: String,
view: View,
}
impl<View> NestedRouteView<View> {
pub fn new<'a, Matcher>(outer_owner: &Owner, matched: Matcher) -> Self
where
Matcher: MatchInterface<'a>,
Matcher::View: ChooseView<Output = View>,
{
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<R, View> Render<R> for NestedRouteView<View>
where
View: Render<R>,
R: Renderer,
{
type State = NestedRouteState<View::State>;
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<Self::FallibleState> {
todo!()
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> tachys::error::Result<()> {
todo!()
}
}
pub struct NestedRouteState<ViewState> {
id: RouteMatchId,
owner: Owner,
params: Params,
matched: String,
view: ViewState,
}
impl<ViewState, R> Mountable<R> for NestedRouteState<ViewState>
where
ViewState: Mountable<R>,
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<R>,
) -> bool {
self.view.insert_before_this(parent, child)
}
}
impl<Rndr, Loc, FallbackFn, Fallback, Children, View> RenderHtml<Rndr>
for Router<Rndr, Loc, Children, FallbackFn>
where
@ -239,6 +400,40 @@ where
self
}
}
macro_rules! tuples {
($either:ident => $($ty:ident),*) => {
paste::paste! {
impl<$($ty, [<Fn $ty>],)*> ChooseView for $either<$([<Fn $ty>],)*>
where
$([<Fn $ty>]: 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<Rndr, Loc, Fal, Children> RenderHtml<Rndr>
for Router<Rndr, Loc, Children, Fal>

View file

@ -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<Item = (&'a str, &'a str)>;
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<Self::Match>, &'a str);
fn match_nested(
&'a self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &'a str);
fn generate_routes(
&self,

View file

@ -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<Segments, Children, Data, View> {
#[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
<<Children::Match as MatchInterface<'a>>::Params as IntoIterator>::IntoIter,
>, Children::Match, View>;
fn match_nested(&'a self, path: &'a str) -> (Option<Self::Match>, &'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 {
Some((
id,
NestedMatch {
id,
matched,
params: params.chain(inner.to_params()),
child: inner,
view: &self.view,
}),
},
)),
remaining,
))
} else {

View file

@ -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<Self::Match>, &'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<Self::Match>, &'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<A::Child, B::Child>;
type View = Either<A::View, B::View>;
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<A::Match, B::Match>;
fn match_nested(&'a self, path: &'a str) -> (Option<Self::Match>, &'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<Self::Match>, &'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);

View file

@ -16,7 +16,7 @@ where
B: Mountable<Rndr>,
Rndr: Renderer,
{
state: Either<A, B>,
pub state: Either<A, B>,
marker: Rndr::Placeholder,
}