mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
Significant router work
This commit is contained in:
parent
02558c2f66
commit
7cfebcbfee
64 changed files with 3149 additions and 357 deletions
|
@ -6,6 +6,8 @@ edition = "2021"
|
|||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
wee_alloc = "0.4"
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use leptos::*;
|
||||
|
||||
pub fn simple_counter(cx: Scope) -> web_sys::Element {
|
||||
let (value, set_value) = cx.create_signal(0);
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
log::debug!("ok");
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value(|value| *value -= 1)>"-1"</button>
|
||||
<span>{move || value().to_string()}</span>
|
||||
<span>"Value: " {move || value().to_string()}</span>
|
||||
<button on:click=move |_| set_value(|value| *value += 1)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -5,5 +5,6 @@ use leptos::*;
|
|||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
pub fn main() {
|
||||
console_log::init_with_level(log::Level::Debug);
|
||||
mount_to_body(simple_counter)
|
||||
}
|
||||
|
|
|
@ -10,13 +10,13 @@ struct CounterUpdater {
|
|||
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
let (next_counter_id, set_next_counter_id) = cx.create_signal(0);
|
||||
let (counters, set_counters) = cx.create_signal::<CounterHolder>(vec![]);
|
||||
cx.provide_context(CounterUpdater { set_counters });
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
|
||||
provide_context(cx, CounterUpdater { set_counters });
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id();
|
||||
let sig = cx.create_signal(0);
|
||||
let sig = create_signal(cx, 0);
|
||||
set_counters(|counters| counters.push((id, sig)));
|
||||
set_next_counter_id(|id| *id += 1);
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
|
|||
let add_many_counters = move |_| {
|
||||
let mut new_counters = vec![];
|
||||
for next_id in 0..1000 {
|
||||
let signal = cx.create_signal(0);
|
||||
let signal = create_signal(cx, 0);
|
||||
new_counters.push((next_id, signal));
|
||||
}
|
||||
set_counters(move |n| *n = new_counters.clone());
|
||||
|
@ -78,7 +78,7 @@ fn Counter(
|
|||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> web_sys::Element {
|
||||
let CounterUpdater { set_counters } = cx.use_context().unwrap_throw();
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value(|value| *value = event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
|
|
19
examples/router/Cargo.toml
Normal file
19
examples/router/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "counter"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos", default-features = false, features = ["browser"] }
|
||||
wee_alloc = "0.4"
|
||||
futures = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
7
examples/router/index.html
Normal file
7
examples/router/index.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
69
examples/router/src/api.rs
Normal file
69
examples/router/src/api.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use futures::{
|
||||
channel::oneshot::{self, Canceled},
|
||||
Future,
|
||||
};
|
||||
use leptos::set_timeout;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContactSummary {
|
||||
pub id: usize,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Contact {
|
||||
pub id: usize,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub address_1: String,
|
||||
pub address_2: String,
|
||||
pub city: String,
|
||||
pub state: String,
|
||||
pub zip: String,
|
||||
pub email: String,
|
||||
pub phone: String,
|
||||
}
|
||||
|
||||
pub async fn get_contacts(search: String) -> Vec<ContactSummary> {
|
||||
// fake an API call with an artificial delay
|
||||
delay(Duration::from_millis(100)).await;
|
||||
vec![ContactSummary {
|
||||
id: 0,
|
||||
first_name: "Bill".into(),
|
||||
last_name: "Smith".into(),
|
||||
}]
|
||||
}
|
||||
|
||||
pub async fn get_contact(id: Option<usize>) -> Option<Contact> {
|
||||
// fake an API call with an artificial delay
|
||||
delay(Duration::from_millis(350)).await;
|
||||
match id {
|
||||
Some(0) => Some(Contact {
|
||||
id: 0,
|
||||
first_name: "Bill".into(),
|
||||
last_name: "Smith".into(),
|
||||
address_1: "12 Mulberry Lane".into(),
|
||||
address_2: "".into(),
|
||||
city: "Boston".into(),
|
||||
state: "MA".into(),
|
||||
zip: "02129".into(),
|
||||
email: "bill@smith.com".into(),
|
||||
phone: "617-121-1221".into(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn delay(duration: Duration) -> impl Future<Output = Result<(), Canceled>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
set_timeout(
|
||||
move || {
|
||||
tx.send(());
|
||||
},
|
||||
duration,
|
||||
);
|
||||
rx
|
||||
}
|
138
examples/router/src/lib.rs
Normal file
138
examples/router/src/lib.rs
Normal file
|
@ -0,0 +1,138 @@
|
|||
mod api;
|
||||
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use api::{Contact, ContactSummary};
|
||||
use futures::Future;
|
||||
use leptos::*;
|
||||
|
||||
use crate::api::{get_contact, get_contacts};
|
||||
|
||||
fn contact_list(
|
||||
cx: Scope,
|
||||
params: ParamsMap,
|
||||
location: Location,
|
||||
) -> Resource<String, Vec<ContactSummary>> {
|
||||
log::debug!("(contact_list) reloading contact list");
|
||||
create_resource(cx, location.search, move |s| get_contacts(s.to_string()))
|
||||
}
|
||||
|
||||
fn contact(
|
||||
cx: Scope,
|
||||
params: ParamsMap,
|
||||
location: Location,
|
||||
) -> Resource<Option<usize>, Option<Contact>> {
|
||||
log::debug!("(contact) reloading contact");
|
||||
create_resource(
|
||||
cx,
|
||||
move || {
|
||||
params
|
||||
.get("id")
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
},
|
||||
move |id| get_contact(id),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn router_example(cx: Scope) -> Element {
|
||||
view! {
|
||||
<div>
|
||||
<nav>
|
||||
<a href="/">"Contacts"</a>
|
||||
<a href="/about">"About"</a>
|
||||
<a href="/settings">"Settings"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<Router
|
||||
mode=BrowserIntegration {}
|
||||
base="/"
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
path=""
|
||||
element=move || view! { <ContactList/> }
|
||||
loader=contact_list.into()
|
||||
>
|
||||
<Route
|
||||
path=":id"
|
||||
loader=contact.into()
|
||||
element=move || view! { <Contact/> }
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="about"
|
||||
element=move || view! { <About/> }
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element=move || view! { <Settings/> }
|
||||
/>
|
||||
</Routes>
|
||||
</Router>
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> Vec<Element> {
|
||||
let contacts = use_loader::<Resource<String, Vec<ContactSummary>>>(cx);
|
||||
|
||||
view! {
|
||||
<>
|
||||
<h1>"Contacts"</h1>
|
||||
<ul>
|
||||
<For each={move || contacts.read().unwrap_or_default()} key=|contact| contact.id>
|
||||
{|cx, contact: &ContactSummary| {
|
||||
view! {
|
||||
<li><a href=format!("/contacts/{}", contact.id)> {&contact.first_name} " " {&contact.last_name}</a></li>
|
||||
}
|
||||
}}
|
||||
</For>
|
||||
</ul>
|
||||
<div><Outlet/></div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Contact(cx: Scope) -> Element {
|
||||
//let contact = use_loader::<Resource<Option<usize>, Option<Contact>>>(cx);
|
||||
|
||||
view! {
|
||||
<pre>"Contact info here"</pre>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn About(cx: Scope) -> Vec<Element> {
|
||||
view! {
|
||||
<>
|
||||
<h1>"About"</h1>
|
||||
<p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."</p>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Settings(cx: Scope) -> Vec<Element> {
|
||||
view! {
|
||||
<>
|
||||
<h1>"Settings"</h1>
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>"Name"</legend>
|
||||
<input type="text" name="first_name" placeholder="First"/>
|
||||
<input type="text" name="first_name" placeholder="Last"/>
|
||||
</fieldset>
|
||||
<pre>"This page is just a placeholder."</pre>
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
}
|
10
examples/router/src/main.rs
Normal file
10
examples/router/src/main.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use counter::router_example;
|
||||
use leptos::*;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
pub fn main() {
|
||||
console_log::init_with_level(log::Level::Debug);
|
||||
mount_to_body(router_example)
|
||||
}
|
|
@ -7,4 +7,8 @@ edition = "2021"
|
|||
leptos_core = { path = "../leptos_core" }
|
||||
leptos_dom = { path = "../leptos_dom" }
|
||||
leptos_macro = { path = "../leptos_macro" }
|
||||
leptos_reactive = { path = "../leptos_reactive" }
|
||||
leptos_reactive = { path = "../leptos_reactive" }
|
||||
leptos_router = { path = "../router" }
|
||||
|
||||
[features]
|
||||
browser = ["leptos_router/browser"]
|
|
@ -6,3 +6,4 @@ pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};
|
|||
pub use leptos_dom::*;
|
||||
pub use leptos_macro::*;
|
||||
pub use leptos_reactive::*;
|
||||
pub use leptos_router::*;
|
||||
|
|
|
@ -9,16 +9,17 @@ use crate::map::map_keyed;
|
|||
|
||||
/// Properties for the [For](crate::For) component.
|
||||
#[derive(Props)]
|
||||
pub struct ForProps<T, G, I, K>
|
||||
pub struct ForProps<E, T, G, I, K>
|
||||
where
|
||||
E: Fn() -> Vec<T>,
|
||||
G: Fn(Scope, &T) -> Element,
|
||||
I: Fn(&T) -> K,
|
||||
K: Eq + Hash,
|
||||
T: Eq + Clone + 'static,
|
||||
{
|
||||
pub each: ReadSignal<Vec<T>>,
|
||||
pub each: E,
|
||||
pub key: I,
|
||||
pub children: G,
|
||||
pub children: Vec<G>,
|
||||
}
|
||||
|
||||
/// Iterates over children and displays them, keyed by `PartialEq`. If you want to provide your
|
||||
|
@ -27,12 +28,14 @@ where
|
|||
/// This is much more efficient than naively iterating over nodes with `.iter().map(|n| view! { ... })...`,
|
||||
/// as it avoids re-creating DOM nodes that are not being changed.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn For<T, G, I, K>(cx: Scope, props: ForProps<T, G, I, K>) -> ReadSignal<Vec<Element>>
|
||||
pub fn For<E, T, G, I, K>(cx: Scope, mut props: ForProps<E, T, G, I, K>) -> ReadSignal<Vec<Element>>
|
||||
where
|
||||
E: Fn() -> Vec<T> + 'static,
|
||||
G: Fn(Scope, &T) -> Element + 'static,
|
||||
I: Fn(&T) -> K + 'static,
|
||||
K: Eq + Hash,
|
||||
T: Eq + Clone + Debug + 'static,
|
||||
{
|
||||
map_keyed(cx, props.each, props.children, props.key).clone()
|
||||
let map_fn = props.children.remove(0);
|
||||
map_keyed(cx, props.each, map_fn, props.key)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use leptos_reactive::{ReadSignal, Scope, ScopeDisposer};
|
||||
use leptos_reactive::{create_effect, create_signal, ReadSignal, Scope, ScopeDisposer};
|
||||
use std::{collections::HashMap, fmt::Debug, hash::Hash};
|
||||
|
||||
/// Function that maps a `Vec` to another `Vec` via a map function. The mapped `Vec` is lazy
|
||||
|
@ -17,12 +17,12 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash};
|
|||
/// which is in turned based on on the TypeScript implementation in <https://github.com/solidjs/solid>_
|
||||
pub fn map_keyed<T, U, K>(
|
||||
cx: Scope,
|
||||
list: ReadSignal<Vec<T>>,
|
||||
list: impl Fn() -> Vec<T> + 'static,
|
||||
map_fn: impl Fn(Scope, &T) -> U + 'static,
|
||||
key_fn: impl Fn(&T) -> K + 'static,
|
||||
) -> ReadSignal<Vec<U>>
|
||||
where
|
||||
T: PartialEq + Debug + Clone,
|
||||
T: PartialEq + Debug + Clone + 'static,
|
||||
K: Eq + Hash,
|
||||
U: PartialEq + Debug + Clone,
|
||||
{
|
||||
|
@ -30,12 +30,12 @@ where
|
|||
let mut mapped: Vec<U> = Vec::new();
|
||||
let mut disposers: Vec<Option<ScopeDisposer>> = Vec::new();
|
||||
|
||||
let (item_signal, set_item_signal) = cx.create_signal(Vec::new());
|
||||
let (item_signal, set_item_signal) = create_signal(cx, Vec::new());
|
||||
|
||||
// Diff and update signal each time list is updated.
|
||||
cx.create_effect(move |items| {
|
||||
create_effect(cx, move |items| {
|
||||
let items: Vec<T> = items.unwrap_or_default();
|
||||
let new_items = list.get();
|
||||
let new_items = list();
|
||||
let new_items_len = new_items.len();
|
||||
|
||||
if new_items.is_empty() {
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
use crate as leptos;
|
||||
use leptos_dom::{Child, IntoChild};
|
||||
use leptos_dom::{Child, Element, IntoChild};
|
||||
use leptos_macro::Props;
|
||||
use leptos_reactive::{Scope, SuspenseContext};
|
||||
use leptos_reactive::{provide_context, Scope, SuspenseContext};
|
||||
|
||||
#[derive(Props)]
|
||||
pub struct SuspenseProps<F, C, G>
|
||||
pub struct SuspenseProps<F, G>
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
C: IntoChild + Clone,
|
||||
G: Fn() -> C,
|
||||
G: Fn() -> Element,
|
||||
{
|
||||
fallback: F,
|
||||
children: G,
|
||||
children: Vec<G>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Suspense<F, C, G>(cx: Scope, props: SuspenseProps<F, C, G>) -> impl Fn() -> Child
|
||||
pub fn Suspense<F, C, G>(cx: Scope, mut props: SuspenseProps<F, G>) -> impl Fn() -> Child
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
C: IntoChild + Clone,
|
||||
G: Fn() -> C,
|
||||
G: Fn() -> Element,
|
||||
{
|
||||
let context = SuspenseContext::new(cx);
|
||||
|
||||
// provide this SuspenseContext to any resources below it
|
||||
cx.provide_context(context);
|
||||
provide_context(cx, context);
|
||||
|
||||
move || {
|
||||
if context.ready() || cx.transition_pending() {
|
||||
(props.children)().into_child(cx)
|
||||
(props.children.iter().map(|child| (child)()))
|
||||
.collect::<Vec<_>>()
|
||||
.into_child(cx)
|
||||
} else {
|
||||
props.fallback.clone().into_child(cx)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::rc::Rc;
|
||||
use std::{any::Any, rc::Rc};
|
||||
|
||||
use leptos_reactive::Scope;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
|
|
@ -66,7 +66,6 @@ where
|
|||
pub fn create_component<F, T>(cx: Scope, f: F) -> T
|
||||
where
|
||||
F: Fn() -> T,
|
||||
T: IntoChild,
|
||||
{
|
||||
// TODO hydration logic here
|
||||
cx.untrack(f)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use leptos_reactive::Scope;
|
||||
use leptos_reactive::{create_render_effect, Scope};
|
||||
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||
|
||||
use crate::{
|
||||
|
@ -11,7 +11,7 @@ pub fn attribute(cx: Scope, el: &web_sys::Element, attr_name: &'static str, valu
|
|||
match value {
|
||||
Attribute::Fn(f) => {
|
||||
let el = el.clone();
|
||||
cx.create_render_effect(move |_| attribute_expression(&el, attr_name, f()));
|
||||
create_render_effect(cx, move |_| attribute_expression(&el, attr_name, f()));
|
||||
}
|
||||
_ => attribute_expression(el, attr_name, value),
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ pub fn property(cx: Scope, el: &web_sys::Element, prop_name: &'static str, value
|
|||
match value {
|
||||
Property::Fn(f) => {
|
||||
let el = el.clone();
|
||||
cx.create_render_effect(move |_| property_expression(&el, prop_name, f()));
|
||||
create_render_effect(cx, move |_| property_expression(&el, prop_name, f()));
|
||||
}
|
||||
Property::Value(value) => property_expression(el, prop_name, value),
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ pub fn class(cx: Scope, el: &web_sys::Element, class_name: &'static str, value:
|
|||
match value {
|
||||
Class::Fn(f) => {
|
||||
let el = el.clone();
|
||||
cx.create_render_effect(move |_| class_expression(&el, class_name, f()));
|
||||
create_render_effect(cx, move |_| class_expression(&el, class_name, f()));
|
||||
}
|
||||
Class::Value(value) => class_expression(el, class_name, value),
|
||||
}
|
||||
|
@ -65,9 +65,10 @@ fn class_expression(el: &web_sys::Element, class_name: &str, value: bool) {
|
|||
pub fn insert(
|
||||
cx: Scope,
|
||||
parent: web_sys::Node,
|
||||
value: Child,
|
||||
mut value: Child,
|
||||
before: Option<web_sys::Node>,
|
||||
initial: Option<Child>,
|
||||
multi: bool,
|
||||
) {
|
||||
/* let initial = if before.is_some() && initial.is_none() {
|
||||
Some(Child::Nodes(vec![]))
|
||||
|
@ -75,16 +76,25 @@ pub fn insert(
|
|||
initial
|
||||
}; */
|
||||
|
||||
/* while let Some(Child::Fn(f)) = current {
|
||||
current = Some(f());
|
||||
log::debug!(
|
||||
"inserting {value:?} into {} before {:?} with initial {:?}",
|
||||
parent.node_name(),
|
||||
before.as_ref().map(|n| n.node_name()),
|
||||
initial
|
||||
);
|
||||
|
||||
/* while let Child::Fn(f) = value {
|
||||
value = f();
|
||||
log::debug!("insert Fn value = {value:?}");
|
||||
} */
|
||||
|
||||
match value {
|
||||
Child::Fn(f) => {
|
||||
cx.create_render_effect(move |current| {
|
||||
let current = current
|
||||
create_render_effect(cx, move |current| {
|
||||
let mut current = current
|
||||
.unwrap_or_else(|| initial.clone())
|
||||
.unwrap_or(Child::Null);
|
||||
|
||||
let mut value = f();
|
||||
while let Child::Fn(f) = value {
|
||||
value = f();
|
||||
|
@ -92,9 +102,10 @@ pub fn insert(
|
|||
|
||||
Some(insert_expression(
|
||||
parent.clone().unchecked_into(),
|
||||
&f(),
|
||||
&value,
|
||||
current,
|
||||
before.as_ref(),
|
||||
multi,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
@ -104,6 +115,7 @@ pub fn insert(
|
|||
&value,
|
||||
initial.unwrap_or(Child::Null),
|
||||
before.as_ref(),
|
||||
multi,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -114,11 +126,17 @@ pub fn insert_expression(
|
|||
new_value: &Child,
|
||||
mut current: Child,
|
||||
before: Option<&web_sys::Node>,
|
||||
multi: bool,
|
||||
) -> Child {
|
||||
log::debug!(
|
||||
"insert_expression {new_value:?} into {} before {:?} with current {current:?} and multi = {multi}",
|
||||
parent.node_name(),
|
||||
before.map(|b| b.node_name())
|
||||
);
|
||||
if new_value == ¤t {
|
||||
log::debug!("insert_expression: values are equal");
|
||||
current
|
||||
} else {
|
||||
let multi = before.is_some();
|
||||
let parent = if multi {
|
||||
match ¤t {
|
||||
Child::Nodes(nodes) => nodes
|
||||
|
@ -140,15 +158,19 @@ pub fn insert_expression(
|
|||
remove_child(&parent, &old_node);
|
||||
Child::Null
|
||||
} else {
|
||||
clean_children(&parent, current, before, None)
|
||||
clean_children(&parent, current, before, None, multi)
|
||||
}
|
||||
}
|
||||
// if it's a new text value, set that text value
|
||||
Child::Text(data) => insert_str(&parent, data, before, multi, current),
|
||||
Child::Node(node) => match current {
|
||||
Child::Nodes(current) => {
|
||||
clean_children(&parent, Child::Nodes(current), before, Some(node.clone()))
|
||||
}
|
||||
Child::Nodes(current) => clean_children(
|
||||
&parent,
|
||||
Child::Nodes(current),
|
||||
before,
|
||||
Some(node.clone()),
|
||||
multi,
|
||||
),
|
||||
Child::Null => Child::Node(append_child(&parent, node)),
|
||||
Child::Text(current_text) => {
|
||||
if current_text.is_empty() {
|
||||
|
@ -178,7 +200,7 @@ pub fn insert_expression(
|
|||
},
|
||||
Child::Nodes(new_nodes) => {
|
||||
if new_nodes.is_empty() {
|
||||
clean_children(&parent, current, before, None)
|
||||
clean_children(&parent, current, before, None, multi)
|
||||
} else if let Child::Nodes(ref mut current_nodes) = current {
|
||||
if current_nodes.is_empty() {
|
||||
Child::Nodes(append_nodes(&parent, new_nodes, before))
|
||||
|
@ -187,19 +209,20 @@ pub fn insert_expression(
|
|||
Child::Nodes(new_nodes.to_vec())
|
||||
}
|
||||
} else {
|
||||
clean_children(&parent, Child::Null, None, None);
|
||||
clean_children(&parent, Child::Null, None, None, multi);
|
||||
append_nodes(&parent, new_nodes, before);
|
||||
Child::Nodes(new_nodes.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
// Nested Signals here simply won't do anything; they should be flattened so it's a single Signal
|
||||
Child::Fn(_) => {
|
||||
debug_warn!(
|
||||
"{}: Child<Fn<'a, Child<Fn<'a, ...>>> should be flattened.",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
current
|
||||
Child::Fn(f) => {
|
||||
let mut value = f();
|
||||
while let Child::Fn(f) = value {
|
||||
value = f();
|
||||
log::debug!("insert_expression Fn value = {value:?}");
|
||||
}
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -212,6 +235,12 @@ pub fn insert_str(
|
|||
multi: bool,
|
||||
current: Child,
|
||||
) -> Child {
|
||||
log::debug!(
|
||||
"insert_str {data:?} into {} before {:?} with current {current:?} and multi = {multi}",
|
||||
parent.node_name(),
|
||||
before.map(|b| b.node_name())
|
||||
);
|
||||
|
||||
if multi {
|
||||
let node = if let Child::Nodes(nodes) = ¤t {
|
||||
if let Some(node) = nodes.get(0) {
|
||||
|
@ -233,7 +262,7 @@ pub fn insert_str(
|
|||
} else {
|
||||
create_text_node(data).unchecked_into()
|
||||
};
|
||||
clean_children(parent, current, before, Some(node))
|
||||
clean_children(parent, current, before, Some(node), multi)
|
||||
} else {
|
||||
match current {
|
||||
Child::Text(_) => match before {
|
||||
|
@ -286,42 +315,45 @@ fn clean_children(
|
|||
current: Child,
|
||||
marker: Option<&web_sys::Node>,
|
||||
replacement: Option<web_sys::Node>,
|
||||
multi: bool,
|
||||
) -> Child {
|
||||
match marker {
|
||||
None => {
|
||||
parent.set_text_content(Some(""));
|
||||
Child::Null
|
||||
}
|
||||
Some(marker) => {
|
||||
let node = replacement.unwrap_or_else(|| create_text_node("").unchecked_into());
|
||||
log::debug!(
|
||||
"clean_children on {} before {:?} with current {current:?} and replacement {replacement:?} and multi = {multi}",
|
||||
parent.node_name(),
|
||||
marker.map(|b| b.node_name())
|
||||
);
|
||||
|
||||
match current {
|
||||
Child::Null => Child::Node(insert_before(parent, &node, Some(marker))),
|
||||
Child::Text(_) => Child::Node(insert_before(parent, &node, Some(marker))),
|
||||
Child::Node(node) => Child::Node(insert_before(parent, &node, Some(marker))),
|
||||
Child::Nodes(nodes) => {
|
||||
let mut inserted = false;
|
||||
let mut result = Vec::new();
|
||||
for (idx, el) in nodes.iter().enumerate().rev() {
|
||||
if &node != el {
|
||||
let is_parent =
|
||||
el.parent_node() == Some(parent.clone().unchecked_into());
|
||||
if !inserted && idx == 0 {
|
||||
if is_parent {
|
||||
replace_child(parent, &node, el);
|
||||
result.push(node.clone())
|
||||
} else {
|
||||
result.push(insert_before(parent, &node, Some(marker)))
|
||||
}
|
||||
if marker.is_none() && !multi {
|
||||
parent.set_text_content(Some(""));
|
||||
Child::Null
|
||||
} else {
|
||||
let node = replacement.unwrap_or_else(|| create_text_node("").unchecked_into());
|
||||
|
||||
match current {
|
||||
Child::Null => Child::Node(insert_before(parent, &node, marker)),
|
||||
Child::Text(_) => Child::Node(insert_before(parent, &node, marker)),
|
||||
Child::Node(node) => Child::Node(insert_before(parent, &node, marker)),
|
||||
Child::Nodes(nodes) => {
|
||||
let mut inserted = false;
|
||||
let mut result = Vec::new();
|
||||
for (idx, el) in nodes.iter().enumerate().rev() {
|
||||
if &node != el {
|
||||
let is_parent = el.parent_node() == Some(parent.clone().unchecked_into());
|
||||
if !inserted && idx == 0 {
|
||||
if is_parent {
|
||||
replace_child(parent, &node, el);
|
||||
result.push(node.clone())
|
||||
} else {
|
||||
result.push(insert_before(parent, &node, marker))
|
||||
}
|
||||
} else {
|
||||
inserted = true;
|
||||
}
|
||||
} else {
|
||||
inserted = true;
|
||||
}
|
||||
Child::Nodes(result)
|
||||
}
|
||||
Child::Fn(_) => todo!(),
|
||||
Child::Nodes(result)
|
||||
}
|
||||
Child::Fn(_) => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,3 +12,4 @@ proc-macro2 = "1"
|
|||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
syn-rsx = "0.8.1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
|
|
@ -2,14 +2,22 @@ use proc_macro2::{Ident, Span, TokenStream};
|
|||
use quote::{quote, quote_spanned};
|
||||
use syn::{spanned::Spanned, ExprPath};
|
||||
use syn_rsx::{Node, NodeName, NodeType};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::is_component_node;
|
||||
|
||||
pub fn client_side_rendering(nodes: &[Node]) -> TokenStream {
|
||||
let template_uid = Ident::new(
|
||||
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
|
||||
Span::call_site(),
|
||||
);
|
||||
|
||||
if nodes.len() == 1 {
|
||||
first_node_to_tokens(&nodes[0])
|
||||
first_node_to_tokens(&template_uid, &nodes[0])
|
||||
} else {
|
||||
let nodes = nodes.iter().map(first_node_to_tokens);
|
||||
let nodes = nodes
|
||||
.iter()
|
||||
.map(|node| first_node_to_tokens(&template_uid, node));
|
||||
quote! {
|
||||
{
|
||||
vec![
|
||||
|
@ -20,11 +28,14 @@ pub fn client_side_rendering(nodes: &[Node]) -> TokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
fn first_node_to_tokens(node: &Node) -> TokenStream {
|
||||
fn first_node_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
|
||||
match node.node_type {
|
||||
NodeType::Doctype | NodeType::Comment => quote! {},
|
||||
NodeType::Fragment => {
|
||||
let nodes = node.children.iter().map(first_node_to_tokens);
|
||||
let nodes = node
|
||||
.children
|
||||
.iter()
|
||||
.map(|node| first_node_to_tokens(template_uid, node));
|
||||
quote! {
|
||||
{
|
||||
vec![
|
||||
|
@ -33,7 +44,7 @@ fn first_node_to_tokens(node: &Node) -> TokenStream {
|
|||
}
|
||||
}
|
||||
}
|
||||
NodeType::Element => root_element_to_tokens(node),
|
||||
NodeType::Element => root_element_to_tokens(template_uid, node),
|
||||
NodeType::Block => node
|
||||
.value
|
||||
.as_ref()
|
||||
|
@ -43,7 +54,7 @@ fn first_node_to_tokens(node: &Node) -> TokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
fn root_element_to_tokens(node: &Node) -> TokenStream {
|
||||
fn root_element_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
|
||||
let mut template = String::new();
|
||||
let mut navigations = Vec::new();
|
||||
let mut expressions = Vec::new();
|
||||
|
@ -63,8 +74,10 @@ fn root_element_to_tokens(node: &Node) -> TokenStream {
|
|||
|
||||
quote! {
|
||||
{
|
||||
let template = #template;
|
||||
let root = leptos_dom::clone_template(&leptos_dom::create_template(template));
|
||||
thread_local! {
|
||||
static #template_uid: web_sys::HtmlTemplateElement = leptos_dom::create_template(#template);
|
||||
};
|
||||
let root = #template_uid.with(|template| leptos_dom::clone_template(template));
|
||||
|
||||
#(#navigations);*
|
||||
#(#expressions);*;
|
||||
|
@ -150,6 +163,7 @@ fn element_to_tokens(
|
|||
|
||||
// iterate over children
|
||||
let mut prev_sib = prev_sib;
|
||||
let multi = node.children.len() >= 2;
|
||||
for (idx, child) in node.children.iter().enumerate() {
|
||||
// set next sib (for any insertions)
|
||||
let next_sib = node.children.get(idx + 1).and_then(|next_sib| {
|
||||
|
@ -169,6 +183,7 @@ fn element_to_tokens(
|
|||
template,
|
||||
navigations,
|
||||
expressions,
|
||||
multi,
|
||||
);
|
||||
|
||||
prev_sib = match curr_id {
|
||||
|
@ -302,11 +317,12 @@ fn child_to_tokens(
|
|||
template: &mut String,
|
||||
navigations: &mut Vec<TokenStream>,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
multi: bool,
|
||||
) -> PrevSibChange {
|
||||
match node.node_type {
|
||||
NodeType::Element => {
|
||||
if is_component_node(node) {
|
||||
component_to_tokens(node, Some(parent), next_sib, expressions)
|
||||
component_to_tokens(node, Some(parent), next_sib, expressions, multi)
|
||||
} else {
|
||||
PrevSibChange::Sib(element_to_tokens(
|
||||
node,
|
||||
|
@ -370,6 +386,7 @@ fn child_to_tokens(
|
|||
#value.into_child(cx),
|
||||
#before,
|
||||
None,
|
||||
#multi,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -386,6 +403,7 @@ fn component_to_tokens(
|
|||
parent: Option<&Ident>,
|
||||
next_sib: Option<Ident>,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
multi: bool,
|
||||
) -> PrevSibChange {
|
||||
let create_component = create_component(node);
|
||||
|
||||
|
@ -402,6 +420,7 @@ fn component_to_tokens(
|
|||
#create_component.into_child(cx),
|
||||
#before,
|
||||
None,
|
||||
#multi
|
||||
);
|
||||
});
|
||||
} else {
|
||||
|
@ -416,34 +435,74 @@ fn create_component(node: &Node) -> TokenStream {
|
|||
let span = node.name_span().unwrap();
|
||||
let component_props_name = Ident::new(&format!("{component_name}Props"), span);
|
||||
|
||||
let children = if !node.children.is_empty() {
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else if node.children.len() == 1 {
|
||||
let child = client_side_rendering(&node.children);
|
||||
quote! { .children(vec![#child]) }
|
||||
} else {
|
||||
let children = client_side_rendering(&node.children);
|
||||
quote! { .children(#children) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let props = node.attributes.iter().map(|attr| {
|
||||
let name = ident_from_tag_name(attr.name.as_ref().unwrap());
|
||||
let value = attr.value.as_ref().expect("component props need values");
|
||||
let span = attr.name_span().unwrap();
|
||||
quote_spanned! {
|
||||
span => .#name(#value)
|
||||
let props = node.attributes.iter().filter_map(|attr| {
|
||||
let attr_name = attr.name_as_string().unwrap_or_default();
|
||||
if attr_name.strip_prefix("on:").is_some() {
|
||||
None
|
||||
} else {
|
||||
let name = ident_from_tag_name(attr.name.as_ref().unwrap());
|
||||
let value = attr.value.as_ref().expect("component props need values");
|
||||
let span = attr.name_span().unwrap();
|
||||
Some(quote_spanned! {
|
||||
span => .#name(#value)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let mut events = node.attributes.iter().filter_map(|attr| {
|
||||
let attr_name = attr.name_as_string().unwrap_or_default();
|
||||
if let Some(event_name) = attr_name.strip_prefix("on:") {
|
||||
let span = attr.name_span().unwrap();
|
||||
let handler = attr
|
||||
.value
|
||||
.as_ref()
|
||||
.expect("event listener attributes need a value");
|
||||
Some(quote_spanned! {
|
||||
span => add_event_listener(#component_name.unchecked_ref(), #event_name, #handler)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).peekable();
|
||||
|
||||
// TODO children
|
||||
|
||||
quote_spanned! {
|
||||
span => create_component(cx, || {
|
||||
#component_name(
|
||||
cx,
|
||||
#component_props_name::builder()
|
||||
#(#props)*
|
||||
#children
|
||||
.build(),
|
||||
)
|
||||
})
|
||||
if events.peek().is_none() {
|
||||
quote_spanned! {
|
||||
span => create_component(cx, move || {
|
||||
#component_name(
|
||||
cx,
|
||||
#component_props_name::builder()
|
||||
#(#props)*
|
||||
#children
|
||||
.build(),
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {
|
||||
span => create_component(cx, move || {
|
||||
let #component_name = #component_name(
|
||||
cx,
|
||||
#component_props_name::builder()
|
||||
#(#props)*
|
||||
#children
|
||||
.build(),
|
||||
);
|
||||
#(#events);*;
|
||||
#component_name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,34 +2,32 @@ use std::any::{Any, TypeId};
|
|||
|
||||
use crate::Scope;
|
||||
|
||||
impl Scope {
|
||||
pub fn provide_context<T>(self, value: T)
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
let id = value.type_id();
|
||||
self.runtime.scope(self.id, |scope_state| {
|
||||
scope_state
|
||||
.contexts
|
||||
.borrow_mut()
|
||||
.insert(id, Box::new(value));
|
||||
})
|
||||
}
|
||||
|
||||
pub fn use_context<T>(self) -> Option<T>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
let id = TypeId::of::<T>();
|
||||
self.runtime.scope(self.id, |scope_state| {
|
||||
let contexts = scope_state.contexts.borrow();
|
||||
let local_value = contexts.get(&id).and_then(|val| val.downcast_ref::<T>());
|
||||
match local_value {
|
||||
Some(val) => Some(val.clone()),
|
||||
None => scope_state
|
||||
.parent
|
||||
.and_then(|parent| parent.use_context::<T>()),
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn provide_context<T>(cx: Scope, value: T)
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
let id = value.type_id();
|
||||
cx.runtime.scope(cx.id, |scope_state| {
|
||||
scope_state
|
||||
.contexts
|
||||
.borrow_mut()
|
||||
.insert(id, Box::new(value));
|
||||
})
|
||||
}
|
||||
|
||||
pub fn use_context<T>(cx: Scope) -> Option<T>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
let id = TypeId::of::<T>();
|
||||
cx.runtime.scope(cx.id, |scope_state| {
|
||||
let contexts = scope_state.contexts.borrow();
|
||||
let local_value = contexts.get(&id).and_then(|val| val.downcast_ref::<T>());
|
||||
match local_value {
|
||||
Some(val) => Some(val.clone()),
|
||||
None => scope_state
|
||||
.parent
|
||||
.and_then(|parent| use_context::<T>(parent)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::{
|
|||
fmt::Debug,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
|
@ -11,14 +12,15 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::{
|
||||
create_effect, create_memo, create_signal, queue_microtask, runtime::Runtime,
|
||||
spawn::spawn_local, Memo, ReadSignal, Scope, ScopeId, SuspenseContext, WriteSignal,
|
||||
spawn::spawn_local, use_context, Memo, ReadSignal, Scope, ScopeId, SuspenseContext,
|
||||
WriteSignal,
|
||||
};
|
||||
|
||||
pub fn create_resource<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: ReadSignal<S>,
|
||||
fetcher: impl Fn(&S) -> Fu + 'static,
|
||||
) -> Resource<S, T, Fu>
|
||||
source: impl Fn() -> S + 'static,
|
||||
fetcher: impl Fn(S) -> Fu + 'static,
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
|
@ -29,10 +31,10 @@ where
|
|||
|
||||
pub fn create_resource_with_initial_value<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: ReadSignal<S>,
|
||||
fetcher: impl Fn(&S) -> Fu + 'static,
|
||||
source: impl Fn() -> S + 'static,
|
||||
fetcher: impl Fn(S) -> Fu + 'static,
|
||||
initial_value: Option<T>,
|
||||
) -> Resource<S, T, Fu>
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
|
@ -42,7 +44,7 @@ where
|
|||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
let (loading, set_loading) = create_signal(cx, false);
|
||||
let (track, trigger) = create_signal(cx, 0);
|
||||
let fetcher = Rc::new(fetcher);
|
||||
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
|
||||
let source = create_memo(cx, move |_| source());
|
||||
|
||||
// TODO hydration/streaming logic
|
||||
|
@ -76,61 +78,56 @@ where
|
|||
id,
|
||||
source_ty: PhantomData,
|
||||
out_ty: PhantomData,
|
||||
fut_ty: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T, Fu> Resource<S, T, Fu>
|
||||
impl<S, T> Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
pub fn read(&self) -> Option<T> {
|
||||
self.runtime.resource(
|
||||
(self.scope, self.id),
|
||||
|resource: &ResourceState<S, T, Fu>| resource.read(),
|
||||
)
|
||||
self.runtime
|
||||
.resource((self.scope, self.id), |resource: &ResourceState<S, T>| {
|
||||
resource.read()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn loading(&self) -> bool {
|
||||
self.runtime.resource(
|
||||
(self.scope, self.id),
|
||||
|resource: &ResourceState<S, T, Fu>| resource.loading.get(),
|
||||
)
|
||||
self.runtime
|
||||
.resource((self.scope, self.id), |resource: &ResourceState<S, T>| {
|
||||
resource.loading.get()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refetch(&self) {
|
||||
self.runtime.resource(
|
||||
(self.scope, self.id),
|
||||
|resource: &ResourceState<S, T, Fu>| resource.refetch(),
|
||||
)
|
||||
self.runtime
|
||||
.resource((self.scope, self.id), |resource: &ResourceState<S, T>| {
|
||||
resource.refetch()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Resource<S, T, Fu>
|
||||
pub struct Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
runtime: &'static Runtime,
|
||||
pub(crate) scope: ScopeId,
|
||||
pub(crate) id: ResourceId,
|
||||
pub(crate) source_ty: PhantomData<S>,
|
||||
pub(crate) out_ty: PhantomData<T>,
|
||||
pub(crate) fut_ty: PhantomData<Fu>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub(crate) struct ResourceId(pub(crate) usize);
|
||||
|
||||
impl<S, T, Fu> Clone for Resource<S, T, Fu>
|
||||
impl<S, T> Clone for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
|
@ -139,25 +136,22 @@ where
|
|||
id: self.id,
|
||||
source_ty: PhantomData,
|
||||
out_ty: PhantomData,
|
||||
fut_ty: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T, Fu> Copy for Resource<S, T, Fu>
|
||||
impl<S, T> Copy for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ResourceState<S, T, Fu>
|
||||
pub struct ResourceState<S, T>
|
||||
where
|
||||
S: 'static,
|
||||
T: Clone + Debug + 'static,
|
||||
Fu: Future<Output = T>,
|
||||
{
|
||||
scope: Scope,
|
||||
value: ReadSignal<Option<T>>,
|
||||
|
@ -167,20 +161,19 @@ where
|
|||
track: ReadSignal<usize>,
|
||||
trigger: WriteSignal<usize>,
|
||||
source: Memo<S>,
|
||||
fetcher: Rc<dyn Fn(&S) -> Fu>,
|
||||
fetcher: Rc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
|
||||
resolved: Rc<Cell<bool>>,
|
||||
scheduled: Rc<Cell<bool>>,
|
||||
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
|
||||
}
|
||||
|
||||
impl<S, T, Fu> ResourceState<S, T, Fu>
|
||||
impl<S, T> ResourceState<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
pub fn read(&self) -> Option<T> {
|
||||
let suspense_cx = self.scope.use_context::<SuspenseContext>();
|
||||
let suspense_cx = use_context::<SuspenseContext>(self.scope);
|
||||
|
||||
let v = self.value.get();
|
||||
|
||||
|
@ -219,7 +212,7 @@ where
|
|||
|
||||
let loaded_under_transition = self.scope.runtime.running_transition().is_some();
|
||||
|
||||
let fut = (self.fetcher)(&self.source.get());
|
||||
let fut = (self.fetcher)(self.source.get());
|
||||
|
||||
// `scheduled` is true for the rest of this code only
|
||||
self.scheduled.set(true);
|
||||
|
|
|
@ -25,6 +25,10 @@ impl Runtime {
|
|||
if let Some(scope) = scope {
|
||||
(f)(&scope)
|
||||
} else {
|
||||
log::error!(
|
||||
"couldn't locate {id:?} in scopes {:#?}",
|
||||
self.scopes.borrow()
|
||||
);
|
||||
panic!("couldn't locate {id:?}");
|
||||
}
|
||||
}
|
||||
|
@ -91,26 +95,24 @@ impl Runtime {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn resource<S, T, Fu, U>(
|
||||
pub fn resource<S, T, U>(
|
||||
&self,
|
||||
id: (ScopeId, ResourceId),
|
||||
f: impl FnOnce(&ResourceState<S, T, Fu>) -> U,
|
||||
f: impl FnOnce(&ResourceState<S, T>) -> U,
|
||||
) -> U
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
self.scope(id.0, |scope| {
|
||||
if let Some(n) = scope.resources.get(id.1 .0) {
|
||||
if let Some(n) = n.downcast_ref::<ResourceState<S, T, Fu>>() {
|
||||
if let Some(n) = n.downcast_ref::<ResourceState<S, T>>() {
|
||||
f(n)
|
||||
} else {
|
||||
panic!(
|
||||
"couldn't convert {id:?} to ResourceState<{}, {}, {}>",
|
||||
"couldn't convert {id:?} to ResourceState<{}, {}>",
|
||||
std::any::type_name::<S>(),
|
||||
std::any::type_name::<T>(),
|
||||
std::any::type_name::<Fu>()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -71,11 +71,10 @@ impl Scope {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn push_resource<S, T, Fu>(&self, state: Rc<ResourceState<S, T, Fu>>) -> ResourceId
|
||||
pub(crate) fn push_resource<S, T>(&self, state: Rc<ResourceState<S, T>>) -> ResourceId
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
self.runtime.scope(self.id, |scope| {
|
||||
scope.resources.push(state);
|
||||
|
|
|
@ -12,11 +12,22 @@ common_macros = "0.1"
|
|||
lazy_static = "1"
|
||||
linear-map = "1"
|
||||
log = "0.4"
|
||||
regex = "1"
|
||||
regex = { version = "1", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
url = { version = "2", optional = true }
|
||||
urlencoding = "2"
|
||||
thiserror = "1"
|
||||
web-sys = { version = "0.3", optional = true }
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"History",
|
||||
"HtmlAnchorElement",
|
||||
"MouseEvent",
|
||||
"Url"
|
||||
]
|
||||
|
||||
[features]
|
||||
browser = ["dep:web-sys", "dep:js-sys"]
|
||||
default = ["dep:url", "dep:regex"]
|
||||
browser = ["dep:js-sys"]
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
mod route;
|
||||
mod router;
|
||||
mod routes;
|
||||
|
||||
use leptos_dom::IntoChild;
|
||||
use leptos_reactive::Scope;
|
||||
|
||||
pub use route::*;
|
||||
pub use router::*;
|
||||
pub use routes::*;
|
||||
|
||||
use crate::use_route;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Outlet(cx: Scope) -> impl IntoChild {
|
||||
let route = use_route(cx);
|
||||
move || route.child().map(|child| child.outlet())
|
||||
}
|
||||
|
|
|
@ -1,13 +1,112 @@
|
|||
use std::borrow::Cow;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
fmt::Debug,
|
||||
rc::{Rc, Weak},
|
||||
};
|
||||
|
||||
pub struct RouteContext {}
|
||||
use leptos_dom::{Child, IntoChild};
|
||||
|
||||
use crate::{DataFunction, ParamsMap, PathMatch, Route, RouteMatch, RouterContext};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouteDefinition {
|
||||
pub path: Vec<String>,
|
||||
pub data: Option<DataFunction>,
|
||||
pub children: Vec<RouteDefinition>,
|
||||
pub component: Child,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouteContext {
|
||||
pub(crate) inner: Rc<RouteContextInner>,
|
||||
}
|
||||
|
||||
pub(crate) struct RouteContextInner {
|
||||
pub(crate) parent: Option<RouteContext>,
|
||||
pub(crate) get_child: Box<dyn Fn() -> Option<RouteContext>>,
|
||||
pub(crate) data: Option<Box<dyn Any>>,
|
||||
pub(crate) path: String,
|
||||
pub(crate) params: ParamsMap,
|
||||
pub(crate) outlet: Box<dyn Fn() -> Option<Child>>,
|
||||
}
|
||||
|
||||
impl Debug for RouteContextInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RouteContextInner")
|
||||
.field("parent", &self.parent)
|
||||
.field("path", &self.path)
|
||||
.field("ParamsMap", &self.params)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_route_context(
|
||||
router: &RouterContext,
|
||||
parent: &RouteContext,
|
||||
child: impl Fn() -> Option<RouteContext> + 'static,
|
||||
matcher: impl Fn() -> RouteMatch,
|
||||
) -> RouteContext {
|
||||
let location = &router.inner.location;
|
||||
let base = &router.inner.base;
|
||||
let RouteMatch { path_match, route } = matcher();
|
||||
let component = route.key.component.clone();
|
||||
let PathMatch { path, params } = path_match;
|
||||
log::debug!("in create_route_context, params = {params:?}");
|
||||
let Route {
|
||||
key,
|
||||
pattern,
|
||||
original_path,
|
||||
matcher,
|
||||
} = route;
|
||||
let get_child = Box::new(child);
|
||||
|
||||
RouteContext {
|
||||
inner: Rc::new(RouteContextInner {
|
||||
parent: Some(parent.clone()),
|
||||
get_child,
|
||||
data: None, // TODO route data,
|
||||
path,
|
||||
params,
|
||||
outlet: Box::new(move || Some(component.clone())),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
impl RouteContext {
|
||||
pub fn new(path: &str) -> Self {
|
||||
Self {}
|
||||
pub fn base(path: &str) -> Self {
|
||||
Self {
|
||||
inner: Rc::new(RouteContextInner {
|
||||
parent: None,
|
||||
get_child: Box::new(|| None),
|
||||
data: None,
|
||||
path: path.to_string(),
|
||||
params: ParamsMap::new(),
|
||||
outlet: Box::new(|| None),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_path(&self, to: &str) -> Option<Cow<str>> {
|
||||
log::debug!("RouteContext::resolve_path");
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub(crate) fn child(&self) -> Option<RouteContext> {
|
||||
(self.inner.get_child)()
|
||||
}
|
||||
|
||||
pub fn outlet(&self) -> impl IntoChild {
|
||||
log::debug!("looking for outlet A");
|
||||
let o = (self.inner.outlet)();
|
||||
log::debug!("outlet = {o:#?}");
|
||||
o
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RouteDefinition {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path
|
||||
&& self.children == other.children
|
||||
&& self.component == other.component
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
use std::{any::Any, cell::RefCell, future::Future};
|
||||
use std::{any::Any, cell::RefCell, future::Future, rc::Rc};
|
||||
|
||||
use leptos_dom::IntoChild;
|
||||
use leptos_reactive::{ReadSignal, Scope, WriteSignal};
|
||||
use leptos_core as leptos;
|
||||
use leptos_dom::{location, window_event_listener, IntoChild};
|
||||
use leptos_macro::{view, Props};
|
||||
use leptos_reactive::{
|
||||
create_effect, create_render_effect, create_signal, provide_context, use_transition,
|
||||
ReadSignal, Scope, WriteSignal,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
create_location, resolve_path, DataFunction, HistoryIntegration, Integration, Location,
|
||||
LocationChange, Params, RouteContext, State,
|
||||
create_location, integrations, resolve_path, unescape, DataFunction, Location, LocationChange,
|
||||
ParamsMap, RouteContext, State, Url,
|
||||
};
|
||||
|
||||
#[derive(Props)]
|
||||
pub struct RouterProps<C, D, Fu, T>
|
||||
where
|
||||
C: IntoChild,
|
||||
D: Fn(Params, Location) -> Fu + Clone + 'static,
|
||||
D: Fn(ParamsMap, Location) -> Fu + Clone + 'static,
|
||||
Fu: Future<Output = T>,
|
||||
T: Any + 'static,
|
||||
{
|
||||
|
@ -22,30 +28,39 @@ where
|
|||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Router<C, D, Fu, T>(cx: Scope, props: RouterProps<C, D, Fu, T>) -> C
|
||||
pub fn Router<C, D, Fu, T>(cx: Scope, props: RouterProps<C, D, Fu, T>) -> impl IntoChild
|
||||
where
|
||||
C: IntoChild,
|
||||
D: Fn(Params, Location) -> Fu + Clone + 'static,
|
||||
D: Fn(ParamsMap, Location) -> Fu + Clone + 'static,
|
||||
Fu: Future<Output = T>,
|
||||
T: Any + 'static,
|
||||
{
|
||||
let integration = HistoryIntegration {};
|
||||
cx.provide_context(RouterContext::new(
|
||||
provide_context(
|
||||
cx,
|
||||
integration,
|
||||
props.base,
|
||||
props.data.map(|data| DataFunction::from(data)),
|
||||
));
|
||||
RouterContext::new(
|
||||
cx,
|
||||
props.base,
|
||||
props.data.map(|data| DataFunction::from(data)),
|
||||
),
|
||||
);
|
||||
|
||||
log::debug!("provided RouterContext");
|
||||
|
||||
props.children
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouterContext {
|
||||
pub(crate) inner: Rc<RouterContextInner>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RouterContextInner {
|
||||
pub location: Location,
|
||||
pub base: RouteContext,
|
||||
cx: Scope,
|
||||
reference: ReadSignal<String>,
|
||||
set_reference: WriteSignal<String>,
|
||||
referrers: RefCell<Vec<LocationChange>>,
|
||||
referrers: Rc<RefCell<Vec<LocationChange>>>,
|
||||
source: ReadSignal<LocationChange>,
|
||||
set_source: WriteSignal<LocationChange>,
|
||||
state: ReadSignal<State>,
|
||||
|
@ -53,13 +68,8 @@ pub struct RouterContext {
|
|||
}
|
||||
|
||||
impl RouterContext {
|
||||
pub fn new(
|
||||
cx: Scope,
|
||||
integration: impl Integration,
|
||||
base: Option<String>,
|
||||
data: Option<DataFunction>,
|
||||
) -> Self {
|
||||
let (source, set_source) = integration.normalize(cx);
|
||||
pub fn new(cx: Scope, base: Option<String>, data: Option<DataFunction>) -> Self {
|
||||
let (source, set_source) = integrations::normalize(cx);
|
||||
let base = base.unwrap_or_default();
|
||||
let base_path = resolve_path("", &base, None);
|
||||
if let Some(base_path) = &base_path && source.with(|s| s.value.is_empty()) {
|
||||
|
@ -70,19 +80,25 @@ impl RouterContext {
|
|||
state: State(None)
|
||||
});
|
||||
}
|
||||
let (reference, set_reference) = cx.create_signal(source.with(|s| s.value.clone()));
|
||||
let (state, set_state) = cx.create_signal(source.with(|s| s.state.clone()));
|
||||
let transition = cx.use_transition();
|
||||
let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone()));
|
||||
let (state, set_state) = create_signal(cx, source.with(|s| s.state.clone()));
|
||||
let transition = use_transition(cx);
|
||||
let location = create_location(cx, reference, state);
|
||||
let referrers: Vec<LocationChange> = Vec::new();
|
||||
let referrers: Rc<RefCell<Vec<LocationChange>>> = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let base_path = RouteContext::new(&base_path.unwrap_or_default());
|
||||
let base_path = base_path.map(|s| s.to_owned()).unwrap_or_default();
|
||||
let base = RouteContext::base(&base_path);
|
||||
|
||||
if let Some(data) = data {
|
||||
todo!()
|
||||
log::debug!("skipping data fn");
|
||||
// TODO
|
||||
}
|
||||
|
||||
cx.create_render_effect(move |_| {
|
||||
create_effect(cx, move |_| {
|
||||
log::debug!("location.path_name = {:?}", location.path_name.get())
|
||||
});
|
||||
|
||||
create_render_effect(cx, move |_| {
|
||||
let LocationChange { value, state, .. } = source();
|
||||
cx.untrack(move || {
|
||||
if value != reference() {
|
||||
|
@ -94,29 +110,44 @@ impl RouterContext {
|
|||
});
|
||||
});
|
||||
|
||||
// TODO handle anchor click
|
||||
|
||||
Self {
|
||||
let inner = Rc::new(RouterContextInner {
|
||||
location,
|
||||
base,
|
||||
cx,
|
||||
reference,
|
||||
set_reference,
|
||||
referrers: RefCell::new(referrers),
|
||||
referrers,
|
||||
source,
|
||||
set_source,
|
||||
state,
|
||||
set_state,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pub fn navigate_from_route(
|
||||
&self,
|
||||
route: &RouteContext,
|
||||
if cfg!(feature = "browser") {
|
||||
window_event_listener("click", {
|
||||
let inner = Rc::clone(&inner);
|
||||
move |ev| inner.clone().handle_anchor_click(ev)
|
||||
});
|
||||
// TODO on_cleanup remove event listener
|
||||
}
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl RouterContextInner {
|
||||
pub(crate) fn navigate_from_route(
|
||||
self: Rc<Self>,
|
||||
to: &str,
|
||||
options: &NavigateOptions,
|
||||
) -> Result<(), NavigationError> {
|
||||
self.cx.untrack(move || {
|
||||
let cx = self.cx;
|
||||
let this = Rc::clone(&self);
|
||||
|
||||
// TODO untrack causes an error here
|
||||
cx.untrack(move || {
|
||||
let resolved_to = if options.resolve {
|
||||
route.resolve_path(to)
|
||||
this.base.resolve_path(to)
|
||||
} else {
|
||||
resolve_path("", to, None)
|
||||
};
|
||||
|
@ -128,9 +159,7 @@ impl RouterContext {
|
|||
return Err(NavigationError::MaxRedirects);
|
||||
}
|
||||
|
||||
let current = self.reference.get();
|
||||
|
||||
if resolved_to != current || options.state != self.state.get() {
|
||||
if resolved_to != (this.reference)() || options.state != (this.state).get() {
|
||||
if cfg!(feature = "server") {
|
||||
// TODO server out
|
||||
self.set_source.update(|source| {
|
||||
|
@ -147,28 +176,34 @@ impl RouterContext {
|
|||
value: resolved_to.to_string(),
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: self.state.get(),
|
||||
state: State(None), // TODO state state.get(),
|
||||
});
|
||||
}
|
||||
let len = self.referrers.borrow().len();
|
||||
|
||||
let transition = self.cx.use_transition();
|
||||
transition.start(move || {
|
||||
self.set_reference.update({
|
||||
let resolved = resolved_to.to_string();
|
||||
move |r| *r = resolved
|
||||
});
|
||||
self.set_state.update({
|
||||
let next_state = options.state.clone();
|
||||
move |state| *state = next_state
|
||||
});
|
||||
if self.referrers.borrow().len() == len {
|
||||
self.navigate_end(LocationChange {
|
||||
value: resolved_to.to_string(),
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state: options.state.clone(),
|
||||
})
|
||||
let transition = use_transition(self.cx);
|
||||
transition.start({
|
||||
let set_reference = self.set_reference;
|
||||
let set_state = self.set_state;
|
||||
let referrers = self.referrers.clone();
|
||||
let this = Rc::clone(&self);
|
||||
move || {
|
||||
set_reference.update({
|
||||
let resolved = resolved_to.to_string();
|
||||
move |r| *r = resolved
|
||||
});
|
||||
set_state.update({
|
||||
let next_state = options.state.clone();
|
||||
move |state| *state = next_state
|
||||
});
|
||||
if referrers.borrow().len() == len {
|
||||
this.navigate_end(LocationChange {
|
||||
value: resolved_to.to_string(),
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state: options.state.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -180,11 +215,12 @@ impl RouterContext {
|
|||
})
|
||||
}
|
||||
|
||||
fn navigate_end(&self, next: LocationChange) {
|
||||
pub(crate) fn navigate_end(self: Rc<Self>, next: LocationChange) {
|
||||
let first = self.referrers.borrow().get(0).cloned();
|
||||
if let Some(first) = first {
|
||||
if next.value != first.value || next.state != first.state {
|
||||
self.set_source.update(|source| {
|
||||
let next = next.clone();
|
||||
self.set_source.update(move |source| {
|
||||
*source = next;
|
||||
source.replace = first.replace;
|
||||
source.scroll = first.scroll;
|
||||
|
@ -192,6 +228,87 @@ impl RouterContext {
|
|||
}
|
||||
self.referrers.borrow_mut().clear();
|
||||
}
|
||||
integrations::navigate(&next);
|
||||
}
|
||||
|
||||
pub(crate) fn handle_anchor_click(self: Rc<Self>, ev: web_sys::Event) {
|
||||
use leptos_dom::wasm_bindgen::JsCast;
|
||||
let ev = ev.unchecked_into::<web_sys::MouseEvent>();
|
||||
/* if ev.default_prevented()
|
||||
|| ev.button() != 0
|
||||
|| ev.meta_key()
|
||||
|| ev.alt_key()
|
||||
|| ev.ctrl_key()
|
||||
|| ev.shift_key()
|
||||
{
|
||||
log::debug!("branch A prevent");
|
||||
return;
|
||||
} */
|
||||
|
||||
let composed_path = ev.composed_path();
|
||||
let mut a: Option<web_sys::HtmlAnchorElement> = None;
|
||||
for i in 0..composed_path.length() {
|
||||
if let Ok(el) = composed_path
|
||||
.get(i)
|
||||
.dyn_into::<web_sys::HtmlAnchorElement>()
|
||||
{
|
||||
a = Some(el);
|
||||
}
|
||||
}
|
||||
if let Some(a) = a {
|
||||
let href = a.href();
|
||||
let target = a.target();
|
||||
|
||||
// let browser handle this event if link has target,
|
||||
// or if it doesn't have href or state
|
||||
/* if !target.is_empty() || (href.is_empty() && !a.has_attribute("state")) {
|
||||
log::debug!("target or href empty");
|
||||
ev.prevent_default();
|
||||
return;
|
||||
} */
|
||||
|
||||
let rel = a.get_attribute("rel").unwrap_or_default();
|
||||
let mut rel = rel.split([' ', '\t']);
|
||||
|
||||
// let browser handle event if it has rel=external or download
|
||||
if a.has_attribute("download") || rel.any(|p| p == "external") {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = Url::try_from(href.as_str()).unwrap();
|
||||
let path_name = unescape(&url.path_name);
|
||||
|
||||
// let browser handle this event if it leaves our domain
|
||||
// or our base path
|
||||
/* if url.origin != leptos_dom::location().origin().unwrap_or_default()
|
||||
|| (!base_path.is_empty()
|
||||
&& !path_name.is_empty()
|
||||
&& !path_name
|
||||
.to_lowercase()
|
||||
.starts_with(&base_path.to_lowercase()))
|
||||
{
|
||||
return;
|
||||
} */
|
||||
|
||||
let to = path_name + &unescape(&url.search) + &unescape(&url.hash);
|
||||
let state = a.get_attribute("state"); // TODO state
|
||||
|
||||
ev.prevent_default();
|
||||
log::debug!("navigate to {to}");
|
||||
|
||||
match self.navigate_from_route(
|
||||
&to,
|
||||
&NavigateOptions {
|
||||
resolve: false,
|
||||
replace: a.has_attribute("replace"),
|
||||
scroll: !a.has_attribute("noscroll"),
|
||||
state: State(None), // TODO state
|
||||
},
|
||||
) {
|
||||
Ok(_) => log::debug!("navigated"),
|
||||
Err(e) => log::error!("{e:#?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
123
leptos_router/src/components/routes.rs
Normal file
123
leptos_router/src/components/routes.rs
Normal file
|
@ -0,0 +1,123 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use leptos_dom::*;
|
||||
use leptos_dom::wasm_bindgen::JsCast;
|
||||
use leptos_macro::{view, Props};
|
||||
use leptos_reactive::{create_memo, Scope, ScopeDisposer, create_effect, provide_context};
|
||||
use leptos_core as leptos;
|
||||
|
||||
use crate::{
|
||||
create_branches, get_route_matches, join_paths, use_route, use_router, Outlet, RouteContext,
|
||||
RouteDefinition, RouteMatch, RouteContextInner, create_route_context,
|
||||
};
|
||||
|
||||
#[derive(Props)]
|
||||
pub struct RoutesProps {
|
||||
base: Option<String>,
|
||||
children: Vec<RouteDefinition>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Routes(cx: Scope, props: RoutesProps) -> Element {
|
||||
let router = use_router(cx);
|
||||
|
||||
let parent_route = use_route(cx);
|
||||
|
||||
let route_defs = props.children;
|
||||
|
||||
let branches = create_branches(
|
||||
&route_defs,
|
||||
&join_paths(&parent_route.inner.path, &props.base.unwrap_or_default()),
|
||||
Some((move || Outlet(cx)).into_child(cx)),
|
||||
);
|
||||
|
||||
let path_name = router.inner.location.path_name;
|
||||
let matches = create_memo(cx, move |_| {
|
||||
get_route_matches(branches.clone(), path_name.get())
|
||||
});
|
||||
|
||||
// TODO router.out (for SSR)
|
||||
|
||||
let mut disposers: Vec<ScopeDisposer> = Vec::new();
|
||||
|
||||
let route_states = create_memo(
|
||||
cx,
|
||||
move |prev: Option<&(Vec<RouteMatch>, Vec<RouteContext>, Option<RouteContext>)>| {
|
||||
|
||||
let (prev_matches, prev, root) = match prev {
|
||||
Some((a, b, c)) => (Some(a), Some(b), c),
|
||||
None => (None, None, &None),
|
||||
};
|
||||
let next_matches = matches.get();
|
||||
let mut equal = prev_matches
|
||||
.map(|prev_matches| next_matches.len() == prev_matches.len())
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut next: Vec<RouteContext> = Vec::new();
|
||||
|
||||
for i in 0..next_matches.len() {
|
||||
let prev_match = prev_matches.and_then(|p| p.get(i));
|
||||
let next_match = next_matches.get(i).unwrap();
|
||||
|
||||
if let Some(prev) = prev && let Some(prev_match) = prev_match && next_match.route.key == prev_match.route.key {
|
||||
next[i] = prev[i].clone();
|
||||
} else {
|
||||
equal = false;
|
||||
if let Some(disposer) = disposers.get(i) {
|
||||
// TODO
|
||||
//disposer.dispose();
|
||||
}
|
||||
|
||||
|
||||
let disposer = cx.child_scope(|cx| {
|
||||
let possible_parent = if i == 0 {
|
||||
None
|
||||
} else {
|
||||
next.get(i - 1)
|
||||
};
|
||||
|
||||
let next_ctx = create_route_context(&router, possible_parent.unwrap_or(&parent_route), { let c = next.get(i + 1).cloned(); move || c.clone()}, move || matches().get(i).cloned().unwrap());
|
||||
|
||||
if next.len() > i + 1 {
|
||||
next[i] = next_ctx;
|
||||
} else {
|
||||
next.push(next_ctx);
|
||||
}
|
||||
});
|
||||
|
||||
if disposers.len() > i + 1 {
|
||||
disposers[i] = disposer;
|
||||
} else {
|
||||
disposers.push(disposer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prev) = prev && equal {
|
||||
(next_matches, prev.to_vec(), root.clone())
|
||||
} else {
|
||||
let root = next.get(0).cloned();
|
||||
(next_matches, next, root)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
todo!()
|
||||
|
||||
/* let outlet = view! { <div></div> };
|
||||
leptos_dom::insert(
|
||||
cx,
|
||||
outlet.clone().unchecked_into(),
|
||||
(move || route_states.with(|(_, _, route)| {
|
||||
log::debug!("new route {route:#?}");
|
||||
if let Some(route) = route {
|
||||
provide_context(cx, route.clone());
|
||||
}
|
||||
route.as_ref().map(|route| route.outlet())
|
||||
})).into_child(cx),
|
||||
None,
|
||||
None
|
||||
);
|
||||
outlet */
|
||||
|
||||
}
|
|
@ -1,20 +1,21 @@
|
|||
use std::{any::Any, future::Future, pin::Pin};
|
||||
use std::{any::Any, future::Future, pin::Pin, rc::Rc};
|
||||
|
||||
use crate::{Location, Params};
|
||||
use crate::{Location, ParamsMap};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DataFunction {
|
||||
data: Box<dyn Fn(Params, Location) -> Pin<Box<dyn Future<Output = Box<dyn Any>>>>>,
|
||||
data: Rc<dyn Fn(ParamsMap, Location) -> Pin<Box<dyn Future<Output = Box<dyn Any>>>>>,
|
||||
}
|
||||
|
||||
impl<F, Fu, T> From<F> for DataFunction
|
||||
where
|
||||
F: Fn(Params, Location) -> Fu + Clone + 'static,
|
||||
F: Fn(ParamsMap, Location) -> Fu + Clone + 'static,
|
||||
Fu: Future<Output = T>,
|
||||
T: Any + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self {
|
||||
data: Box::new(move |params, location| {
|
||||
data: Rc::new(move |params, location| {
|
||||
Box::pin({
|
||||
let f = f.clone();
|
||||
async move {
|
||||
|
@ -26,3 +27,9 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DataFunction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("DataFunction").finish()
|
||||
}
|
||||
}
|
||||
|
|
13
leptos_router/src/error.rs
Normal file
13
leptos_router/src/error.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RouterError {
|
||||
#[error("loader found no data at this path")]
|
||||
NoMatch(String),
|
||||
#[error("route was matched, but loader returned None")]
|
||||
NotFound(String),
|
||||
#[error("could not find parameter {0}")]
|
||||
MissingParam(String),
|
||||
#[error("failed to deserialize parameters")]
|
||||
Params(Box<dyn std::error::Error + Send + Sync>),
|
||||
}
|
|
@ -1,21 +1,49 @@
|
|||
use leptos_reactive::{ReadSignal, Scope, WriteSignal};
|
||||
use leptos_dom::{document, window, window_event_listener};
|
||||
use leptos_reactive::{create_signal, ReadSignal, Scope, WriteSignal};
|
||||
|
||||
use crate::LocationChange;
|
||||
use crate::{LocationChange, State};
|
||||
|
||||
pub trait Integration {
|
||||
fn normalize(&self, cx: Scope) -> (ReadSignal<LocationChange>, WriteSignal<LocationChange>) {
|
||||
todo!()
|
||||
pub(crate) fn normalize(cx: Scope) -> (ReadSignal<LocationChange>, WriteSignal<LocationChange>) {
|
||||
let (loc, set_loc) = create_signal(cx, location());
|
||||
notify(Box::new(move || set_loc.update(|l| *l = location())));
|
||||
(loc, set_loc)
|
||||
}
|
||||
|
||||
fn location() -> LocationChange {
|
||||
let loc = leptos_dom::location();
|
||||
LocationChange {
|
||||
value: loc.pathname().unwrap_or_default()
|
||||
+ &loc.search().unwrap_or_default()
|
||||
+ &loc.hash().unwrap_or_default(),
|
||||
replace: true,
|
||||
scroll: true,
|
||||
state: State(None), // TODO
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerIntegration {}
|
||||
fn notify(f: impl Fn()) {
|
||||
window_event_listener("popstate", |_| f());
|
||||
}
|
||||
|
||||
impl Integration for ServerIntegration {}
|
||||
|
||||
pub struct HashIntegration {}
|
||||
|
||||
impl Integration for HashIntegration {}
|
||||
|
||||
pub struct HistoryIntegration {}
|
||||
|
||||
impl Integration for HistoryIntegration {}
|
||||
pub(crate) fn navigate(loc: &LocationChange) {
|
||||
let history = window().history().unwrap();
|
||||
if loc.replace {
|
||||
log::debug!("replacing state");
|
||||
history.replace_state_with_url(&loc.state.to_js_value(), "", Some(&loc.value));
|
||||
} else {
|
||||
log::debug!("pushing state");
|
||||
history.push_state_with_url(&loc.state.to_js_value(), "", Some(&loc.value));
|
||||
}
|
||||
// scroll to el
|
||||
if let Ok(hash) = leptos_dom::location().hash() {
|
||||
if !hash.is_empty() {
|
||||
let hash = &hash[1..];
|
||||
let el = document().query_selector(&format!("#{}", hash)).unwrap();
|
||||
if let Some(el) = el {
|
||||
el.scroll_into_view()
|
||||
} else if loc.scroll {
|
||||
window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
#![feature(auto_traits)]
|
||||
#![feature(let_chains)]
|
||||
#![feature(negative_impls)]
|
||||
#![feature(trait_alias)]
|
||||
|
||||
mod components;
|
||||
mod data;
|
||||
mod error;
|
||||
mod integrations;
|
||||
mod location;
|
||||
mod params;
|
||||
|
@ -10,11 +13,11 @@ mod routing;
|
|||
mod url;
|
||||
mod utils;
|
||||
|
||||
pub use crate::url::*;
|
||||
pub use components::*;
|
||||
pub use data::*;
|
||||
pub use integrations::*;
|
||||
pub use error::*;
|
||||
pub use location::*;
|
||||
pub use params::*;
|
||||
pub use routing::*;
|
||||
pub use url::*;
|
||||
pub use utils::*;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use std::{any::Any, rc::Rc};
|
||||
|
||||
use leptos_dom::wasm_bindgen::JsValue;
|
||||
use leptos_reactive::Memo;
|
||||
|
||||
use crate::Params;
|
||||
use crate::ParamsMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Location {
|
||||
pub query: Memo<Params>,
|
||||
pub query: Memo<ParamsMap>,
|
||||
pub path_name: Memo<String>,
|
||||
pub search: Memo<String>,
|
||||
pub hash: Memo<String>,
|
||||
|
@ -20,9 +21,16 @@ pub struct LocationChange {
|
|||
pub state: State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct State(pub Option<Rc<dyn Any>>);
|
||||
|
||||
impl State {
|
||||
pub fn to_js_value(&self) -> JsValue {
|
||||
// TODO
|
||||
JsValue::UNDEFINED
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for State {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
matches!((self.0.as_ref(), other.0.as_ref()), (None, None))
|
||||
|
@ -31,7 +39,13 @@ impl PartialEq for State {
|
|||
|
||||
impl Eq for State {}
|
||||
|
||||
/* pub trait State {}
|
||||
|
||||
impl<T> State for T where T: Any + std::fmt::Debug + PartialEq + Eq + Clone {}
|
||||
*/
|
||||
impl Default for LocationChange {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: Default::default(),
|
||||
replace: true,
|
||||
scroll: true,
|
||||
state: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use leptos_reactive::Scope;
|
||||
use linear_map::LinearMap;
|
||||
|
||||
use crate::{use_route, RouterError};
|
||||
|
||||
pub fn use_params<T: Params>(cx: Scope) -> Result<T, RouterError> {
|
||||
let route = use_route(cx);
|
||||
T::from_map(&route.inner.params)
|
||||
}
|
||||
|
||||
pub fn use_params_map(cx: Scope) -> ParamsMap {
|
||||
let route = use_route(cx);
|
||||
route.inner.params.clone()
|
||||
}
|
||||
|
||||
// For now, implemented with a `LinearMap`, as `n` is small enough
|
||||
// that O(n) iteration over a vectorized map is (*probably*) more space-
|
||||
// and time-efficient than hashing and using an actual `HashMap`
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Params(pub LinearMap<String, String>);
|
||||
pub struct ParamsMap(pub LinearMap<String, String>);
|
||||
|
||||
impl Params {
|
||||
impl ParamsMap {
|
||||
pub fn new() -> Self {
|
||||
Self(LinearMap::new())
|
||||
}
|
||||
|
@ -18,9 +33,13 @@ impl Params {
|
|||
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
|
||||
self.0.insert(key, value)
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<&String> {
|
||||
self.0.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Params {
|
||||
impl Default for ParamsMap {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
|
@ -30,15 +49,68 @@ impl Default for Params {
|
|||
// Copyright (c) 2019 Philipp Korber
|
||||
// https://github.com/rustonaut/common_macros/blob/master/src/lib.rs
|
||||
#[macro_export]
|
||||
macro_rules! params {
|
||||
macro_rules! params_map {
|
||||
($($key:expr => $val:expr),* ,) => (
|
||||
$crate::params!($($key => $val),*)
|
||||
$crate::ParamsMap!($($key => $val),*)
|
||||
);
|
||||
($($key:expr => $val:expr),*) => ({
|
||||
let start_capacity = common_macros::const_expr_count!($($key);*);
|
||||
#[allow(unused_mut)]
|
||||
let mut map = linear_map::LinearMap::with_capacity(start_capacity);
|
||||
$( map.insert($key, $val); )*
|
||||
$crate::Params(map)
|
||||
$crate::ParamsMap(map)
|
||||
});
|
||||
}
|
||||
|
||||
pub trait Params
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn from_map(map: &ParamsMap) -> Result<Self, RouterError>;
|
||||
}
|
||||
|
||||
impl Params for () {
|
||||
fn from_map(_map: &ParamsMap) -> Result<Self, RouterError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoParam
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, RouterError>;
|
||||
}
|
||||
|
||||
impl<T> IntoParam for Option<T>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, RouterError> {
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(value) => match T::from_str(value) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Err(RouterError::Params(Box::new(e)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto trait NotOption {}
|
||||
impl<T> !NotOption for Option<T> {}
|
||||
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr + NotOption,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, RouterError> {
|
||||
let value = value.ok_or_else(|| RouterError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| RouterError::Params(Box::new(e)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,39 @@
|
|||
use std::{any::Any, rc::Rc};
|
||||
use std::{any::Any, borrow::Cow, rc::Rc};
|
||||
|
||||
use leptos_reactive::{ReadSignal, Scope};
|
||||
use leptos_dom::{Child, IntoChild};
|
||||
use leptos_reactive::{create_memo, use_context, ReadSignal, Scope};
|
||||
|
||||
use crate::{Location, Url, State};
|
||||
use crate::{
|
||||
expand_optionals, join_paths, Location, Matcher, PathMatch, RouteContext, RouteDefinition,
|
||||
RouterContext, State, Url,
|
||||
};
|
||||
|
||||
pub fn use_router(cx: Scope) -> RouterContext {
|
||||
use_context(cx).expect("You must call use_router() within a <Router/> component")
|
||||
}
|
||||
|
||||
pub fn use_route(cx: Scope) -> RouteContext {
|
||||
use_context(cx).unwrap_or(use_router(cx).inner.base.clone())
|
||||
}
|
||||
|
||||
pub fn create_location(cx: Scope, path: ReadSignal<String>, state: ReadSignal<State>) -> Location {
|
||||
let url = cx.create_memo(move |prev: Option<&Url>| {
|
||||
path.with(|path| match Url::try_from(path.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
log::error!("[Leptos Router] Invalid path {path}\n\n{e:?}");
|
||||
prev.unwrap().clone()
|
||||
let url = create_memo(cx, move |prev: Option<&Url>| {
|
||||
path.with(|path| {
|
||||
log::debug!("create_location with path {path}");
|
||||
match Url::try_from(path.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
log::error!("[Leptos Router] Invalid path {path}\n\n{e:?}");
|
||||
prev.unwrap().clone()
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let path_name = cx.create_memo(move |_| url.with(|url| url.path_name.clone()));
|
||||
let search = cx.create_memo(move |_| url.with(|url| url.search.clone()));
|
||||
let hash = cx.create_memo(move |_| url.with(|url| url.hash.clone()));
|
||||
let query = cx.create_memo(move |_| url.with(|url| url.search_params()));
|
||||
let path_name = create_memo(cx, move |_| url.with(|url| url.path_name.clone()));
|
||||
let search = create_memo(cx, move |_| url.with(|url| url.search.clone()));
|
||||
let hash = create_memo(cx, move |_| url.with(|url| url.hash.clone()));
|
||||
let query = create_memo(cx, move |_| url.with(|url| url.search_params()));
|
||||
|
||||
Location {
|
||||
path_name,
|
||||
|
@ -27,3 +42,149 @@ pub fn create_location(cx: Scope, path: ReadSignal<String>, state: ReadSignal<St
|
|||
query,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Route {
|
||||
pub key: RouteDefinition,
|
||||
pub pattern: String,
|
||||
pub original_path: String,
|
||||
pub matcher: Matcher,
|
||||
}
|
||||
|
||||
pub(crate) fn create_branches(
|
||||
route_defs: &[RouteDefinition],
|
||||
base: &str,
|
||||
fallback: Option<Child>,
|
||||
) -> Vec<Branch> {
|
||||
let mut branches = Vec::new();
|
||||
create_branches_helper(route_defs, base, fallback, &mut Vec::new(), &mut branches);
|
||||
branches
|
||||
}
|
||||
|
||||
pub(crate) fn create_branches_helper(
|
||||
route_defs: &[RouteDefinition],
|
||||
base: &str,
|
||||
fallback: Option<Child>,
|
||||
stack: &mut Vec<Route>,
|
||||
branches: &mut Vec<Branch>,
|
||||
) {
|
||||
for def in route_defs {
|
||||
let routes = create_routes(def, base, fallback.clone());
|
||||
for route in routes {
|
||||
stack.push(route.clone());
|
||||
|
||||
if def.children.is_empty() {
|
||||
let branch = create_branch(&stack, branches.len());
|
||||
branches.push(branch);
|
||||
} else {
|
||||
create_branches_helper(
|
||||
&def.children,
|
||||
&route.pattern,
|
||||
fallback.clone(),
|
||||
stack,
|
||||
branches,
|
||||
);
|
||||
}
|
||||
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if stack.is_empty() {
|
||||
branches.sort_by_key(|branch| branch.score);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_routes(
|
||||
route_def: &RouteDefinition,
|
||||
base: &str,
|
||||
fallback: Option<impl IntoChild>,
|
||||
) -> Vec<Route> {
|
||||
let RouteDefinition {
|
||||
data,
|
||||
children,
|
||||
component,
|
||||
..
|
||||
} = route_def;
|
||||
let is_leaf = children.is_empty();
|
||||
route_def.path.iter().fold(Vec::new(), |mut acc, path| {
|
||||
for original_path in expand_optionals(&path) {
|
||||
let path = join_paths(base, &original_path);
|
||||
let pattern = if is_leaf {
|
||||
path
|
||||
} else {
|
||||
path.split("/*")
|
||||
.next()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or(path)
|
||||
};
|
||||
acc.push(Route {
|
||||
key: route_def.clone(),
|
||||
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
|
||||
pattern,
|
||||
original_path: original_path.to_string(),
|
||||
})
|
||||
}
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn create_branch(routes: &[Route], index: usize) -> Branch {
|
||||
Branch {
|
||||
routes: routes.to_vec(),
|
||||
score: score_route(routes.last().unwrap()) * 10000 - index,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Branch {
|
||||
routes: Vec<Route>,
|
||||
score: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct RouteMatch {
|
||||
pub path_match: PathMatch,
|
||||
pub route: Route,
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
fn matcher<'a>(&'a self, location: &'a str) -> Option<Vec<RouteMatch>> {
|
||||
let mut matches = Vec::new();
|
||||
for route in self.routes.iter().rev() {
|
||||
match route.matcher.test(location) {
|
||||
None => return None,
|
||||
Some(m) => matches.push(RouteMatch {
|
||||
path_match: m,
|
||||
route: route.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
matches.reverse();
|
||||
Some(matches)
|
||||
}
|
||||
}
|
||||
|
||||
fn score_route(route: &Route) -> usize {
|
||||
let (pattern, splat) = match route.pattern.split_once("/*") {
|
||||
Some((p, s)) => (p, Some(s)),
|
||||
None => (route.pattern.as_str(), None),
|
||||
};
|
||||
let segments = pattern
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
segments.iter().fold(
|
||||
segments.len() - if splat.is_none() { 0 } else { 1 },
|
||||
|score, segment| score + if segment.starts_with(':') { 2 } else { 3 },
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get_route_matches(branches: Vec<Branch>, location: String) -> Vec<RouteMatch> {
|
||||
for branch in branches {
|
||||
if let Some(matches) = branch.matcher(&location) {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
|
|
@ -1,22 +1,73 @@
|
|||
use crate::Params;
|
||||
use crate::ParamsMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Url {
|
||||
pub origin: String,
|
||||
pub path_name: String,
|
||||
pub search: String,
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
impl Url {
|
||||
pub fn search_params(&self) -> Params {
|
||||
todo!()
|
||||
pub fn search_params(&self) -> ParamsMap {
|
||||
let map = self
|
||||
.search
|
||||
.split('&')
|
||||
.filter_map(|piece| {
|
||||
let mut parts = piece.split('=');
|
||||
let (k, v) = (parts.next(), parts.next());
|
||||
match k {
|
||||
Some(k) if !k.is_empty() => {
|
||||
Some((unescape(k).to_string(), unescape(v.unwrap_or_default())))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<linear_map::LinearMap<String, String>>();
|
||||
ParamsMap(map)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "browser"))]
|
||||
pub(crate) fn unescape(s: &str) -> String {
|
||||
urlencoding::decode(s)
|
||||
.unwrap_or_else(|_| std::borrow::Cow::from(s))
|
||||
.replace('+', " ")
|
||||
}
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
pub(crate) fn unescape(s: &str) -> String {
|
||||
js_sys::decode_uri(s).unwrap().into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
impl TryFrom<&str> for Url {
|
||||
type Error = ();
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
todo!()
|
||||
fn try_from(url: &str) -> Result<Self, Self::Error> {
|
||||
let fake_host = String::from("http://leptos");
|
||||
let url = web_sys::Url::new_with_base(url, &fake_host)
|
||||
.map_err(|e| e.as_string().unwrap_or_default())?;
|
||||
Ok(Self {
|
||||
origin: url.origin(),
|
||||
path_name: url.pathname(),
|
||||
search: url.search(),
|
||||
hash: url.hash(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "browser"))]
|
||||
impl TryFrom<&str> for Url {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(url: &str) -> Result<Self, Self::Error> {
|
||||
let url = url::Url::parse(url).map_err(|e| e.to_string())?;
|
||||
Ok(Self {
|
||||
origin: url.origin().unicode_serialization(),
|
||||
path_name: url.path().to_string(),
|
||||
search: url.query().unwrap_or_default().to_string(),
|
||||
hash: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,51 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
#[doc(hidden)]
|
||||
/* pub fn expand_optionals(pattern: &str) -> impl Iterator<Item = Cow<str>> {
|
||||
todo!()
|
||||
} */
|
||||
#[cfg(feature = "browser")]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
// TODO real implementation for browser
|
||||
vec![pattern.into()]
|
||||
}
|
||||
|
||||
const CONTAINS_OPTIONAL: &str = r#"(/?\:[^\/]+)\?"#;
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(feature = "browser"))]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
use regex::Regex;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref OPTIONAL_RE: Regex = Regex::new(OPTIONAL).expect("could not compile OPTIONAL_RE");
|
||||
pub static ref OPTIONAL_RE_2: Regex = Regex::new(OPTIONAL_2).expect("could not compile OPTIONAL_RE_2");
|
||||
}
|
||||
|
||||
let captures = OPTIONAL_RE.find(pattern);
|
||||
match captures {
|
||||
None => vec![pattern.into()],
|
||||
Some(matched) => {
|
||||
let mut prefix = pattern[0..matched.start()].to_string();
|
||||
let captures = OPTIONAL_RE.captures(pattern).unwrap();
|
||||
let mut suffix = &pattern[matched.start() + captures[1].len()..];
|
||||
let mut prefixes = vec![prefix.clone()];
|
||||
|
||||
prefix += &captures[1];
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
while let Some(captures) = OPTIONAL_RE_2.captures(suffix.trim_start_matches('?')) {
|
||||
prefix += &captures[1];
|
||||
prefixes.push(prefix.clone());
|
||||
suffix = &suffix[captures[0].len()..];
|
||||
}
|
||||
|
||||
expand_optionals(suffix)
|
||||
.iter()
|
||||
.fold(Vec::new(), |mut results, expansion| {
|
||||
results.extend(prefixes.iter().map(|prefix| {
|
||||
Cow::Owned(prefix.clone() + expansion.trim_start_matches('?'))
|
||||
}));
|
||||
results
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const OPTIONAL: &str = r#"(/?:[^/]+)\?"#;
|
||||
const OPTIONAL_2: &str = r#"^(/:[^/]+)\?"#;
|
||||
|
|
|
@ -3,38 +3,40 @@
|
|||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::Params;
|
||||
use crate::ParamsMap;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[doc(hidden)]
|
||||
pub struct PathMatch<'a> {
|
||||
pub path: Cow<'a, str>,
|
||||
pub params: Params,
|
||||
pub struct PathMatch {
|
||||
pub path: String,
|
||||
pub params: ParamsMap,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct Matcher<'a> {
|
||||
splat: Option<&'a str>,
|
||||
segments: Vec<&'a str>,
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Matcher {
|
||||
splat: Option<String>,
|
||||
segments: Vec<String>,
|
||||
len: usize,
|
||||
partial: bool,
|
||||
}
|
||||
|
||||
impl<'a> Matcher<'a> {
|
||||
impl Matcher {
|
||||
#[doc(hidden)]
|
||||
pub fn new(path: &'a str) -> Self {
|
||||
pub fn new(path: &str) -> Self {
|
||||
Self::new_with_partial(path, false)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn new_with_partial(path: &'a str, partial: bool) -> Self {
|
||||
pub fn new_with_partial(path: &str, partial: bool) -> Self {
|
||||
let (pattern, splat) = match path.split_once("/*") {
|
||||
Some((p, s)) => (p, Some(s)),
|
||||
Some((p, s)) => (p, Some(s.to_string())),
|
||||
None => (path, None),
|
||||
};
|
||||
let segments = pattern
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let len = segments.len();
|
||||
|
@ -48,16 +50,14 @@ impl<'a> Matcher<'a> {
|
|||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn test<'b>(&self, location: &'b str) -> Option<PathMatch<'b>>
|
||||
where
|
||||
'a: 'b,
|
||||
{
|
||||
pub fn test(&self, location: &str) -> Option<PathMatch> {
|
||||
let loc_segments = location
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let loc_len = loc_segments.len();
|
||||
let len_diff = loc_len - self.len;
|
||||
let len_diff: i32 = loc_len as i32 - self.len as i32;
|
||||
|
||||
// quick path: not a match if
|
||||
// 1) matcher has add'l segments not found in location
|
||||
|
@ -67,17 +67,9 @@ impl<'a> Matcher<'a> {
|
|||
}
|
||||
// otherwise, start building a match
|
||||
else {
|
||||
/* let matched = PathMatch {
|
||||
path: if self.len > 0 {
|
||||
"".into()
|
||||
} else {
|
||||
"/".into()
|
||||
},
|
||||
params: Params::new()
|
||||
}; */
|
||||
|
||||
let mut path = String::new();
|
||||
let mut params = Params::new();
|
||||
let mut params = ParamsMap::new();
|
||||
|
||||
for (segment, loc_segment) in self.segments.iter().zip(loc_segments.iter()) {
|
||||
if let Some(param_name) = segment.strip_prefix(':') {
|
||||
params.insert(param_name.into(), (*loc_segment).into());
|
||||
|
@ -90,9 +82,9 @@ impl<'a> Matcher<'a> {
|
|||
path.push_str(loc_segment);
|
||||
}
|
||||
|
||||
if let Some(splat) = self.splat && !splat.is_empty() {
|
||||
if let Some(splat) = &self.splat && !splat.is_empty() {
|
||||
let value = if len_diff > 0 {
|
||||
loc_segments[self.len..].join("/").into()
|
||||
loc_segments[self.len..].join("/")
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ pub fn resolve_path<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "browser"))]
|
||||
fn has_scheme(path: &str) -> bool {
|
||||
use regex::Regex;
|
||||
lazy_static::lazy_static! {
|
||||
|
@ -43,6 +44,13 @@ fn has_scheme(path: &str) -> bool {
|
|||
HAS_SCHEME_RE.is_match(path)
|
||||
}
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
fn has_scheme(path: &str) -> bool {
|
||||
log::debug!("using browser re #2");
|
||||
let re = js_sys::RegExp::new(HAS_SCHEME, "");
|
||||
re.test(path)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn normalize(path: &str, omit_slash: bool) -> Cow<'_, str> {
|
||||
let s = replace_trim_path(path, "");
|
||||
|
@ -70,7 +78,9 @@ const QUERY: &str = r#"/*(\*.*)?$"#;
|
|||
|
||||
#[cfg(feature = "browser")]
|
||||
fn replace_trim_path<'a>(text: &'a str, replace: &str) -> Cow<'a, str> {
|
||||
let re = js_sys::Regexp::new(TRIM_PATH, "g");
|
||||
log::debug!("using browser re #1");
|
||||
|
||||
let re = js_sys::RegExp::new(TRIM_PATH, "g");
|
||||
js_sys::JsString::from(text)
|
||||
.replace_by_pattern(&re, "")
|
||||
.as_string()
|
||||
|
@ -80,13 +90,15 @@ fn replace_trim_path<'a>(text: &'a str, replace: &str) -> Cow<'a, str> {
|
|||
|
||||
#[cfg(feature = "browser")]
|
||||
fn begins_with_query_or_hash(text: &str) -> bool {
|
||||
let re = js_sys::Regexp::new(BEGINS_WITH_QUERY_OR_HASH, "");
|
||||
log::debug!("using browser re #2");
|
||||
let re = js_sys::RegExp::new(BEGINS_WITH_QUERY_OR_HASH, "");
|
||||
re.test(text)
|
||||
}
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
fn replace_query(text: &str) -> String {
|
||||
let re = js_sys::Regexp::new(QUERY, "g");
|
||||
log::debug!("using browser re #3");
|
||||
let re = js_sys::RegExp::new(QUERY, "g");
|
||||
js_sys::JsString::from(text)
|
||||
.replace_by_pattern(&re, "")
|
||||
.as_string()
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
use leptos_router::expand_optionals;
|
||||
|
||||
#[test]
|
||||
fn expand_optionals_should_expand() {
|
||||
assert_eq!(expand_optionals("/foo/:x"), vec!["/foo/:x"]);
|
||||
assert_eq!(expand_optionals("/foo/:x?"), vec!["/foo", "/foo/:x"]);
|
||||
assert_eq!(expand_optionals("/bar/:x?/"), vec!["/bar/", "/bar/:x/"]);
|
||||
assert_eq!(
|
||||
expand_optionals("/foo/:x?/:y?/:z"),
|
||||
vec!["/foo/:z", "/foo/:x/:z", "/foo/:x/:y/:z"]
|
||||
);
|
||||
assert_eq!(
|
||||
expand_optionals("/foo/:x?/:y/:z?"),
|
||||
vec!["/foo/:y", "/foo/:x/:y", "/foo/:y/:z", "/foo/:x/:y/:z"]
|
||||
);
|
||||
assert_eq!(
|
||||
expand_optionals("/foo/:x?/bar/:y?/baz/:z?"),
|
||||
vec![
|
||||
"/foo/bar/baz",
|
||||
"/foo/:x/bar/baz",
|
||||
"/foo/bar/:y/baz",
|
||||
"/foo/:x/bar/:y/baz",
|
||||
"/foo/bar/baz/:z",
|
||||
"/foo/:x/bar/baz/:z",
|
||||
"/foo/bar/:y/baz/:z",
|
||||
"/foo/:x/bar/:y/baz/:z"
|
||||
]
|
||||
)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// Test cases drawn from Solid Router
|
||||
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
|
||||
|
||||
use leptos_router::{params, Matcher, PathMatch};
|
||||
use leptos_router::{params_map, Matcher, PathMatch};
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_return_no_params_when_location_matches_exactly() {
|
||||
|
@ -11,7 +11,7 @@ fn create_matcher_should_return_no_params_when_location_matches_exactly() {
|
|||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params!()
|
||||
params: params_map!()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ fn create_matcher_should_build_params_collection() {
|
|||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/abc-123".into(),
|
||||
params: params!(
|
||||
params: params_map!(
|
||||
"id".into() => "abc-123".into()
|
||||
)
|
||||
})
|
||||
|
@ -46,7 +46,7 @@ fn create_matcher_should_match_past_end_when_ending_in_asterisk() {
|
|||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params!()
|
||||
params: params_map!()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ fn create_matcher_should_include_remaining_unmatched_location_as_param_when_endi
|
|||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params!(
|
||||
params: params_map!(
|
||||
"something".into() => "baz/qux".into()
|
||||
)
|
||||
})
|
||||
|
@ -82,7 +82,7 @@ fn create_matcher_should_include_empty_param_when_perfect_match_ends_in_asterisk
|
|||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params!(
|
||||
params: params_map!(
|
||||
"something".into() => "".into()
|
||||
)
|
||||
})
|
||||
|
|
38
router/Cargo.toml
Normal file
38
router/Cargo.toml
Normal file
|
@ -0,0 +1,38 @@
|
|||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
[dependencies]
|
||||
leptos_core = { path = "../leptos_core" }
|
||||
leptos_dom = { path = "../leptos_dom"}
|
||||
leptos_macro = { path = "../leptos_macro"}
|
||||
leptos_reactive = { path = "../leptos_reactive"}
|
||||
common_macros = "0.1"
|
||||
itertools = "0.10"
|
||||
lazy_static = "1"
|
||||
linear-map = "1"
|
||||
log = "0.4"
|
||||
regex = { version = "1", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
url = { version = "2", optional = true }
|
||||
urlencoding = "2"
|
||||
thiserror = "1"
|
||||
typed-builder = "0.10"
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"History",
|
||||
"HtmlAnchorElement",
|
||||
"MouseEvent",
|
||||
"Url"
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["dep:url", "dep:regex"]
|
||||
browser = ["dep:js-sys"]
|
9
router/src/components/mod.rs
Normal file
9
router/src/components/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
mod outlet;
|
||||
mod route;
|
||||
mod router;
|
||||
mod routes;
|
||||
|
||||
pub use outlet::*;
|
||||
pub use route::*;
|
||||
pub use router::*;
|
||||
pub use routes::*;
|
18
router/src/components/outlet.rs
Normal file
18
router/src/components/outlet.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use crate::use_route;
|
||||
use leptos_core as leptos;
|
||||
use leptos_dom::IntoChild;
|
||||
use leptos_macro::component;
|
||||
use leptos_macro::Props;
|
||||
use leptos_reactive::Scope;
|
||||
|
||||
#[component]
|
||||
pub fn Outlet(cx: Scope) -> impl IntoChild {
|
||||
let route = use_route(cx);
|
||||
log::debug!("trying to render Outlet: route.child = {:?}", route.child());
|
||||
move || {
|
||||
route.child().as_ref().map(|child| {
|
||||
log::debug!("rendering <Outlet/>");
|
||||
child.outlet()
|
||||
})
|
||||
}
|
||||
}
|
170
router/src/components/route.rs
Normal file
170
router/src/components/route.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
use std::{any::Any, borrow::Cow, rc::Rc};
|
||||
|
||||
use leptos_dom::{Child, Element, IntoChild};
|
||||
use leptos_reactive::Scope;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{
|
||||
matching::{PathMatch, RouteDefinition, RouteMatch},
|
||||
Action, Loader, ParamsMap, RouteData, RouterContext,
|
||||
};
|
||||
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct RouteProps<F, E>
|
||||
where
|
||||
F: Fn() -> E + 'static,
|
||||
E: IntoChild,
|
||||
{
|
||||
path: &'static str,
|
||||
element: F,
|
||||
#[builder(default, setter(strip_option))]
|
||||
loader: Option<Loader>,
|
||||
#[builder(default, setter(strip_option))]
|
||||
action: Option<Action>,
|
||||
#[builder(default)]
|
||||
children: Vec<RouteDefinition>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Route<F, E>(cx: Scope, props: RouteProps<F, E>) -> RouteDefinition
|
||||
where
|
||||
F: Fn() -> E + 'static,
|
||||
E: IntoChild,
|
||||
{
|
||||
RouteDefinition {
|
||||
path: props.path,
|
||||
loader: props.loader,
|
||||
action: props.action,
|
||||
children: props.children,
|
||||
element: Rc::new(move || (props.element)().into_child(cx)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouteContext {
|
||||
inner: Rc<RouteContextInner>,
|
||||
}
|
||||
|
||||
impl RouteContext {
|
||||
pub(crate) fn new(
|
||||
cx: Scope,
|
||||
router: &RouterContext,
|
||||
child: impl Fn() -> Option<RouteContext> + 'static,
|
||||
matcher: impl Fn() -> RouteMatch,
|
||||
) -> Self {
|
||||
let location = &router.inner.location;
|
||||
let RouteMatch { path_match, route } = matcher();
|
||||
let RouteDefinition {
|
||||
element,
|
||||
loader,
|
||||
action,
|
||||
..
|
||||
} = route.key;
|
||||
let PathMatch { path, params } = path_match;
|
||||
|
||||
let data = loader.map(|loader| {
|
||||
let data = (loader.data)(cx, params.clone(), location.clone());
|
||||
log::debug!(
|
||||
"RouteContext: set data to {:?}\n\ntype ID is {:?}",
|
||||
data,
|
||||
data.type_id()
|
||||
);
|
||||
data
|
||||
});
|
||||
|
||||
Self {
|
||||
inner: Rc::new(RouteContextInner {
|
||||
child: Box::new(child),
|
||||
data,
|
||||
action,
|
||||
path,
|
||||
params,
|
||||
outlet: Box::new(move || Some(element())),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn params(&self) -> &ParamsMap {
|
||||
&self.inner.params
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &Option<Box<dyn Any>> {
|
||||
&self.inner.data
|
||||
}
|
||||
|
||||
pub fn base(cx: Scope, path: &str, fallback: Option<fn() -> Element>) -> Self {
|
||||
Self {
|
||||
inner: Rc::new(RouteContextInner {
|
||||
child: Box::new(|| None),
|
||||
data: None,
|
||||
action: None,
|
||||
path: path.to_string(),
|
||||
params: ParamsMap::new(),
|
||||
outlet: Box::new(move || fallback.map(|f| f().into_child(cx))),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_path(&self, to: &str) -> Option<Cow<str>> {
|
||||
log::debug!("RouteContext::resolve_path");
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub(crate) fn child(&self) -> Option<RouteContext> {
|
||||
(self.inner.child)()
|
||||
}
|
||||
|
||||
pub fn outlet(&self) -> impl IntoChild {
|
||||
log::debug!("looking for outlet A");
|
||||
let o = (self.inner.outlet)();
|
||||
log::debug!("outlet = {o:#?}");
|
||||
o
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RouteContextInner {
|
||||
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
|
||||
pub(crate) data: Option<Box<dyn Any>>,
|
||||
pub(crate) action: Option<Action>,
|
||||
pub(crate) path: String,
|
||||
pub(crate) params: ParamsMap,
|
||||
pub(crate) outlet: Box<dyn Fn() -> Option<Child>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RouteContextInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RouteContextInner")
|
||||
.field("path", &self.path)
|
||||
.field("ParamsMap", &self.params)
|
||||
.field("child", &(self.child)())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoChildRoutes {
|
||||
fn into_child_routes(self) -> Vec<RouteDefinition>;
|
||||
}
|
||||
|
||||
impl IntoChildRoutes for () {
|
||||
fn into_child_routes(self) -> Vec<RouteDefinition> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoChildRoutes for RouteDefinition {
|
||||
fn into_child_routes(self) -> Vec<RouteDefinition> {
|
||||
vec![self]
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoChildRoutes for Option<RouteDefinition> {
|
||||
fn into_child_routes(self) -> Vec<RouteDefinition> {
|
||||
self.map(|c| vec![c]).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoChildRoutes for Vec<RouteDefinition> {
|
||||
fn into_child_routes(self) -> Vec<RouteDefinition> {
|
||||
self
|
||||
}
|
||||
}
|
469
router/src/components/router.rs
Normal file
469
router/src/components/router.rs
Normal file
|
@ -0,0 +1,469 @@
|
|||
use std::ops::IndexMut;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use leptos_dom as leptos;
|
||||
use leptos_dom::{Element, IntoChild, UnwrapThrowExt};
|
||||
use leptos_macro::view;
|
||||
use leptos_reactive::{
|
||||
create_memo, create_render_effect, create_signal, provide_context, use_transition, Memo,
|
||||
ReadSignal, Scope, ScopeDisposer, WriteSignal,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{
|
||||
create_location,
|
||||
matching::{get_route_matches, resolve_path, Branch, RouteMatch},
|
||||
unescape, History, Location, LocationChange, RouteContext, State, Url,
|
||||
};
|
||||
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct RouterProps<H: History + 'static> {
|
||||
mode: H,
|
||||
#[builder(default, setter(strip_option))]
|
||||
base: Option<&'static str>,
|
||||
#[builder(default, setter(strip_option))]
|
||||
fallback: Option<fn() -> Element>,
|
||||
#[builder(default)]
|
||||
children: Vec<Vec<Branch>>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Router<H>(cx: Scope, props: RouterProps<H>) -> impl IntoChild
|
||||
where
|
||||
H: History,
|
||||
{
|
||||
// create a new RouterContext and provide it to every component beneath the router
|
||||
let router = RouterContext::new(cx, props.mode, props.base, props.fallback);
|
||||
provide_context(cx, router.clone());
|
||||
|
||||
// whenever path changes, update matches
|
||||
let branches = props.children.into_iter().flatten().collect::<Vec<_>>();
|
||||
let matches = create_memo(cx, {
|
||||
let router = router.clone();
|
||||
move |_| {
|
||||
get_route_matches(branches.clone(), router.pathname().get())
|
||||
}
|
||||
});
|
||||
|
||||
// TODO router.out for SSR
|
||||
|
||||
// Rebuild the list of nested routes conservatively, and show the root route here
|
||||
let mut disposers = Vec::<ScopeDisposer>::new();
|
||||
|
||||
let route_states: Memo<RouterState> = create_memo(cx, move |prev: Option<&RouterState>| {
|
||||
let next_matches = matches();
|
||||
let prev_matches = prev.map(|p| &p.matches);
|
||||
let prev_routes = prev.map(|p| &p.routes);
|
||||
|
||||
// are the new route matches the same as the previous route matches so far?
|
||||
let mut equal = prev_matches
|
||||
.map(|prev_matches| next_matches.len() == prev_matches.len())
|
||||
.unwrap_or(false);
|
||||
|
||||
// iterate over the new matches, reusing old routes when they are the same
|
||||
// and replacing them with new routes when they differ
|
||||
let next: Rc<RefCell<Vec<RouteContext>>> = Default::default();
|
||||
|
||||
for i in 0..next_matches.len() {
|
||||
let next = next.clone();
|
||||
let prev_match = prev_matches.and_then(|p| p.get(i));
|
||||
let next_match = next_matches.get(i).unwrap();
|
||||
|
||||
if let Some(prev) = prev_routes && let Some(prev_match) = prev_match && next_match.route.key == prev_match.route.key {
|
||||
let prev_one = { prev.borrow()[i].clone() };
|
||||
if i >= next.borrow().len() {
|
||||
next.borrow_mut().push(prev_one);
|
||||
} else {
|
||||
*(next.borrow_mut().index_mut(i)) = prev_one;
|
||||
}
|
||||
} else {
|
||||
equal = false;
|
||||
|
||||
let disposer = cx.child_scope({let next = next.clone(); let router = Rc::clone(&router.inner); move |cx| {
|
||||
let next = next.clone();
|
||||
let next_ctx = RouteContext::new(
|
||||
cx,
|
||||
&RouterContext { inner: router },
|
||||
{let next = next.clone(); move || next.borrow().get(i + 1).cloned()},
|
||||
move || matches().get(i).cloned().unwrap()
|
||||
);
|
||||
|
||||
if next.borrow().len() > i + 1 {
|
||||
next.borrow_mut()[i] = next_ctx;
|
||||
} else {
|
||||
next.borrow_mut().push(next_ctx);
|
||||
}
|
||||
}});
|
||||
|
||||
if disposers.len() > i + 1 {
|
||||
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
|
||||
old_route_disposer.dispose();
|
||||
} else {
|
||||
disposers.push(disposer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO dispose of extra routes from previous matches if they're longer than new ones
|
||||
|
||||
if let Some(prev) = prev && equal {
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: prev_routes.cloned().unwrap_or_default(),
|
||||
root: prev.root.clone()
|
||||
}
|
||||
} else {
|
||||
let root = next.borrow().get(0).cloned();
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: next,
|
||||
root
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// show the root route
|
||||
let root_outlet = (move || {
|
||||
route_states.with(|state| {
|
||||
log::debug!("new routes {:#?}", state.routes);
|
||||
let root = state.routes.borrow();
|
||||
let root = root.get(0).clone();
|
||||
if let Some(route) = root {
|
||||
provide_context(cx, route.clone());
|
||||
}
|
||||
root.as_ref().map(|route| route.outlet())
|
||||
})
|
||||
})
|
||||
.into_child(cx);
|
||||
view! { <div>{root_outlet}</div> }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RouterState {
|
||||
matches: Vec<RouteMatch>,
|
||||
routes: Rc<RefCell<Vec<RouteContext>>>,
|
||||
root: Option<RouteContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouterContext {
|
||||
pub(crate) inner: Rc<RouterContextInner>,
|
||||
}
|
||||
pub(crate) struct RouterContextInner {
|
||||
pub location: Location,
|
||||
pub base: RouteContext,
|
||||
history: Box<dyn History>,
|
||||
cx: Scope,
|
||||
reference: ReadSignal<String>,
|
||||
set_reference: WriteSignal<String>,
|
||||
referrers: Rc<RefCell<Vec<LocationChange>>>,
|
||||
state: ReadSignal<State>,
|
||||
set_state: WriteSignal<State>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RouterContextInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RouterContextInner")
|
||||
.field("location", &self.location)
|
||||
.field("base", &self.base)
|
||||
.field("history", &std::any::type_name_of_val(&self.history))
|
||||
.field("cx", &self.cx)
|
||||
.field("reference", &self.reference)
|
||||
.field("set_reference", &self.set_reference)
|
||||
.field("referrers", &self.referrers)
|
||||
.field("state", &self.state)
|
||||
.field("set_state", &self.set_state)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RouterContext {
|
||||
pub fn new(
|
||||
cx: Scope,
|
||||
history: impl History + 'static,
|
||||
base: Option<&'static str>,
|
||||
fallback: Option<fn() -> Element>,
|
||||
) -> Self {
|
||||
// Any `History` type gives a way to get a reactive signal of the current location
|
||||
// in the browser context, this is drawn from the `popstate` event
|
||||
// different server adapters can provide different `History` implementations to allow server routing
|
||||
let source = history.location(cx);
|
||||
|
||||
// if initial route is empty, redirect to base path, if it exists
|
||||
let base = base.unwrap_or_default();
|
||||
let base_path = resolve_path("", &base, None);
|
||||
|
||||
if let Some(base_path) = &base_path && source.with(|s| s.value.is_empty()) {
|
||||
history.navigate(&LocationChange {
|
||||
value: base_path.to_string(),
|
||||
replace: true,
|
||||
scroll: false,
|
||||
state: State(None)
|
||||
});
|
||||
}
|
||||
|
||||
// the current URL
|
||||
let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone()));
|
||||
|
||||
// the current History.state
|
||||
let (state, set_state) = create_signal(cx, source.with(|s| s.state.clone()));
|
||||
|
||||
// we'll use this transition to wait for async resources to load when navigating to a new route
|
||||
let transition = use_transition(cx);
|
||||
|
||||
// Each field of `location` reactively represents a different part of the current location
|
||||
let location = create_location(cx, reference, state);
|
||||
let referrers: Rc<RefCell<Vec<LocationChange>>> = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
// Create base route with fallback element
|
||||
let base_path = base_path.unwrap_or_default();
|
||||
let base = RouteContext::base(cx, &base_path, fallback);
|
||||
|
||||
// Every time the History gives us a new location,
|
||||
// 1) start a transition
|
||||
// 2) update the reference (URL)
|
||||
// 3) update the state
|
||||
// this will trigger the new route match below
|
||||
create_render_effect(cx, move |_| {
|
||||
let LocationChange { value, state, .. } = source();
|
||||
cx.untrack(move || {
|
||||
if value != reference() {
|
||||
transition.start(move || {
|
||||
set_reference(|r| *r = value.clone());
|
||||
set_state(|s| *s = state.clone());
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let inner = Rc::new(RouterContextInner {
|
||||
location,
|
||||
base,
|
||||
history: Box::new(history),
|
||||
cx,
|
||||
reference,
|
||||
set_reference,
|
||||
referrers,
|
||||
state,
|
||||
set_state,
|
||||
});
|
||||
|
||||
// handle all click events on anchor tags
|
||||
if cfg!(feature = "browser") {
|
||||
leptos_dom::window_event_listener("click", {
|
||||
let inner = Rc::clone(&inner);
|
||||
move |ev| inner.clone().handle_anchor_click(ev)
|
||||
});
|
||||
// TODO on_cleanup remove event listener
|
||||
}
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn pathname(&self) -> Memo<String> {
|
||||
self.inner.location.pathname
|
||||
}
|
||||
|
||||
pub fn base(&self) -> RouteContext {
|
||||
self.inner.base.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl RouterContextInner {
|
||||
pub(crate) fn navigate_from_route(
|
||||
self: Rc<Self>,
|
||||
to: &str,
|
||||
options: &NavigateOptions,
|
||||
) -> Result<(), NavigationError> {
|
||||
let cx = self.cx;
|
||||
let this = Rc::clone(&self);
|
||||
|
||||
// TODO untrack causes an error here
|
||||
cx.untrack(move || {
|
||||
let resolved_to = if options.resolve {
|
||||
this.base.resolve_path(to)
|
||||
} else {
|
||||
resolve_path("", to, None)
|
||||
};
|
||||
|
||||
match resolved_to {
|
||||
None => Err(NavigationError::NotRoutable(to.to_string())),
|
||||
Some(resolved_to) => {
|
||||
if self.referrers.borrow().len() > 32 {
|
||||
return Err(NavigationError::MaxRedirects);
|
||||
}
|
||||
|
||||
if resolved_to != (this.reference)() || options.state != (this.state).get() {
|
||||
if cfg!(feature = "server") {
|
||||
// TODO server out
|
||||
self.history.navigate(&LocationChange {
|
||||
value: resolved_to.to_string(),
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: options.state.clone(),
|
||||
});
|
||||
} else {
|
||||
{
|
||||
self.referrers.borrow_mut().push(LocationChange {
|
||||
value: resolved_to.to_string(),
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: State(None), // TODO state state.get(),
|
||||
});
|
||||
}
|
||||
let len = self.referrers.borrow().len();
|
||||
|
||||
let transition = use_transition(self.cx);
|
||||
transition.start({
|
||||
let set_reference = self.set_reference;
|
||||
let set_state = self.set_state;
|
||||
let referrers = self.referrers.clone();
|
||||
let this = Rc::clone(&self);
|
||||
move || {
|
||||
set_reference.update({
|
||||
let resolved = resolved_to.to_string();
|
||||
move |r| *r = resolved
|
||||
});
|
||||
set_state.update({
|
||||
let next_state = options.state.clone();
|
||||
move |state| *state = next_state
|
||||
});
|
||||
if referrers.borrow().len() == len {
|
||||
this.navigate_end(LocationChange {
|
||||
value: resolved_to.to_string(),
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state: options.state.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn navigate_end(self: Rc<Self>, next: LocationChange) {
|
||||
let first = self.referrers.borrow().get(0).cloned();
|
||||
if let Some(first) = first {
|
||||
if next.value != first.value || next.state != first.state {
|
||||
let mut next = next.clone();
|
||||
next.replace = first.replace;
|
||||
next.scroll = first.scroll;
|
||||
self.history.navigate(&next);
|
||||
}
|
||||
self.referrers.borrow_mut().clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_anchor_click(self: Rc<Self>, ev: web_sys::Event) {
|
||||
use leptos_dom::wasm_bindgen::JsCast;
|
||||
let ev = ev.unchecked_into::<web_sys::MouseEvent>();
|
||||
/* if ev.default_prevented()
|
||||
|| ev.button() != 0
|
||||
|| ev.meta_key()
|
||||
|| ev.alt_key()
|
||||
|| ev.ctrl_key()
|
||||
|| ev.shift_key()
|
||||
{
|
||||
log::debug!("branch A prevent");
|
||||
return;
|
||||
} */
|
||||
|
||||
let composed_path = ev.composed_path();
|
||||
let mut a: Option<web_sys::HtmlAnchorElement> = None;
|
||||
for i in 0..composed_path.length() {
|
||||
if let Ok(el) = composed_path
|
||||
.get(i)
|
||||
.dyn_into::<web_sys::HtmlAnchorElement>()
|
||||
{
|
||||
a = Some(el);
|
||||
}
|
||||
}
|
||||
if let Some(a) = a {
|
||||
let href = a.href();
|
||||
let target = a.target();
|
||||
|
||||
// let browser handle this event if link has target,
|
||||
// or if it doesn't have href or state
|
||||
/* if !target.is_empty() || (href.is_empty() && !a.has_attribute("state")) {
|
||||
log::debug!("target or href empty");
|
||||
ev.prevent_default();
|
||||
return;
|
||||
} */
|
||||
|
||||
let rel = a.get_attribute("rel").unwrap_or_default();
|
||||
let mut rel = rel.split([' ', '\t']);
|
||||
|
||||
// let browser handle event if it has rel=external or download
|
||||
if a.has_attribute("download") || rel.any(|p| p == "external") {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = Url::try_from(href.as_str()).unwrap();
|
||||
let path_name = unescape(&url.pathname);
|
||||
|
||||
// let browser handle this event if it leaves our domain
|
||||
// or our base path
|
||||
/* if url.origin != leptos_dom::location().origin().unwrap_or_default()
|
||||
|| (!base_path.is_empty()
|
||||
&& !path_name.is_empty()
|
||||
&& !path_name
|
||||
.to_lowercase()
|
||||
.starts_with(&base_path.to_lowercase()))
|
||||
{
|
||||
return;
|
||||
} */
|
||||
|
||||
let to = path_name + &unescape(&url.search) + &unescape(&url.hash);
|
||||
let state = a.get_attribute("state"); // TODO state
|
||||
|
||||
ev.prevent_default();
|
||||
log::debug!("navigate to {to}");
|
||||
|
||||
match self.navigate_from_route(
|
||||
&to,
|
||||
&NavigateOptions {
|
||||
resolve: false,
|
||||
replace: a.has_attribute("replace"),
|
||||
scroll: !a.has_attribute("noscroll"),
|
||||
state: State(None), // TODO state
|
||||
},
|
||||
) {
|
||||
Ok(_) => log::debug!("navigated"),
|
||||
Err(e) => log::error!("{e:#?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NavigationError {
|
||||
#[error("Path {0:?} is not routable")]
|
||||
NotRoutable(String),
|
||||
#[error("Too many redirects")]
|
||||
MaxRedirects,
|
||||
}
|
||||
|
||||
pub struct NavigateOptions {
|
||||
pub resolve: bool,
|
||||
pub replace: bool,
|
||||
pub scroll: bool,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
impl Default for NavigateOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
resolve: true,
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state: State(None),
|
||||
}
|
||||
}
|
||||
}
|
112
router/src/components/routes.rs
Normal file
112
router/src/components/routes.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use std::{cmp::Reverse, rc::Rc};
|
||||
|
||||
use leptos_dom::{Child, IntoChild};
|
||||
use leptos_reactive::Scope;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::matching::{expand_optionals, join_paths, Branch, Matcher, RouteDefinition};
|
||||
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct RoutesProps {
|
||||
#[builder(default, setter(strip_option))]
|
||||
base: Option<String>,
|
||||
#[builder(default)]
|
||||
children: Vec<RouteDefinition>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Routes(_cx: Scope, props: RoutesProps) -> Vec<Branch> {
|
||||
let mut branches = Vec::new();
|
||||
|
||||
create_branches(
|
||||
&props.children,
|
||||
&props.base.unwrap_or_default(),
|
||||
&mut Vec::new(),
|
||||
&mut branches,
|
||||
);
|
||||
|
||||
branches
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RouteData {
|
||||
pub key: RouteDefinition,
|
||||
pub pattern: String,
|
||||
pub original_path: String,
|
||||
pub matcher: Matcher,
|
||||
}
|
||||
|
||||
impl RouteData {
|
||||
fn score(&self) -> usize {
|
||||
let (pattern, splat) = match self.pattern.split_once("/*") {
|
||||
Some((p, s)) => (p, Some(s)),
|
||||
None => (self.pattern.as_str(), None),
|
||||
};
|
||||
let segments = pattern
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
segments.iter().fold(
|
||||
segments.len() - if splat.is_none() { 0 } else { 1 },
|
||||
|score, segment| score + if segment.starts_with(':') { 2 } else { 3 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_branches(
|
||||
route_defs: &[RouteDefinition],
|
||||
base: &str,
|
||||
stack: &mut Vec<RouteData>,
|
||||
branches: &mut Vec<Branch>,
|
||||
) {
|
||||
for def in route_defs {
|
||||
let routes = create_routes(def, base);
|
||||
for route in routes {
|
||||
stack.push(route.clone());
|
||||
|
||||
if def.children.is_empty() {
|
||||
let branch = create_branch(stack, branches.len());
|
||||
branches.push(branch);
|
||||
} else {
|
||||
create_branches(&def.children, &route.pattern, stack, branches);
|
||||
}
|
||||
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if stack.is_empty() {
|
||||
branches.sort_by_key(|branch| Reverse(branch.score));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_branch(routes: &[RouteData], index: usize) -> Branch {
|
||||
Branch {
|
||||
routes: routes.to_vec(),
|
||||
score: routes.last().unwrap().score() * 10000 - index,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_routes(route_def: &RouteDefinition, base: &str) -> Vec<RouteData> {
|
||||
let RouteDefinition { children, .. } = route_def;
|
||||
let is_leaf = children.is_empty();
|
||||
let mut acc = Vec::new();
|
||||
for original_path in expand_optionals(&route_def.path) {
|
||||
let path = join_paths(base, &original_path);
|
||||
let pattern = if is_leaf {
|
||||
path
|
||||
} else {
|
||||
path.split("/*")
|
||||
.next()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or(path)
|
||||
};
|
||||
acc.push(RouteData {
|
||||
key: route_def.clone(),
|
||||
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
|
||||
pattern,
|
||||
original_path: original_path.to_string(),
|
||||
});
|
||||
}
|
||||
acc
|
||||
}
|
32
router/src/data/action.rs
Normal file
32
router/src/data/action.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use std::{future::Future, rc::Rc};
|
||||
|
||||
use crate::{PinnedFuture, Request, Response};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Action {
|
||||
f: Rc<dyn Fn(&Request) -> PinnedFuture<Response>>,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub async fn send(&self, req: &Request) -> Response {
|
||||
(self.f)(req).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, Fu> From<F> for Action
|
||||
where
|
||||
F: Fn(&Request) -> Fu + Clone + 'static,
|
||||
Fu: Future<Output = Response> + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self {
|
||||
f: Rc::new(move |req| Box::pin(f(req))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Action").finish()
|
||||
}
|
||||
}
|
39
router/src/data/loader.rs
Normal file
39
router/src/data/loader.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use std::{any::Any, fmt::Debug, rc::Rc};
|
||||
|
||||
use leptos_reactive::Scope;
|
||||
|
||||
use crate::{use_route, Location, ParamsMap};
|
||||
|
||||
pub fn use_loader<T>(cx: Scope) -> T
|
||||
where
|
||||
T: Clone + Debug + 'static,
|
||||
{
|
||||
let route = use_route(cx);
|
||||
let data = route.data().as_ref().unwrap();
|
||||
let data = data.downcast_ref::<T>().unwrap();
|
||||
|
||||
data.clone()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Loader {
|
||||
pub(crate) data: Rc<dyn Fn(Scope, ParamsMap, Location) -> Box<dyn Any>>,
|
||||
}
|
||||
|
||||
impl<F, T> From<F> for Loader
|
||||
where
|
||||
F: Fn(Scope, ParamsMap, Location) -> T + 'static,
|
||||
T: Any + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self {
|
||||
data: Rc::new(move |cx, params, location| Box::new(f(cx, params, location))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Loader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Loader").finish()
|
||||
}
|
||||
}
|
9
router/src/data/mod.rs
Normal file
9
router/src/data/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
mod action;
|
||||
mod loader;
|
||||
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
pub use action::*;
|
||||
pub use loader::*;
|
||||
|
||||
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
13
router/src/error.rs
Normal file
13
router/src/error.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RouterError {
|
||||
#[error("loader found no data at this path")]
|
||||
NoMatch(String),
|
||||
#[error("route was matched, but loader returned None")]
|
||||
NotFound(String),
|
||||
#[error("could not find parameter {0}")]
|
||||
MissingParam(String),
|
||||
#[error("failed to deserialize parameters")]
|
||||
Params(Box<dyn std::error::Error + Send + Sync>),
|
||||
}
|
10
router/src/fetch.rs
Normal file
10
router/src/fetch.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
/// Types that wrap the [Web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
|
||||
/// These can be used to implement isomorphic server actions.
|
||||
|
||||
/// A Rust equivalent to the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
|
||||
/// in the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
|
||||
pub struct Request {}
|
||||
|
||||
/// A Rust equivalent to the [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object
|
||||
/// in the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
|
||||
pub struct Response {}
|
61
router/src/history/location.rs
Normal file
61
router/src/history/location.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use leptos_reactive::{create_memo, Memo, ReadSignal, Scope};
|
||||
|
||||
use crate::{State, Url};
|
||||
|
||||
use super::params::ParamsMap;
|
||||
|
||||
pub fn create_location(cx: Scope, path: ReadSignal<String>, state: ReadSignal<State>) -> Location {
|
||||
let url = create_memo(cx, move |prev: Option<&Url>| {
|
||||
path.with(|path| {
|
||||
log::debug!("create_location with path {path}");
|
||||
match Url::try_from(path.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
log::error!("[Leptos Router] Invalid path {path}\n\n{e:?}");
|
||||
prev.unwrap().clone()
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let pathname = create_memo(cx, move |_| url.with(|url| url.pathname.clone()));
|
||||
let search = create_memo(cx, move |_| url.with(|url| url.search.clone()));
|
||||
let hash = create_memo(cx, move |_| url.with(|url| url.hash.clone()));
|
||||
let query = create_memo(cx, move |_| url.with(|url| url.search_params()));
|
||||
|
||||
Location {
|
||||
pathname,
|
||||
search,
|
||||
hash,
|
||||
query,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Location {
|
||||
pub query: Memo<ParamsMap>,
|
||||
pub pathname: Memo<String>,
|
||||
pub search: Memo<String>,
|
||||
pub hash: Memo<String>,
|
||||
pub state: ReadSignal<State>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocationChange {
|
||||
pub value: String,
|
||||
pub replace: bool,
|
||||
pub scroll: bool,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
impl Default for LocationChange {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: Default::default(),
|
||||
replace: true,
|
||||
scroll: true,
|
||||
state: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
75
router/src/history/mod.rs
Normal file
75
router/src/history/mod.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use leptos_reactive::{create_signal, ReadSignal, Scope};
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
mod location;
|
||||
mod params;
|
||||
mod state;
|
||||
mod url;
|
||||
|
||||
pub use self::url::*;
|
||||
pub use location::*;
|
||||
pub use params::*;
|
||||
pub use state::*;
|
||||
|
||||
pub trait History {
|
||||
fn location(&self, cx: Scope) -> ReadSignal<LocationChange>;
|
||||
|
||||
fn navigate(&self, loc: &LocationChange);
|
||||
}
|
||||
|
||||
pub struct BrowserIntegration {}
|
||||
|
||||
impl BrowserIntegration {
|
||||
fn current() -> LocationChange {
|
||||
let loc = leptos_dom::location();
|
||||
LocationChange {
|
||||
value: loc.pathname().unwrap_or_default()
|
||||
+ &loc.search().unwrap_or_default()
|
||||
+ &loc.hash().unwrap_or_default(),
|
||||
replace: true,
|
||||
scroll: true,
|
||||
state: State(None), // TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl History for BrowserIntegration {
|
||||
fn location(&self, cx: Scope) -> ReadSignal<LocationChange> {
|
||||
let (location, set_location) = create_signal(cx, Self::current());
|
||||
|
||||
leptos_dom::window_event_listener("popstate", move |_| {
|
||||
set_location(|change| *change = Self::current());
|
||||
});
|
||||
|
||||
location
|
||||
}
|
||||
|
||||
fn navigate(&self, loc: &LocationChange) {
|
||||
let history = leptos_dom::window().history().unwrap();
|
||||
if loc.replace {
|
||||
log::debug!("replacing state");
|
||||
history
|
||||
.replace_state_with_url(&loc.state.to_js_value(), "", Some(&loc.value))
|
||||
.unwrap_throw();
|
||||
} else {
|
||||
log::debug!("pushing state");
|
||||
history
|
||||
.push_state_with_url(&loc.state.to_js_value(), "", Some(&loc.value))
|
||||
.unwrap_throw();
|
||||
}
|
||||
// scroll to el
|
||||
if let Ok(hash) = leptos_dom::location().hash() {
|
||||
if !hash.is_empty() {
|
||||
let hash = &hash[1..];
|
||||
let el = leptos_dom::document()
|
||||
.query_selector(&format!("#{}", hash))
|
||||
.unwrap();
|
||||
if let Some(el) = el {
|
||||
el.scroll_into_view()
|
||||
} else if loc.scroll {
|
||||
leptos_dom::window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
105
router/src/history/params.rs
Normal file
105
router/src/history/params.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use linear_map::LinearMap;
|
||||
|
||||
use crate::RouterError;
|
||||
|
||||
// For now, implemented with a `LinearMap`, as `n` is small enough
|
||||
// that O(n) iteration over a vectorized map is (*probably*) more space-
|
||||
// and time-efficient than hashing and using an actual `HashMap`
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ParamsMap(pub LinearMap<String, String>);
|
||||
|
||||
impl ParamsMap {
|
||||
pub fn new() -> Self {
|
||||
Self(LinearMap::new())
|
||||
}
|
||||
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self(LinearMap::with_capacity(capacity))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
|
||||
self.0.insert(key, value)
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<&String> {
|
||||
self.0.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ParamsMap {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from hash_map! in common_macros crate
|
||||
// Copyright (c) 2019 Philipp Korber
|
||||
// https://github.com/rustonaut/common_macros/blob/master/src/lib.rs
|
||||
#[macro_export]
|
||||
macro_rules! params_map {
|
||||
($($key:expr => $val:expr),* ,) => (
|
||||
$crate::ParamsMap!($($key => $val),*)
|
||||
);
|
||||
($($key:expr => $val:expr),*) => ({
|
||||
let start_capacity = common_macros::const_expr_count!($($key);*);
|
||||
#[allow(unused_mut)]
|
||||
let mut map = linear_map::LinearMap::with_capacity(start_capacity);
|
||||
$( map.insert($key, $val); )*
|
||||
$crate::ParamsMap(map)
|
||||
});
|
||||
}
|
||||
|
||||
pub trait Params
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn from_map(map: &ParamsMap) -> Result<Self, RouterError>;
|
||||
}
|
||||
|
||||
impl Params for () {
|
||||
fn from_map(_map: &ParamsMap) -> Result<Self, RouterError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoParam
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, RouterError>;
|
||||
}
|
||||
|
||||
impl<T> IntoParam for Option<T>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, RouterError> {
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(value) => match T::from_str(value) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Err(RouterError::Params(Box::new(e)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto trait NotOption {}
|
||||
impl<T> !NotOption for Option<T> {}
|
||||
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr + NotOption,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, RouterError> {
|
||||
let value = value.ok_or_else(|| RouterError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| RouterError::Params(Box::new(e)))
|
||||
}
|
||||
}
|
22
router/src/history/state.rs
Normal file
22
router/src/history/state.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use wasm_bindgen::JsValue;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct State(pub Option<JsValue>);
|
||||
|
||||
impl State {
|
||||
pub fn to_js_value(&self) -> JsValue {
|
||||
match &self.0 {
|
||||
Some(v) => v.clone(),
|
||||
None => JsValue::UNDEFINED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for State
|
||||
where
|
||||
T: Into<JsValue>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
State(Some(value.into()))
|
||||
}
|
||||
}
|
73
router/src/history/url.rs
Normal file
73
router/src/history/url.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use crate::ParamsMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Url {
|
||||
pub origin: String,
|
||||
pub pathname: String,
|
||||
pub search: String,
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
impl Url {
|
||||
pub fn search_params(&self) -> ParamsMap {
|
||||
let map = self
|
||||
.search
|
||||
.split('&')
|
||||
.filter_map(|piece| {
|
||||
let mut parts = piece.split('=');
|
||||
let (k, v) = (parts.next(), parts.next());
|
||||
match k {
|
||||
Some(k) if !k.is_empty() => {
|
||||
Some((unescape(k), unescape(v.unwrap_or_default())))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<linear_map::LinearMap<String, String>>();
|
||||
ParamsMap(map)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "browser"))]
|
||||
pub(crate) fn unescape(s: &str) -> String {
|
||||
urlencoding::decode(s)
|
||||
.unwrap_or_else(|_| std::borrow::Cow::from(s))
|
||||
.replace('+', " ")
|
||||
}
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
pub(crate) fn unescape(s: &str) -> String {
|
||||
js_sys::decode_uri(s).unwrap().into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
impl TryFrom<&str> for Url {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(url: &str) -> Result<Self, Self::Error> {
|
||||
let fake_host = String::from("http://leptos");
|
||||
let url = web_sys::Url::new_with_base(url, &fake_host)
|
||||
.map_err(|e| e.as_string().unwrap_or_default())?;
|
||||
Ok(Self {
|
||||
origin: url.origin(),
|
||||
pathname: url.pathname(),
|
||||
search: url.search(),
|
||||
hash: url.hash(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "browser"))]
|
||||
impl TryFrom<&str> for Url {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(url: &str) -> Result<Self, Self::Error> {
|
||||
let url = url::Url::parse(url).map_err(|e| e.to_string())?;
|
||||
Ok(Self {
|
||||
origin: url.origin().unicode_serialization(),
|
||||
pathname: url.path().to_string(),
|
||||
search: url.query().unwrap_or_default().to_string(),
|
||||
hash: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
21
router/src/hooks.rs
Normal file
21
router/src/hooks.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use leptos_reactive::{use_context, Scope};
|
||||
|
||||
use crate::{Params, ParamsMap, RouteContext, RouterContext, RouterError};
|
||||
|
||||
pub fn use_router(cx: Scope) -> RouterContext {
|
||||
use_context(cx).expect("You must call use_router() within a <Router/> component")
|
||||
}
|
||||
|
||||
pub fn use_route(cx: Scope) -> RouteContext {
|
||||
use_context(cx).unwrap_or_else(|| use_router(cx).base())
|
||||
}
|
||||
|
||||
pub fn use_params<T: Params>(cx: Scope) -> Result<T, RouterError> {
|
||||
let route = use_route(cx);
|
||||
T::from_map(route.params())
|
||||
}
|
||||
|
||||
pub fn use_params_map(cx: Scope) -> ParamsMap {
|
||||
let route = use_route(cx);
|
||||
route.params().clone()
|
||||
}
|
19
router/src/lib.rs
Normal file
19
router/src/lib.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
#![feature(auto_traits)]
|
||||
#![feature(let_chains)]
|
||||
#![feature(negative_impls)]
|
||||
#![feature(type_name_of_val)]
|
||||
|
||||
mod components;
|
||||
mod data;
|
||||
mod error;
|
||||
mod fetch;
|
||||
mod history;
|
||||
mod hooks;
|
||||
mod matching;
|
||||
|
||||
pub use components::*;
|
||||
pub use data::*;
|
||||
pub use error::*;
|
||||
pub use fetch::*;
|
||||
pub use history::*;
|
||||
pub use hooks::*;
|
51
router/src/matching/expand_optionals.rs
Normal file
51
router/src/matching/expand_optionals.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "browser")]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
// TODO real implementation for browser
|
||||
vec![pattern.into()]
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(feature = "browser"))]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
use regex::Regex;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref OPTIONAL_RE: Regex = Regex::new(OPTIONAL).expect("could not compile OPTIONAL_RE");
|
||||
pub static ref OPTIONAL_RE_2: Regex = Regex::new(OPTIONAL_2).expect("could not compile OPTIONAL_RE_2");
|
||||
}
|
||||
|
||||
let captures = OPTIONAL_RE.find(pattern);
|
||||
match captures {
|
||||
None => vec![pattern.into()],
|
||||
Some(matched) => {
|
||||
let mut prefix = pattern[0..matched.start()].to_string();
|
||||
let captures = OPTIONAL_RE.captures(pattern).unwrap();
|
||||
let mut suffix = &pattern[matched.start() + captures[1].len()..];
|
||||
let mut prefixes = vec![prefix.clone()];
|
||||
|
||||
prefix += &captures[1];
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
while let Some(captures) = OPTIONAL_RE_2.captures(suffix.trim_start_matches('?')) {
|
||||
prefix += &captures[1];
|
||||
prefixes.push(prefix.clone());
|
||||
suffix = &suffix[captures[0].len()..];
|
||||
}
|
||||
|
||||
expand_optionals(suffix)
|
||||
.iter()
|
||||
.fold(Vec::new(), |mut results, expansion| {
|
||||
results.extend(prefixes.iter().map(|prefix| {
|
||||
Cow::Owned(prefix.clone() + expansion.trim_start_matches('?'))
|
||||
}));
|
||||
results
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const OPTIONAL: &str = r#"(/?:[^/]+)\?"#;
|
||||
const OPTIONAL_2: &str = r#"^(/:[^/]+)\?"#;
|
95
router/src/matching/matcher.rs
Normal file
95
router/src/matching/matcher.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
// Implementation based on Solid Router
|
||||
// see https://github.com/solidjs/solid-router/blob/main/src/utils.ts
|
||||
|
||||
use crate::ParamsMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[doc(hidden)]
|
||||
pub struct PathMatch {
|
||||
pub path: String,
|
||||
pub params: ParamsMap,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Matcher {
|
||||
splat: Option<String>,
|
||||
segments: Vec<String>,
|
||||
len: usize,
|
||||
partial: bool,
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
#[doc(hidden)]
|
||||
pub fn new(path: &str) -> Self {
|
||||
Self::new_with_partial(path, false)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn new_with_partial(path: &str, partial: bool) -> Self {
|
||||
let (pattern, splat) = match path.split_once("/*") {
|
||||
Some((p, s)) => (p, Some(s.to_string())),
|
||||
None => (path, None),
|
||||
};
|
||||
let segments = pattern
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let len = segments.len();
|
||||
|
||||
Self {
|
||||
splat,
|
||||
segments,
|
||||
len,
|
||||
partial,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn test(&self, location: &str) -> Option<PathMatch> {
|
||||
let loc_segments = location
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let loc_len = loc_segments.len();
|
||||
let len_diff: i32 = loc_len as i32 - self.len as i32;
|
||||
|
||||
// quick path: not a match if
|
||||
// 1) matcher has add'l segments not found in location
|
||||
// 2) location has add'l segments, there's no splat, and partial matches not allowed
|
||||
if loc_len < self.len || (len_diff > 0 && self.splat.is_none() && !self.partial) {
|
||||
None
|
||||
}
|
||||
// otherwise, start building a match
|
||||
else {
|
||||
let mut path = String::new();
|
||||
let mut params = ParamsMap::new();
|
||||
|
||||
for (segment, loc_segment) in self.segments.iter().zip(loc_segments.iter()) {
|
||||
if let Some(param_name) = segment.strip_prefix(':') {
|
||||
params.insert(param_name.into(), (*loc_segment).into());
|
||||
} else if segment != loc_segment {
|
||||
// if any segment doesn't match and isn't a param, there's no path match
|
||||
return None;
|
||||
}
|
||||
|
||||
path.push('/');
|
||||
path.push_str(loc_segment);
|
||||
}
|
||||
|
||||
if let Some(splat) = &self.splat && !splat.is_empty() {
|
||||
let value = if len_diff > 0 {
|
||||
loc_segments[self.len..].join("/")
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
params.insert(splat.into(), value);
|
||||
}
|
||||
|
||||
Some(PathMatch { path, params })
|
||||
}
|
||||
}
|
||||
}
|
49
router/src/matching/mod.rs
Normal file
49
router/src/matching/mod.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
mod expand_optionals;
|
||||
mod matcher;
|
||||
mod resolve_path;
|
||||
mod route;
|
||||
|
||||
pub(crate) use expand_optionals::*;
|
||||
pub(crate) use matcher::*;
|
||||
pub(crate) use resolve_path::*;
|
||||
pub use route::*;
|
||||
|
||||
use crate::RouteData;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct RouteMatch {
|
||||
pub path_match: PathMatch,
|
||||
pub route: RouteData,
|
||||
}
|
||||
|
||||
pub(crate) fn get_route_matches(branches: Vec<Branch>, location: String) -> Vec<RouteMatch> {
|
||||
for branch in branches {
|
||||
if let Some(matches) = branch.matcher(&location) {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Branch {
|
||||
pub routes: Vec<RouteData>,
|
||||
pub score: usize,
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
fn matcher<'a>(&'a self, location: &'a str) -> Option<Vec<RouteMatch>> {
|
||||
let mut matches = Vec::new();
|
||||
for route in self.routes.iter().rev() {
|
||||
match route.matcher.test(location) {
|
||||
None => return None,
|
||||
Some(m) => matches.push(RouteMatch {
|
||||
path_match: m,
|
||||
route: route.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
matches.reverse();
|
||||
Some(matches)
|
||||
}
|
||||
}
|
133
router/src/matching/resolve_path.rs
Normal file
133
router/src/matching/resolve_path.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
// Implementation based on Solid Router
|
||||
// see https://github.com/solidjs/solid-router/blob/main/src/utils.ts
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn resolve_path<'a>(
|
||||
base: &'a str,
|
||||
path: &'a str,
|
||||
from: Option<&'a str>,
|
||||
) -> Option<Cow<'a, str>> {
|
||||
if has_scheme(path) {
|
||||
None
|
||||
} else {
|
||||
let base_path = normalize(base, false);
|
||||
let from_path = from.map(|from| normalize(from, false));
|
||||
let result = if let Some(from_path) = from_path {
|
||||
if path.starts_with('/') {
|
||||
base_path
|
||||
} else if from_path.to_lowercase().find(&base_path.to_lowercase()) != Some(0) {
|
||||
base_path + from_path
|
||||
} else {
|
||||
from_path
|
||||
}
|
||||
} else {
|
||||
base_path
|
||||
};
|
||||
|
||||
let result_empty = result.is_empty();
|
||||
let prefix = if result_empty { "/".into() } else { result };
|
||||
|
||||
Some(prefix + normalize(path, result_empty))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "browser"))]
|
||||
fn has_scheme(path: &str) -> bool {
|
||||
use regex::Regex;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref HAS_SCHEME_RE: Regex =
|
||||
Regex::new(HAS_SCHEME).expect("couldn't compile HAS_SCHEME_RE");
|
||||
}
|
||||
|
||||
HAS_SCHEME_RE.is_match(path)
|
||||
}
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
fn has_scheme(path: &str) -> bool {
|
||||
let re = js_sys::RegExp::new(HAS_SCHEME, "");
|
||||
re.test(path)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn normalize(path: &str, omit_slash: bool) -> Cow<'_, str> {
|
||||
let s = replace_trim_path(path, "");
|
||||
if !s.is_empty() {
|
||||
if omit_slash || begins_with_query_or_hash(&s) {
|
||||
s
|
||||
} else {
|
||||
format!("/{s}").into()
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn join_paths<'a>(from: &'a str, to: &'a str) -> String {
|
||||
let from = replace_query(&normalize(from, false)).to_string();
|
||||
from + &normalize(to, false)
|
||||
}
|
||||
|
||||
const TRIM_PATH: &str = r#"^/+|/+$"#;
|
||||
const BEGINS_WITH_QUERY_OR_HASH: &str = r#"^[?#]"#;
|
||||
const HAS_SCHEME: &str = r#"^(?:[a-z0-9]+:)?//"#;
|
||||
const QUERY: &str = r#"/*(\*.*)?$"#;
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
fn replace_trim_path<'a>(text: &'a str, replace: &str) -> Cow<'a, str> {
|
||||
let re = js_sys::RegExp::new(TRIM_PATH, "g");
|
||||
js_sys::JsString::from(text)
|
||||
.replace_by_pattern(&re, "")
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
fn begins_with_query_or_hash(text: &str) -> bool {
|
||||
let re = js_sys::RegExp::new(BEGINS_WITH_QUERY_OR_HASH, "");
|
||||
re.test(text)
|
||||
}
|
||||
|
||||
#[cfg(feature = "browser")]
|
||||
fn replace_query(text: &str) -> String {
|
||||
let re = js_sys::RegExp::new(QUERY, "g");
|
||||
js_sys::JsString::from(text)
|
||||
.replace_by_pattern(&re, "")
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "browser"))]
|
||||
fn replace_trim_path<'a>(text: &'a str, replace: &str) -> Cow<'a, str> {
|
||||
use regex::Regex;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TRIM_PATH_RE: Regex =
|
||||
Regex::new(TRIM_PATH).expect("couldn't compile TRIM_PATH_RE");
|
||||
}
|
||||
|
||||
TRIM_PATH_RE.replace(text, replace)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "browser"))]
|
||||
fn begins_with_query_or_hash(text: &str) -> bool {
|
||||
use regex::Regex;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref BEGINS_WITH_QUERY_OR_HASH_RE: Regex =
|
||||
Regex::new(BEGINS_WITH_QUERY_OR_HASH).expect("couldn't compile BEGINS_WITH_HASH_RE");
|
||||
}
|
||||
BEGINS_WITH_QUERY_OR_HASH_RE.is_match(text)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "browser"))]
|
||||
fn replace_query(text: &str) -> Cow<str> {
|
||||
use regex::Regex;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref QUERY_RE: Regex =
|
||||
Regex::new(QUERY).expect("couldn't compile QUERY_RE");
|
||||
}
|
||||
QUERY_RE.replace(text, "")
|
||||
}
|
43
router/src/matching/route.rs
Normal file
43
router/src/matching/route.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use leptos_dom::Child;
|
||||
|
||||
use crate::{Action, Loader};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RouteDefinition {
|
||||
pub path: &'static str,
|
||||
pub loader: Option<Loader>,
|
||||
pub action: Option<Action>,
|
||||
pub children: Vec<RouteDefinition>,
|
||||
pub element: Rc<dyn Fn() -> Child>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RouteDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RouteDefinition")
|
||||
.field("path", &self.path)
|
||||
.field("loader", &self.loader)
|
||||
.field("action", &self.action)
|
||||
.field("children", &self.children)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RouteDefinition {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path && self.children == other.children
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RouteDefinition {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path: Default::default(),
|
||||
loader: Default::default(),
|
||||
action: Default::default(),
|
||||
children: Default::default(),
|
||||
element: Rc::new(|| Child::Null),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue