wip: add router
This commit is contained in:
1 parent
9874f342e2
commit
d298b626d3
17 files changed
+617
-20
No files matched your search
+2
-1
@@ -9,6 +9,7 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { path = "./packages/core", version = "0.1.3" }
|
dioxus-core = { path = "./packages/core", version = "0.1.3" }
|
||||||
|
dioxus-router = { path = "./packages/router", optional = true }
|
||||||
dioxus-core-macro = { path = "./packages/core-macro", optional = true }
|
dioxus-core-macro = { path = "./packages/core-macro", optional = true }
|
||||||
dioxus-html = { path = "./packages/html", optional = true }
|
dioxus-html = { path = "./packages/html", optional = true }
|
||||||
dioxus-web = { path = "./packages/web", optional = true }
|
dioxus-web = { path = "./packages/web", optional = true }
|
||||||
@@ -24,10 +25,10 @@ core = ["macro", "hooks", "html"]
|
|||||||
macro = ["dioxus-core-macro"]
|
macro = ["dioxus-core-macro"]
|
||||||
hooks = ["dioxus-hooks"]
|
hooks = ["dioxus-hooks"]
|
||||||
html = ["dioxus-html"]
|
html = ["dioxus-html"]
|
||||||
|
router = ["dioxus-router"]
|
||||||
|
|
||||||
# utilities
|
# utilities
|
||||||
atoms = []
|
atoms = []
|
||||||
router = []
|
|
||||||
|
|
||||||
# targets
|
# targets
|
||||||
ssr = ["dioxus-ssr"]
|
ssr = ["dioxus-ssr"]
|
||||||
|
|||||||
@@ -173,8 +173,6 @@ In general, Dioxus should be plenty fast for most use cases. However, there are
|
|||||||
|
|
||||||
Don't worry - Dioxus is fast. But, if your app needs *extreme performance*, then take a look at the `Performance Tuning` in the `Advanced Guides` book.
|
Don't worry - Dioxus is fast. But, if your app needs *extreme performance*, then take a look at the `Performance Tuning` in the `Advanced Guides` book.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Moving On
|
## Moving On
|
||||||
|
|
||||||
This overview was a lot of information - but it doesn't tell you everything!
|
This overview was a lot of information - but it doesn't tell you everything!
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use syn::parse_macro_input;
|
|||||||
pub(crate) mod htm;
|
pub(crate) mod htm;
|
||||||
pub(crate) mod ifmt;
|
pub(crate) mod ifmt;
|
||||||
pub(crate) mod props;
|
pub(crate) mod props;
|
||||||
|
pub(crate) mod router;
|
||||||
pub(crate) mod rsx;
|
pub(crate) mod rsx;
|
||||||
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
@@ -82,12 +83,9 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
|||||||
/// }}
|
/// }}
|
||||||
///
|
///
|
||||||
/// // Matching
|
/// // Matching
|
||||||
/// // Matching will throw a Rust error about "no two closures are the same type"
|
|
||||||
/// // To fix this, call "render" method or use the "in" syntax to produce VNodes.
|
|
||||||
/// // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros)
|
|
||||||
/// {match true {
|
/// {match true {
|
||||||
/// true => rsx!(cx, h1 {"Top text"}),
|
/// true => rsx!(h1 {"Top text"}),
|
||||||
/// false => cx.render(rsx!( h1 {"Bottom text"}))
|
/// false => rsx!(h1 {"Bottom text"})
|
||||||
/// }}
|
/// }}
|
||||||
///
|
///
|
||||||
/// // Conditional rendering
|
/// // Conditional rendering
|
||||||
@@ -95,11 +93,11 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
|||||||
/// // You can convert a bool condition to rsx! with .then and .or
|
/// // You can convert a bool condition to rsx! with .then and .or
|
||||||
/// {true.then(|| rsx!(div {}))}
|
/// {true.then(|| rsx!(div {}))}
|
||||||
///
|
///
|
||||||
/// // True conditions need to be rendered (same reasons as matching)
|
/// // True conditions
|
||||||
/// {if true {
|
/// {if true {
|
||||||
/// rsx!(cx, h1 {"Top text"})
|
/// rsx!(h1 {"Top text"})
|
||||||
/// } else {
|
/// } else {
|
||||||
/// rsx!(cx, h1 {"Bottom text"})
|
/// rsx!(h1 {"Bottom text"})
|
||||||
/// }}
|
/// }}
|
||||||
///
|
///
|
||||||
/// // returning "None" is a bit noisy... but rare in practice
|
/// // returning "None" is a bit noisy... but rare in practice
|
||||||
@@ -186,3 +184,31 @@ pub fn rsx(s: TokenStream) -> TokenStream {
|
|||||||
Ok(s) => s.to_token_stream().into(),
|
Ok(s) => s.to_token_stream().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derive macro used to mark an enum as Routable.
|
||||||
|
///
|
||||||
|
/// This macro can only be used on enums. Every varient of the macro needs to be marked
|
||||||
|
/// with the `at` attribute to specify the URL of the route. It generates an implementation of
|
||||||
|
/// `yew_router::Routable` trait and `const`s for the routes passed which are used with `Route`
|
||||||
|
/// component.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use yew_router::Routable;
|
||||||
|
/// #[derive(Debug, Clone, Copy, PartialEq, Routable)]
|
||||||
|
/// enum Routes {
|
||||||
|
/// #[at("/")]
|
||||||
|
/// Home,
|
||||||
|
/// #[at("/secure")]
|
||||||
|
/// Secure,
|
||||||
|
/// #[at("/404")]
|
||||||
|
/// NotFound,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_derive(Routable, attributes(at, not_found))]
|
||||||
|
pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
use router::{routable_derive_impl, Routable};
|
||||||
|
let input = parse_macro_input!(input as Routable);
|
||||||
|
routable_derive_impl(input).into()
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::parse::{Parse, ParseStream};
|
||||||
|
use syn::punctuated::Punctuated;
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant};
|
||||||
|
|
||||||
|
const AT_ATTR_IDENT: &str = "at";
|
||||||
|
const NOT_FOUND_ATTR_IDENT: &str = "not_found";
|
||||||
|
|
||||||
|
pub struct Routable {
|
||||||
|
ident: Ident,
|
||||||
|
ats: Vec<LitStr>,
|
||||||
|
variants: Punctuated<Variant, syn::token::Comma>,
|
||||||
|
not_found_route: Option<Ident>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Routable {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let DeriveInput { ident, data, .. } = input.parse()?;
|
||||||
|
|
||||||
|
let data = match data {
|
||||||
|
Data::Enum(data) => data,
|
||||||
|
Data::Struct(s) => {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
s.struct_token.span(),
|
||||||
|
"expected enum, found struct",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Data::Union(u) => {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
u.union_token.span(),
|
||||||
|
"expected enum, found union",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (not_found_route, ats) = parse_variants_attributes(&data.variants)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
ident,
|
||||||
|
variants: data.variants,
|
||||||
|
ats,
|
||||||
|
not_found_route,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_variants_attributes(
|
||||||
|
variants: &Punctuated<Variant, syn::token::Comma>,
|
||||||
|
) -> syn::Result<(Option<Ident>, Vec<LitStr>)> {
|
||||||
|
let mut not_founds = vec![];
|
||||||
|
let mut ats: Vec<LitStr> = vec![];
|
||||||
|
|
||||||
|
let mut not_found_attrs = vec![];
|
||||||
|
|
||||||
|
for variant in variants.iter() {
|
||||||
|
if let Fields::Unnamed(ref field) = variant.fields {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
field.span(),
|
||||||
|
"only named fields are supported",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let attrs = &variant.attrs;
|
||||||
|
let at_attrs = attrs
|
||||||
|
.iter()
|
||||||
|
.filter(|attr| attr.path.is_ident(AT_ATTR_IDENT))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let attr = match at_attrs.len() {
|
||||||
|
1 => *at_attrs.first().unwrap(),
|
||||||
|
0 => {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
variant.span(),
|
||||||
|
format!(
|
||||||
|
"{} attribute must be present on every variant",
|
||||||
|
AT_ATTR_IDENT
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
quote! { #(#at_attrs)* },
|
||||||
|
format!("only one {} attribute must be present", AT_ATTR_IDENT),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lit = attr.parse_args::<LitStr>()?;
|
||||||
|
ats.push(lit);
|
||||||
|
|
||||||
|
for attr in attrs.iter() {
|
||||||
|
if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) {
|
||||||
|
not_found_attrs.push(attr);
|
||||||
|
not_founds.push(variant.ident.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if not_founds.len() > 1 {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
quote! { #(#not_found_attrs)* },
|
||||||
|
format!("there can only be one {}", NOT_FOUND_ATTR_IDENT),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((not_founds.into_iter().next(), ats))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Routable {
|
||||||
|
fn build_from_path(&self) -> TokenStream {
|
||||||
|
let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||||
|
let ident = &variant.ident;
|
||||||
|
let right = match &variant.fields {
|
||||||
|
Fields::Unit => quote! { Self::#ident },
|
||||||
|
Fields::Named(field) => {
|
||||||
|
let fields = field.named.iter().map(|it| {
|
||||||
|
//named fields have idents
|
||||||
|
it.ident.as_ref().unwrap()
|
||||||
|
});
|
||||||
|
quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } }
|
||||||
|
}
|
||||||
|
Fields::Unnamed(_) => unreachable!(), // already checked
|
||||||
|
};
|
||||||
|
|
||||||
|
let left = self.ats.get(i).unwrap();
|
||||||
|
quote! {
|
||||||
|
#left => ::std::option::Option::Some(#right)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option<Self> {
|
||||||
|
match path {
|
||||||
|
#(#from_path_matches),*,
|
||||||
|
_ => ::std::option::Option::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_to_path(&self) -> TokenStream {
|
||||||
|
let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||||
|
let ident = &variant.ident;
|
||||||
|
let mut right = self.ats.get(i).unwrap().value();
|
||||||
|
|
||||||
|
match &variant.fields {
|
||||||
|
Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) },
|
||||||
|
Fields::Named(field) => {
|
||||||
|
let fields = field
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|it| it.ident.as_ref().unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for field in fields.iter() {
|
||||||
|
// :param -> {param}
|
||||||
|
// so we can pass it to `format!("...", param)`
|
||||||
|
right = right.replace(&format!(":{}", field), &format!("{{{}}}", field))
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Fields::Unnamed(_) => unreachable!(), // already checked
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
fn to_path(&self) -> ::std::string::String {
|
||||||
|
match self {
|
||||||
|
#(#to_path_matches),*,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routable_derive_impl(input: Routable) -> TokenStream {
|
||||||
|
let Routable {
|
||||||
|
ats,
|
||||||
|
not_found_route,
|
||||||
|
ident,
|
||||||
|
..
|
||||||
|
} = &input;
|
||||||
|
|
||||||
|
let from_path = input.build_from_path();
|
||||||
|
let to_path = input.build_to_path();
|
||||||
|
|
||||||
|
let not_found_route = match not_found_route {
|
||||||
|
Some(route) => quote! { ::std::option::Option::Some(Self::#route) },
|
||||||
|
None => quote! { ::std::option::Option::None },
|
||||||
|
};
|
||||||
|
|
||||||
|
let cache_thread_local_ident = Ident::new(
|
||||||
|
&format!("__{}_ROUTER_CURRENT_ROUTE_CACHE", ident),
|
||||||
|
ident.span(),
|
||||||
|
);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
::std::thread_local! {
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
static #cache_thread_local_ident: ::std::cell::RefCell<::std::option::Option<#ident>> = ::std::cell::RefCell::new(::std::option::Option::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[automatically_derived]
|
||||||
|
impl ::dioxus::router::Routable for #ident {
|
||||||
|
#from_path
|
||||||
|
#to_path
|
||||||
|
|
||||||
|
fn routes() -> ::std::vec::Vec<&'static str> {
|
||||||
|
::std::vec![#(#ats),*]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_found_route() -> ::std::option::Option<Self> {
|
||||||
|
#not_found_route
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_route() -> ::std::option::Option<Self> {
|
||||||
|
#cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recognize(pathname: &str) -> ::std::option::Option<Self> {
|
||||||
|
::std::thread_local! {
|
||||||
|
static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>();
|
||||||
|
}
|
||||||
|
let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname));
|
||||||
|
{
|
||||||
|
let route = ::std::clone::Clone::clone(&route);
|
||||||
|
#cache_thread_local_ident.with(move |val| {
|
||||||
|
*val.borrow_mut() = route;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
route
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup() {
|
||||||
|
#cache_thread_local_ident.with(move |val| {
|
||||||
|
*val.borrow_mut() = ::std::option::Option::None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,9 +49,8 @@ impl ToTokens for CallBody {
|
|||||||
match &self.custom_context {
|
match &self.custom_context {
|
||||||
// The `in cx` pattern allows directly rendering
|
// The `in cx` pattern allows directly rendering
|
||||||
Some(ident) => out_tokens.append_all(quote! {
|
Some(ident) => out_tokens.append_all(quote! {
|
||||||
#ident.render(dioxus::prelude::LazyNodes::new(move |__cx: NodeFactory| -> VNode {
|
#ident.render(NodeFactory::annotate_lazy(move |__cx: NodeFactory| -> VNode {
|
||||||
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||||
|
|
||||||
#inner
|
#inner
|
||||||
}))
|
}))
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::innerlude::{Context, Element, VNode};
|
|||||||
/// ```rust
|
/// ```rust
|
||||||
/// struct State {}
|
/// struct State {}
|
||||||
///
|
///
|
||||||
/// fn Example((cx, props): Component<State>) -> DomTree {
|
/// fn Example((cx, props): Scope<State>) -> DomTree {
|
||||||
/// // ...
|
/// // ...
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
@@ -26,7 +26,7 @@ use crate::innerlude::{Context, Element, VNode};
|
|||||||
/// name: &'a str
|
/// name: &'a str
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn Example<'a>((cx, props): Component<'a, State>) -> DomTree<'a> {
|
/// fn Example<'a>((cx, props): Scope<'a, State>) -> DomTree<'a> {
|
||||||
/// // ...
|
/// // ...
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -132,12 +132,11 @@ impl<'src> Context<'src> {
|
|||||||
(self.scope.shared.submit_task)(task)
|
(self.scope.shared.submit_task)(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
|
/// This method enables the ability to expose state to children further down the VirtualDOM Tree.
|
||||||
///
|
///
|
||||||
/// This is a hook, so it should not be called conditionally!
|
/// This is a "fundamental" operation and should only be called during initialization of a hook.
|
||||||
///
|
///
|
||||||
/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
|
/// For a hook that provides the same functionality, use `use_provide_state` and `use_consume_state` instead.
|
||||||
/// so don't put it in a conditional.
|
|
||||||
///
|
///
|
||||||
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
|
/// When the component is dropped, so is the context. Be aware of this behavior when consuming
|
||||||
/// the context via Rc/Weak.
|
/// the context via Rc/Weak.
|
||||||
@@ -148,7 +147,7 @@ impl<'src> Context<'src> {
|
|||||||
/// struct SharedState(&'static str);
|
/// struct SharedState(&'static str);
|
||||||
///
|
///
|
||||||
/// static App: FC<()> = |(cx, props)|{
|
/// static App: FC<()> = |(cx, props)|{
|
||||||
/// cx.provide_state(SharedState("world"));
|
/// cx.use_hook(|_| cx.provide_state(SharedState("world")), |_| {}, |_| {});
|
||||||
/// rsx!(cx, Child {})
|
/// rsx!(cx, Child {})
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
[package]
|
||||||
|
name = "dioxus-router"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dioxus-core = { path = "../core", version = "0.1.3", default-features = false }
|
||||||
|
dioxus-core-macro = { path = "../core-macro", version = "0.1" }
|
||||||
|
dioxus-html = { path = "../html", version = "0.0.0", default-features = false }
|
||||||
|
|
||||||
|
web-sys = { version = "0.3", features = [
|
||||||
|
"Attr",
|
||||||
|
"Document",
|
||||||
|
"History",
|
||||||
|
"HtmlBaseElement",
|
||||||
|
"Event",
|
||||||
|
"NamedNodeMap",
|
||||||
|
"Url",
|
||||||
|
"UrlSearchParams",
|
||||||
|
"Window",
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
|
serde = "1"
|
||||||
|
|
||||||
|
serde_urlencoded = "0.7"
|
||||||
|
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
js-sys = "0.3"
|
||||||
|
gloo = "0.4"
|
||||||
|
route-recognizer = "0.3.1"
|
||||||
|
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["web"]
|
||||||
|
web = ["web-sys"]
|
||||||
|
desktop = []
|
||||||
|
mobile = []
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
|
|
||||||
|
enum Routes {
|
||||||
|
Home,
|
||||||
|
Blog,
|
||||||
|
}
|
||||||
|
|
||||||
|
static App: FC<()> = |(cx, props)| {
|
||||||
|
let route = use_router::<Routes>(cx);
|
||||||
|
|
||||||
|
let content = match route {
|
||||||
|
Routes::Home => rsx!(Home {}),
|
||||||
|
Routes::Blog => rsx!(Blog {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
{content}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
fn use_router<P>(cx: Context) -> &P {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
static Home: FC<()> = |(cx, props)| todo!();
|
||||||
|
static Blog: FC<()> = |(cx, props)| todo!();
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_router::{use_router, Link, Routable};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dbg!(App as *const fn());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub enum Route {
|
||||||
|
// #[at("/")]
|
||||||
|
Home,
|
||||||
|
|
||||||
|
// #[at("/:id")]
|
||||||
|
AllUsers { page: u32 },
|
||||||
|
|
||||||
|
// #[at("/:id")]
|
||||||
|
User { id: u32 },
|
||||||
|
|
||||||
|
// #[at("/:id")]
|
||||||
|
BlogList { page: u32 },
|
||||||
|
|
||||||
|
// #[at("/:id")]
|
||||||
|
BlogPost { post_id: u32 },
|
||||||
|
|
||||||
|
// #[at("/404")]
|
||||||
|
// #[not_found]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
static App: FC<()> = |(cx, props)| {
|
||||||
|
let route = use_router(cx, Route::parse)?;
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
nav {
|
||||||
|
Link { to: Route::Home, "Go home!" }
|
||||||
|
Link { to: Route::AllUsers { page: 0 }, "List all users" }
|
||||||
|
Link { to: Route::BlogList { page: 0 }, "Blog posts" }
|
||||||
|
}
|
||||||
|
{match route {
|
||||||
|
Route::Home => rsx!("Home"),
|
||||||
|
Route::AllUsers { page } => rsx!("All users - page {page}"),
|
||||||
|
Route::User { id } => rsx!("User - id: {id}"),
|
||||||
|
Route::BlogList { page } => rsx!("Blog posts - page {page}"),
|
||||||
|
Route::BlogPost { post_id } => rsx!("Blog post - post {post_id}"),
|
||||||
|
Route::NotFound => rsx!("Not found"),
|
||||||
|
}}
|
||||||
|
footer {}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Route {
|
||||||
|
// Generate the appropriate route from the "tail" end of the URL
|
||||||
|
fn parse(url: &str) -> Self {
|
||||||
|
use Route::*;
|
||||||
|
match url {
|
||||||
|
"/" => Home,
|
||||||
|
"/users" => AllUsers { page: 1 },
|
||||||
|
"/users/:page" => AllUsers { page: 1 },
|
||||||
|
"/users/:page/:id" => User { id: 1 },
|
||||||
|
"/blog" => BlogList { page: 1 },
|
||||||
|
"/blog/:page" => BlogList { page: 1 },
|
||||||
|
"/blog/:page/:id" => BlogPost { post_id: 1 },
|
||||||
|
_ => NotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Routable for Route {
|
||||||
|
fn from_path(path: &str, params: &std::collections::HashMap<&str, &str>) -> Option<Self> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_path(&self) -> String {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn routes() -> Vec<&'static str> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_found_route() -> Option<Self> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recognize(pathname: &str) -> Option<Self> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
mod utils;
|
||||||
|
|
||||||
|
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
|
use dioxus_core as dioxus;
|
||||||
|
use dioxus_core::prelude::*;
|
||||||
|
use dioxus_core_macro::{format_args_f, rsx, Props};
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
use web_sys::Event;
|
||||||
|
|
||||||
|
use crate::utils::fetch_base_url;
|
||||||
|
|
||||||
|
pub struct RouterService<R: Routable> {
|
||||||
|
history: RefCell<Vec<R>>,
|
||||||
|
base_ur: RefCell<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> RouterService<R> {
|
||||||
|
fn push_route(&self, r: R) {
|
||||||
|
self.history.borrow_mut().push(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_route(&self) -> &str {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_route_impl(&self, url: String, push: bool) {
|
||||||
|
let history = web_sys::window().unwrap().history().expect("no history");
|
||||||
|
let base = self.base_ur.borrow();
|
||||||
|
let path = match base.as_ref() {
|
||||||
|
Some(base) => {
|
||||||
|
let path = format!("{}{}", base, url);
|
||||||
|
if path.is_empty() {
|
||||||
|
"/".to_string()
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => url,
|
||||||
|
};
|
||||||
|
|
||||||
|
if push {
|
||||||
|
history
|
||||||
|
.push_state_with_url(&JsValue::NULL, "", Some(&path))
|
||||||
|
.expect("push history");
|
||||||
|
} else {
|
||||||
|
history
|
||||||
|
.replace_state_with_url(&JsValue::NULL, "", Some(&path))
|
||||||
|
.expect("replace history");
|
||||||
|
}
|
||||||
|
let event = Event::new("popstate").unwrap();
|
||||||
|
|
||||||
|
web_sys::window()
|
||||||
|
.unwrap()
|
||||||
|
.dispatch_event(&event)
|
||||||
|
.expect("dispatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This hould only be used once per app
|
||||||
|
///
|
||||||
|
/// You can manually parse the route if you want, but the derived `parse` method on `Routable` will also work just fine
|
||||||
|
pub fn use_router<R: Routable>(cx: Context, cfg: impl FnOnce(&str) -> R) -> Option<R> {
|
||||||
|
// for the web, attach to the history api
|
||||||
|
cx.use_hook(
|
||||||
|
|f| {
|
||||||
|
//
|
||||||
|
use gloo::events::EventListener;
|
||||||
|
|
||||||
|
let base_url = fetch_base_url();
|
||||||
|
|
||||||
|
let service: RouterService<R> = RouterService {
|
||||||
|
history: RefCell::new(vec![]),
|
||||||
|
base_ur: RefCell::new(base_url),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.provide_state(service);
|
||||||
|
|
||||||
|
let regenerate = cx.schedule_update();
|
||||||
|
|
||||||
|
// when "back" is called by the user, we want to to re-render the component
|
||||||
|
let listener = EventListener::new(&web_sys::window().unwrap(), "popstate", move |_| {
|
||||||
|
//
|
||||||
|
regenerate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|f| {
|
||||||
|
//
|
||||||
|
},
|
||||||
|
|f| {
|
||||||
|
//
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let router = use_router_service::<R>(cx)?;
|
||||||
|
Some(cfg(router.get_current_route()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_router_service<R: Routable>(cx: Context) -> Option<&Rc<RouterService<R>>> {
|
||||||
|
cx.use_hook(
|
||||||
|
|_| cx.consume_state::<RouterService<R>>(),
|
||||||
|
|f| f.as_ref(),
|
||||||
|
|f| {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct LinkProps<'a, R: Routable> {
|
||||||
|
to: R,
|
||||||
|
children: ScopeChildren<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn Link<'a, R: Routable>((cx, props): Scope<'a, LinkProps<'a, R>>) -> Element {
|
||||||
|
let service = use_router_service::<R>(cx)?;
|
||||||
|
cx.render(rsx! {
|
||||||
|
a {
|
||||||
|
href: format_args!("{}", props.to.to_path()),
|
||||||
|
onclick: move |_| service.push_route(props.to.clone()),
|
||||||
|
{&props.children},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Routable: Sized + Clone + 'static {
|
||||||
|
/// Converts path to an instance of the routes enum.
|
||||||
|
fn from_path(path: &str, params: &HashMap<&str, &str>) -> Option<Self>;
|
||||||
|
|
||||||
|
/// Converts the route to a string that can passed to the history API.
|
||||||
|
fn to_path(&self) -> String;
|
||||||
|
|
||||||
|
/// Lists all the available routes
|
||||||
|
fn routes() -> Vec<&'static str>;
|
||||||
|
|
||||||
|
/// The route to redirect to on 404
|
||||||
|
fn not_found_route() -> Option<Self>;
|
||||||
|
|
||||||
|
/// Match a route based on the path
|
||||||
|
fn recognize(pathname: &str) -> Option<Self>;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,31 @@
|
|||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::window;
|
||||||
|
|
||||||
|
pub fn fetch_base_url() -> Option<String> {
|
||||||
|
match window()
|
||||||
|
.unwrap()
|
||||||
|
.document()
|
||||||
|
.unwrap()
|
||||||
|
.query_selector("base[href]")
|
||||||
|
{
|
||||||
|
Ok(Some(base)) => {
|
||||||
|
let base = JsCast::unchecked_into::<web_sys::HtmlBaseElement>(base).href();
|
||||||
|
|
||||||
|
let url = web_sys::Url::new(&base).unwrap();
|
||||||
|
let base = url.pathname();
|
||||||
|
|
||||||
|
let base = if base != "/" {
|
||||||
|
strip_slash_suffix(&base)
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(base.to_string())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn strip_slash_suffix(path: &str) -> &str {
|
||||||
|
path.strip_suffix('/').unwrap_or(path)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user