mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
stash
This commit is contained in:
parent
c3b9932172
commit
9f02cc8cc1
26 changed files with 630 additions and 469 deletions
|
@ -33,6 +33,8 @@ members = [
|
|||
# libraries
|
||||
"meta",
|
||||
"router",
|
||||
"routing",
|
||||
"is_server",
|
||||
]
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
|
|
|
@ -10,12 +10,16 @@ lto = true
|
|||
[dependencies]
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
leptos = { path = "../../leptos", features = ["csr", "tracing"] }
|
||||
routing = { path = "../../routing", features = ["tracing"] }
|
||||
#leptos_router = { path = "../../router", features = ["csr"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing-subscriber-wasm = "0.1.0"
|
||||
tracing = "0.1.40"
|
||||
#leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
|
|
@ -2,7 +2,7 @@ use futures::{
|
|||
channel::oneshot::{self, Canceled},
|
||||
Future,
|
||||
};
|
||||
use leptos::set_timeout;
|
||||
use leptos::leptos_dom::helpers::set_timeout;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
|
@ -1,20 +1,69 @@
|
|||
mod api;
|
||||
use crate::api::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_router::*;
|
||||
use leptos::{
|
||||
component,
|
||||
prelude::*,
|
||||
reactive_graph::{
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
},
|
||||
view, IntoView,
|
||||
};
|
||||
use log::{debug, info};
|
||||
use routing::{
|
||||
location::{BrowserUrl, Location},
|
||||
NestedRoute, Router, Routes, StaticSegment,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
struct ExampleContext(i32);
|
||||
|
||||
#[component]
|
||||
pub fn RouterExample() -> impl IntoView {
|
||||
log::debug!("rendering <RouterExample/>");
|
||||
info!("rendering <RouterExample/>");
|
||||
|
||||
// contexts are passed down through the route tree
|
||||
provide_context(ExampleContext(0));
|
||||
|
||||
let router = Router::new(
|
||||
BrowserUrl::new().unwrap(),
|
||||
Routes::new((
|
||||
NestedRoute {
|
||||
segments: StaticSegment("settings"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: Settings,
|
||||
},
|
||||
NestedRoute {
|
||||
segments: StaticSegment("about"),
|
||||
children: (),
|
||||
data: (),
|
||||
view: About,
|
||||
},
|
||||
)),
|
||||
|| "This page could not be found.",
|
||||
);
|
||||
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
// ordinary <a> elements can be used for client-side navigation
|
||||
// using <A> has two effects:
|
||||
// 1) ensuring that relative routing works properly for nested routes
|
||||
// 2) setting the `aria-current` attribute on the current link,
|
||||
// for a11y and styling purposes
|
||||
/*
|
||||
<A exact=true href="/">"Contacts"</A>
|
||||
<A href="about">"About"</A>
|
||||
<A href="settings">"Settings"</A>
|
||||
<A href="redirect-home">"Redirect to Home"</A>
|
||||
*/
|
||||
<a href="/">"Contacts"</a>
|
||||
<a href="about">"About"</a>
|
||||
<a href="settings">"Settings"</a>
|
||||
<a href="redirect-home">"Redirect to Home"</a>
|
||||
</nav>
|
||||
{router}
|
||||
/*<Router>
|
||||
<nav>
|
||||
// ordinary <a> elements can be used for client-side navigation
|
||||
// using <A> has two effects:
|
||||
|
@ -48,10 +97,11 @@ pub fn RouterExample() -> impl IntoView {
|
|||
/>
|
||||
</AnimatedRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
</Router>*/
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// You can define other routes in their own component.
|
||||
// Use a #[component(transparent)] that returns a <Route/>.
|
||||
#[component(transparent)]
|
||||
|
@ -71,8 +121,8 @@ pub fn ContactRoutes() -> impl IntoView {
|
|||
/>
|
||||
</Route>
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
||||
/*
|
||||
#[component]
|
||||
pub fn ContactList() -> impl IntoView {
|
||||
log::debug!("rendering <ContactList/>");
|
||||
|
@ -81,7 +131,7 @@ pub fn ContactList() -> impl IntoView {
|
|||
provide_context(ExampleContext(42));
|
||||
|
||||
on_cleanup(|| {
|
||||
log!("cleaning up <ContactList/>");
|
||||
info!("cleaning up <ContactList/>");
|
||||
});
|
||||
|
||||
let location = use_location();
|
||||
|
@ -113,8 +163,8 @@ pub fn ContactList() -> impl IntoView {
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
||||
/*
|
||||
#[derive(Params, PartialEq, Clone, Debug)]
|
||||
pub struct ContactParams {
|
||||
// Params isn't implemented for usize, only Option<usize>
|
||||
|
@ -123,15 +173,15 @@ pub struct ContactParams {
|
|||
|
||||
#[component]
|
||||
pub fn Contact() -> impl IntoView {
|
||||
log!("rendering <Contact/>");
|
||||
info!("rendering <Contact/>");
|
||||
|
||||
log!(
|
||||
info!(
|
||||
"ExampleContext should be Some(42). It is {:?}",
|
||||
use_context::<ExampleContext>()
|
||||
);
|
||||
|
||||
on_cleanup(|| {
|
||||
log!("cleaning up <Contact/>");
|
||||
info!("cleaning up <Contact/>");
|
||||
});
|
||||
|
||||
let params = use_params::<ContactParams>();
|
||||
|
@ -150,7 +200,7 @@ pub fn Contact() -> impl IntoView {
|
|||
);
|
||||
|
||||
create_effect(move |_| {
|
||||
log!("params = {:#?}", params.get());
|
||||
info!("params = {:#?}", params.get());
|
||||
});
|
||||
|
||||
let contact_display = move || match contact.get() {
|
||||
|
@ -180,45 +230,44 @@ pub fn Contact() -> impl IntoView {
|
|||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
#[component]
|
||||
pub fn About() -> impl IntoView {
|
||||
log!("rendering <About/>");
|
||||
info!("rendering <About/>");
|
||||
|
||||
on_cleanup(|| {
|
||||
log!("cleaning up <About/>");
|
||||
Owner::on_cleanup(|| {
|
||||
info!("cleaning up <About/>");
|
||||
});
|
||||
|
||||
log!(
|
||||
info!(
|
||||
"ExampleContext should be Some(0). It is {:?}",
|
||||
use_context::<ExampleContext>()
|
||||
);
|
||||
|
||||
// use_navigate allows you to navigate programmatically by calling a function
|
||||
let navigate = use_navigate();
|
||||
// TODO
|
||||
// let navigate = use_navigate();
|
||||
|
||||
view! {
|
||||
<>
|
||||
// note: this is just an illustration of how to use `use_navigate`
|
||||
// <button on:click> to navigate is an *anti-pattern*
|
||||
// you should ordinarily use a link instead,
|
||||
// both semantically and so your link will work before WASM loads
|
||||
<button on:click=move |_| navigate("/", Default::default())>
|
||||
"Home"
|
||||
</button>
|
||||
<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>
|
||||
</>
|
||||
// note: this is just an illustration of how to use `use_navigate`
|
||||
// <button on:click> to navigate is an *anti-pattern*
|
||||
// you should ordinarily use a link instead,
|
||||
// both semantically and so your link will work before WASM loads
|
||||
/*<button on:click=move |_| navigate("/", Default::default())>
|
||||
"Home"
|
||||
</button>*/
|
||||
<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() -> impl IntoView {
|
||||
log!("rendering <Settings/>");
|
||||
info!("rendering <Settings/>");
|
||||
|
||||
on_cleanup(|| {
|
||||
log!("cleaning up <Settings/>");
|
||||
Owner::on_cleanup(|| {
|
||||
info!("cleaning up <Settings/>");
|
||||
});
|
||||
|
||||
view! {
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
use leptos::*;
|
||||
use router::*;
|
||||
use tracing_subscriber::fmt;
|
||||
use tracing_subscriber_wasm::MakeConsoleWriter;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
fmt()
|
||||
.with_writer(
|
||||
// To avoide trace events in the browser from showing their
|
||||
// JS backtrace, which is very annoying, in my opinion
|
||||
MakeConsoleWriter::default()
|
||||
.map_trace_level_to(tracing::Level::DEBUG),
|
||||
)
|
||||
// For some reason, if we don't do this in the browser, we get
|
||||
// a runtime error.
|
||||
.without_time()
|
||||
.init();
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| view! { <RouterExample/> })
|
||||
}
|
||||
|
|
8
is_server/Cargo.toml
Normal file
8
is_server/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "is_server"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
14
is_server/src/lib.rs
Normal file
14
is_server/src/lib.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
pub fn add(left: usize, right: usize) -> usize {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
|
@ -72,7 +72,9 @@ serde = ["leptos_reactive/serde"]
|
|||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
miniserde = ["leptos_reactive/miniserde"]
|
||||
rkyv = ["leptos_reactive/rkyv", "server_fn/rkyv"]
|
||||
tracing = ["leptos_macro/tracing", "leptos_dom/tracing"]
|
||||
tracing = [
|
||||
"reactive_graph/tracing",
|
||||
] #, "leptos_macro/tracing", "leptos_dom/tracing"]
|
||||
nonce = ["leptos_dom/nonce"]
|
||||
spin = ["leptos_reactive/spin", "leptos-spin-macro"]
|
||||
experimental-islands = [
|
||||
|
|
|
@ -14,15 +14,14 @@ pub struct View<T>(T)
|
|||
where
|
||||
T: Sized;
|
||||
|
||||
pub trait IntoView:
|
||||
Sized + Render<Dom> + RenderHtml<Dom> + AddAnyAttr<Dom>
|
||||
pub trait IntoView: Sized + Render<Dom> + RenderHtml<Dom> //+ AddAnyAttr<Dom>
|
||||
{
|
||||
fn into_view(self) -> View<Self>;
|
||||
}
|
||||
|
||||
impl<T> IntoView for T
|
||||
where
|
||||
T: Sized + Render<Dom> + RenderHtml<Dom> + AddAnyAttr<Dom>,
|
||||
T: Sized + Render<Dom> + RenderHtml<Dom>, //+ AddAnyAttr<Dom>,
|
||||
{
|
||||
fn into_view(self) -> View<Self> {
|
||||
View(self)
|
||||
|
@ -79,7 +78,7 @@ impl<T: RenderHtml<Dom>> RenderHtml<Dom> for View<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: AddAnyAttr<Dom>> AddAnyAttr<Dom> for View<T> {
|
||||
/*impl<T: AddAnyAttr<Dom>> AddAnyAttr<Dom> for View<T> {
|
||||
type Output<SomeNewAttr: Attribute<Dom>> =
|
||||
<T as AddAnyAttr<Dom>>::Output<SomeNewAttr>;
|
||||
|
||||
|
@ -102,4 +101,4 @@ impl<T: AddAnyAttr<Dom>> AddAnyAttr<Dom> for View<T> {
|
|||
{
|
||||
self.0.add_any_attr_by_ref(attr)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
use crate::children::{ChildrenFn, ViewFn};
|
||||
use leptos_macro::component;
|
||||
use reactive_graph::{computed::ArcMemo, traits::Get};
|
||||
use tachys::{
|
||||
renderer::dom::Dom,
|
||||
view::{either::Either, RenderHtml},
|
||||
};
|
||||
use tachys::{either::Either, renderer::dom::Dom, view::RenderHtml};
|
||||
|
||||
#[component]
|
||||
pub fn Show<W>(
|
||||
|
|
|
@ -212,13 +212,12 @@ pub fn request_idle_callback_with_handle(
|
|||
cb: impl Fn() + 'static,
|
||||
) -> Result<IdleCallbackHandle, JsValue> {
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
let span = ::tracing::Span::current();
|
||||
#[cfg(feature = "tracing")]
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
|
||||
#[inline(never)]
|
||||
fn ric(cb: Box<dyn Fn()>) -> Result<IdleCallbackHandle, JsValue> {
|
||||
|
|
|
@ -26,10 +26,13 @@ impl ReactiveNode for RwLock<EffectInner> {
|
|||
fn mark_subscribers_check(&self) {}
|
||||
|
||||
fn update_if_necessary(&self) -> bool {
|
||||
let mut lock = self.write().or_poisoned();
|
||||
for source in lock.sources.take() {
|
||||
let sources = {
|
||||
let guard = self.read().or_poisoned();
|
||||
guard.sources.clone()
|
||||
};
|
||||
|
||||
for source in sources {
|
||||
if source.update_if_necessary() {
|
||||
lock.observer.notify();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use crate::{
|
||||
channel::channel,
|
||||
effect::inner::EffectInner,
|
||||
graph::{AnySubscriber, SourceSet, Subscriber, ToAnySubscriber},
|
||||
graph::{
|
||||
AnySubscriber, ReactiveNode, SourceSet, Subscriber, ToAnySubscriber,
|
||||
},
|
||||
owner::Owner,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
|
@ -62,14 +64,16 @@ where
|
|||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
subscriber.clear_sources(&subscriber);
|
||||
if subscriber.update_if_necessary() {
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun(old_value))
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun(old_value))
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,8 +4,40 @@ edition = "2021"
|
|||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true }
|
||||
either_of = { workspace = true }
|
||||
reactive_graph = { workspace = true }
|
||||
routing_utils = { workspace = true }
|
||||
tachys = { workspace = true }
|
||||
url = "2"
|
||||
js-sys = { version = "0.3" }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Document",
|
||||
"Window",
|
||||
"console",
|
||||
# History/Routing
|
||||
"History",
|
||||
"HtmlAnchorElement",
|
||||
"Location",
|
||||
"MouseEvent",
|
||||
"Url",
|
||||
# Form
|
||||
"FormData",
|
||||
"HtmlButtonElement",
|
||||
"HtmlFormElement",
|
||||
"HtmlInputElement",
|
||||
"SubmitEvent",
|
||||
"Url",
|
||||
"UrlSearchParams",
|
||||
# Fetching in Hydrate Mode
|
||||
"Headers",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
]
|
||||
|
|
|
@ -119,8 +119,8 @@ impl RouteList {
|
|||
// this is used to indicate to the Router that we are generating
|
||||
// a RouteList for server path generation
|
||||
thread_local! {
|
||||
static IS_GENERATING: Cell<bool> = Cell::new(false);
|
||||
static GENERATED: RefCell<Option<RouteList>> = RefCell::new(None);
|
||||
static IS_GENERATING: Cell<bool> = const { Cell::new(false) };
|
||||
static GENERATED: RefCell<Option<RouteList>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub fn generate<T, Rndr>(app: impl FnOnce() -> T) -> Option<Self>
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
//mod reactive;
|
||||
mod generate_route_list;
|
||||
pub mod location;
|
||||
mod method;
|
||||
mod params;
|
||||
mod router;
|
||||
mod ssr_mode;
|
||||
mod static_route;
|
||||
//pub use reactive::*;
|
||||
pub use generate_route_list::*;
|
||||
pub use method::*;
|
||||
pub use params::*;
|
||||
pub use router::*;
|
||||
pub use routing_utils::*;
|
||||
pub use ssr_mode::*;
|
||||
|
|
|
@ -1,282 +0,0 @@
|
|||
use super::{Location, LocationChange, State, Url, BASE};
|
||||
use crate::params::Params;
|
||||
use alloc::{borrow::Cow, boxed::Box, rc::Rc, string::String};
|
||||
use core::fmt;
|
||||
use js_sys::{try_iter, Array, JsString, Reflect};
|
||||
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
|
||||
use web_sys::{
|
||||
Document, Event, HtmlAnchorElement, MouseEvent, UrlSearchParams, Window,
|
||||
};
|
||||
|
||||
fn document() -> Document {
|
||||
window().document().expect(
|
||||
"router cannot be used in a JS environment without a `document`",
|
||||
)
|
||||
}
|
||||
|
||||
fn window() -> Window {
|
||||
web_sys::window()
|
||||
.expect("router cannot be used in a JS environment without a `window`")
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BrowserUrl {
|
||||
navigation_hook: Option<Rc<dyn Fn(Url)>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for BrowserUrl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("BrowserUrl").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl BrowserUrl {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn unescape(s: &str) -> String {
|
||||
js_sys::decode_uri(s).unwrap().into()
|
||||
}
|
||||
|
||||
fn scroll_to_el(loc_scroll: bool) {
|
||||
if let Ok(hash) = window().location().hash() {
|
||||
if !hash.is_empty() {
|
||||
let hash = js_sys::decode_uri(&hash[1..])
|
||||
.ok()
|
||||
.and_then(|decoded| decoded.as_string())
|
||||
.unwrap_or(hash);
|
||||
let el = document().get_element_by_id(&hash);
|
||||
if let Some(el) = el {
|
||||
el.scroll_into_view();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scroll to top
|
||||
if loc_scroll {
|
||||
window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Location for BrowserUrl {
|
||||
type Error = JsValue;
|
||||
|
||||
fn current(&self) -> Result<Url, Self::Error> {
|
||||
let location = window().location();
|
||||
Ok(Url {
|
||||
origin: location.origin()?,
|
||||
path: location.pathname()?,
|
||||
search: location
|
||||
.search()?
|
||||
.strip_prefix('?')
|
||||
.map(String::from)
|
||||
.unwrap_or_default(),
|
||||
search_params: search_params_from_web_url(
|
||||
&UrlSearchParams::new_with_str(&location.search()?)?,
|
||||
)?,
|
||||
hash: location.hash()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn init(&self) {
|
||||
let this = self.clone();
|
||||
let handle_anchor_click = move |ev: Event| {
|
||||
let ev = ev.unchecked_into::<MouseEvent>();
|
||||
if ev.default_prevented()
|
||||
|| ev.button() != 0
|
||||
|| ev.meta_key()
|
||||
|| ev.alt_key()
|
||||
|| ev.ctrl_key()
|
||||
|| ev.shift_key()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let composed_path = ev.composed_path();
|
||||
let mut a: Option<HtmlAnchorElement> = None;
|
||||
for i in 0..composed_path.length() {
|
||||
if let Ok(el) =
|
||||
composed_path.get(i).dyn_into::<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
|
||||
// TODO "state" is set as a prop, not an attribute
|
||||
if !target.is_empty()
|
||||
|| (href.is_empty() && !a.has_attribute("state"))
|
||||
{
|
||||
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 base = window()
|
||||
.location()
|
||||
.origin()
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or(Cow::Borrowed(BASE));
|
||||
let url = Self::parse_with_base(href.as_str(), &base).unwrap();
|
||||
let path_name = Self::unescape(&url.path);
|
||||
|
||||
// let browser handle this event if it leaves our domain
|
||||
// or our base path
|
||||
if url.origin
|
||||
!= window().location().origin().unwrap_or_default()
|
||||
// TODO base path for router
|
||||
/* || (true // TODO base_path //!self.base_path.is_empty()
|
||||
&& !path_name.is_empty()
|
||||
&& !path_name
|
||||
.to_lowercase()
|
||||
.starts_with(&self.base_path.to_lowercase())) */
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let to = path_name
|
||||
+ if url.search.is_empty() { "" } else { "?" }
|
||||
+ &Self::unescape(&url.search)
|
||||
+ &Self::unescape(&url.hash);
|
||||
let state = Reflect::get(&a, &JsValue::from_str("state"))
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
if value == JsValue::UNDEFINED {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
});
|
||||
|
||||
ev.prevent_default();
|
||||
|
||||
let replace = Reflect::get(&a, &JsValue::from_str("replace"))
|
||||
.ok()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
let change = LocationChange {
|
||||
value: to,
|
||||
replace,
|
||||
scroll: true,
|
||||
state: State(state),
|
||||
};
|
||||
|
||||
// run any router-specific hook
|
||||
if let Some(navigate_hook) = &this.navigation_hook {
|
||||
navigate_hook(url);
|
||||
}
|
||||
|
||||
// complete navigation
|
||||
this.navigate(&change);
|
||||
}
|
||||
};
|
||||
|
||||
let closure = Closure::wrap(
|
||||
Box::new(handle_anchor_click) as Box<dyn FnMut(Event)>
|
||||
)
|
||||
.into_js_value();
|
||||
window()
|
||||
.add_event_listener_with_callback(
|
||||
"click",
|
||||
closure.as_ref().unchecked_ref(),
|
||||
)
|
||||
.expect(
|
||||
"couldn't add `click` listener to `window` to handle `<a>` \
|
||||
clicks",
|
||||
);
|
||||
|
||||
// handle popstate event (forward/back navigation)
|
||||
if let Some(navigation_hook) = self.navigation_hook.clone() {
|
||||
let cb = {
|
||||
let this = self.clone();
|
||||
move || match this.current() {
|
||||
Ok(url) => navigation_hook(url),
|
||||
Err(e) => {
|
||||
#[cfg(debug_assertions)]
|
||||
web_sys::console::error_1(&e);
|
||||
_ = e;
|
||||
}
|
||||
}
|
||||
};
|
||||
let closure =
|
||||
Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
window()
|
||||
.add_event_listener_with_callback(
|
||||
"popstate",
|
||||
closure.as_ref().unchecked_ref(),
|
||||
)
|
||||
.expect("couldn't add `popstate` listener to `window`");
|
||||
}
|
||||
}
|
||||
|
||||
fn set_navigation_hook(&mut self, cb: impl Fn(Url) + 'static) {
|
||||
self.navigation_hook = Some(Rc::new(cb));
|
||||
}
|
||||
|
||||
fn navigate(&self, loc: &LocationChange) {
|
||||
let history = window().history().unwrap();
|
||||
|
||||
if loc.replace {
|
||||
history
|
||||
.replace_state_with_url(
|
||||
&loc.state.to_js_value(),
|
||||
"",
|
||||
Some(&loc.value),
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
// push the "forward direction" marker
|
||||
let state = &loc.state.to_js_value();
|
||||
history
|
||||
.push_state_with_url(state, "", Some(&loc.value))
|
||||
.unwrap();
|
||||
}
|
||||
// scroll to el
|
||||
Self::scroll_to_el(loc.scroll);
|
||||
}
|
||||
|
||||
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error> {
|
||||
let location = web_sys::Url::new_with_base(url, base)?;
|
||||
Ok(Url {
|
||||
origin: location.origin(),
|
||||
path: location.pathname(),
|
||||
search: location
|
||||
.search()
|
||||
.strip_prefix('?')
|
||||
.map(String::from)
|
||||
.unwrap_or_default(),
|
||||
search_params: search_params_from_web_url(
|
||||
&location.search_params(),
|
||||
)?,
|
||||
hash: location.hash(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn search_params_from_web_url(
|
||||
params: &web_sys::UrlSearchParams,
|
||||
) -> Result<Params<String>, JsValue> {
|
||||
let mut search_params = Params::new();
|
||||
for pair in try_iter(params)?.into_iter().flatten() {
|
||||
let row = pair?.unchecked_into::<Array>();
|
||||
search_params.push((
|
||||
row.get(0).unchecked_into::<JsString>().into(),
|
||||
row.get(1).unchecked_into::<JsString>().into(),
|
||||
));
|
||||
}
|
||||
Ok(search_params)
|
||||
}
|
189
routing/src/location/history.rs
Normal file
189
routing/src/location/history.rs
Normal file
|
@ -0,0 +1,189 @@
|
|||
use super::{handle_anchor_click, Location, LocationChange, State, Url, BASE};
|
||||
use crate::params::Params;
|
||||
use core::fmt;
|
||||
use js_sys::{try_iter, Array, JsString, Reflect};
|
||||
use reactive_graph::{signal::ArcRwSignal, traits::Set};
|
||||
use std::{borrow::Cow, boxed::Box, rc::Rc, string::String};
|
||||
use tachys::dom::{document, window};
|
||||
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
|
||||
use web_sys::{Event, HtmlAnchorElement, MouseEvent, UrlSearchParams};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BrowserUrl {
|
||||
url: ArcRwSignal<Url>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for BrowserUrl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("BrowserUrl").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl BrowserUrl {
|
||||
pub fn new() -> Result<Self, JsValue> {
|
||||
let url = ArcRwSignal::new(Self::current()?);
|
||||
Ok(Self { url })
|
||||
}
|
||||
|
||||
fn scroll_to_el(loc_scroll: bool) {
|
||||
if let Ok(hash) = window().location().hash() {
|
||||
if !hash.is_empty() {
|
||||
let hash = js_sys::decode_uri(&hash[1..])
|
||||
.ok()
|
||||
.and_then(|decoded| decoded.as_string())
|
||||
.unwrap_or(hash);
|
||||
let el = document().get_element_by_id(&hash);
|
||||
if let Some(el) = el {
|
||||
el.scroll_into_view();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scroll to top
|
||||
if loc_scroll {
|
||||
window().scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Location for BrowserUrl {
|
||||
type Error = JsValue;
|
||||
|
||||
fn as_url(&self) -> &ArcRwSignal<Url> {
|
||||
&self.url
|
||||
}
|
||||
|
||||
fn current() -> Result<Url, Self::Error> {
|
||||
let location = window().location();
|
||||
Ok(Url {
|
||||
origin: location.origin()?,
|
||||
path: location.pathname()?,
|
||||
search: location
|
||||
.search()?
|
||||
.strip_prefix('?')
|
||||
.map(String::from)
|
||||
.unwrap_or_default(),
|
||||
search_params: search_params_from_web_url(
|
||||
&UrlSearchParams::new_with_str(&location.search()?)?,
|
||||
)?,
|
||||
hash: location.hash()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error> {
|
||||
let location = web_sys::Url::new_with_base(url, base)?;
|
||||
Ok(Url {
|
||||
origin: location.origin(),
|
||||
path: location.pathname(),
|
||||
search: location
|
||||
.search()
|
||||
.strip_prefix('?')
|
||||
.map(String::from)
|
||||
.unwrap_or_default(),
|
||||
search_params: search_params_from_web_url(
|
||||
&location.search_params(),
|
||||
)?,
|
||||
hash: location.hash(),
|
||||
})
|
||||
}
|
||||
|
||||
fn init(&self, base: Option<Cow<'static, str>>) {
|
||||
let window = window();
|
||||
let navigate = {
|
||||
let url = self.url.clone();
|
||||
move |new_url, loc| {
|
||||
web_sys::console::log_1(&JsValue::from_str(
|
||||
"updating URL signal",
|
||||
));
|
||||
url.set(new_url);
|
||||
async move {
|
||||
Self::complete_navigation(&loc);
|
||||
}
|
||||
}
|
||||
};
|
||||
let handle_anchor_click =
|
||||
handle_anchor_click(base, Self::parse_with_base, navigate);
|
||||
let closure = Closure::wrap(Box::new(move |ev: Event| {
|
||||
if let Err(e) = handle_anchor_click(ev) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("{e:?}");
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
web_sys::console::error_1(&e);
|
||||
}
|
||||
}) as Box<dyn FnMut(Event)>)
|
||||
.into_js_value();
|
||||
window
|
||||
.add_event_listener_with_callback(
|
||||
"click",
|
||||
closure.as_ref().unchecked_ref(),
|
||||
)
|
||||
.expect(
|
||||
"couldn't add `click` listener to `window` to handle `<a>` \
|
||||
clicks",
|
||||
);
|
||||
|
||||
// handle popstate event (forward/back navigation)
|
||||
let cb = {
|
||||
let url = self.url.clone();
|
||||
move || match Self::current() {
|
||||
Ok(new_url) => url.set(new_url),
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("{e:?}");
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
web_sys::console::error_1(&e);
|
||||
}
|
||||
}
|
||||
};
|
||||
let closure =
|
||||
Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
window
|
||||
.add_event_listener_with_callback(
|
||||
"popstate",
|
||||
closure.as_ref().unchecked_ref(),
|
||||
)
|
||||
.expect("couldn't add `popstate` listener to `window`");
|
||||
}
|
||||
|
||||
fn complete_navigation(loc: &LocationChange) {
|
||||
let history = window().history().unwrap();
|
||||
|
||||
if loc.replace {
|
||||
history
|
||||
.replace_state_with_url(
|
||||
&loc.state.to_js_value(),
|
||||
"",
|
||||
Some(&loc.value),
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
// push the "forward direction" marker
|
||||
let state = &loc.state.to_js_value();
|
||||
history
|
||||
.push_state_with_url(state, "", Some(&loc.value))
|
||||
.unwrap();
|
||||
}
|
||||
// scroll to el
|
||||
Self::scroll_to_el(loc.scroll);
|
||||
}
|
||||
}
|
||||
|
||||
fn search_params_from_web_url(
|
||||
params: &web_sys::UrlSearchParams,
|
||||
) -> Result<Params, JsValue> {
|
||||
let mut search_params = Params::new();
|
||||
try_iter(params)?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|pair| {
|
||||
pair.and_then(|pair| {
|
||||
let row = pair.dyn_into::<Array>()?;
|
||||
Ok((
|
||||
row.get(0).dyn_into::<JsString>()?.into(),
|
||||
row.get(1).dyn_into::<JsString>()?.into(),
|
||||
))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
|
@ -1,11 +1,16 @@
|
|||
use crate::params::Params;
|
||||
use alloc::string::String;
|
||||
use any_spawner::Executor;
|
||||
use core::fmt::Debug;
|
||||
use wasm_bindgen::JsValue;
|
||||
use js_sys::Reflect;
|
||||
use reactive_graph::signal::ArcRwSignal;
|
||||
use std::{borrow::Cow, future::Future};
|
||||
use tachys::dom::window;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{Event, HtmlAnchorElement, MouseEvent};
|
||||
|
||||
mod browser;
|
||||
mod history;
|
||||
mod server;
|
||||
pub use browser::*;
|
||||
use crate::Params;
|
||||
pub use history::*;
|
||||
pub use server::*;
|
||||
|
||||
pub(crate) const BASE: &str = "https://leptos.dev";
|
||||
|
@ -15,7 +20,7 @@ pub struct Url {
|
|||
origin: String,
|
||||
path: String,
|
||||
search: String,
|
||||
search_params: Params<String>,
|
||||
search_params: Params,
|
||||
hash: String,
|
||||
}
|
||||
|
||||
|
@ -32,7 +37,7 @@ impl Url {
|
|||
&self.search
|
||||
}
|
||||
|
||||
pub fn search_params(&self) -> &Params<String> {
|
||||
pub fn search_params(&self) -> &Params {
|
||||
&self.search_params
|
||||
}
|
||||
|
||||
|
@ -69,15 +74,15 @@ impl Default for LocationChange {
|
|||
pub trait Location {
|
||||
type Error: Debug;
|
||||
|
||||
fn current(&self) -> Result<Url, Self::Error>;
|
||||
fn as_url(&self) -> &ArcRwSignal<Url>;
|
||||
|
||||
fn current() -> Result<Url, Self::Error>;
|
||||
|
||||
/// Sets up any global event listeners or other initialization needed.
|
||||
fn init(&self);
|
||||
fn init(&self, base: Option<Cow<'static, str>>);
|
||||
|
||||
fn set_navigation_hook(&mut self, cb: impl Fn(Url) + 'static);
|
||||
|
||||
/// Navigate to a new location.
|
||||
fn navigate(&self, loc: &LocationChange);
|
||||
/// Update the browser's history to reflect a new location.
|
||||
fn complete_navigation(loc: &LocationChange);
|
||||
|
||||
fn parse(url: &str) -> Result<Url, Self::Error> {
|
||||
Self::parse_with_base(url, BASE)
|
||||
|
@ -106,3 +111,115 @@ where
|
|||
State(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unescape(s: &str) -> String {
|
||||
js_sys::decode_uri(s).unwrap().into()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_anchor_click<NavFn, NavFut>(
|
||||
router_base: Option<Cow<'static, str>>,
|
||||
parse_with_base: fn(&str, &str) -> Result<Url, JsValue>,
|
||||
navigate: NavFn,
|
||||
) -> Box<dyn Fn(Event) -> Result<(), JsValue>>
|
||||
where
|
||||
NavFn: Fn(Url, LocationChange) -> NavFut + 'static,
|
||||
NavFut: Future<Output = ()> + 'static,
|
||||
{
|
||||
let router_base = router_base.unwrap_or_default();
|
||||
|
||||
Box::new(move |ev: Event| {
|
||||
let ev = ev.unchecked_into::<MouseEvent>();
|
||||
if ev.default_prevented()
|
||||
|| ev.button() != 0
|
||||
|| ev.meta_key()
|
||||
|| ev.alt_key()
|
||||
|| ev.ctrl_key()
|
||||
|| ev.shift_key()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let composed_path = ev.composed_path();
|
||||
let mut a: Option<HtmlAnchorElement> = None;
|
||||
for i in 0..composed_path.length() {
|
||||
if let Ok(el) = composed_path.get(i).dyn_into::<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
|
||||
// TODO "state" is set as a prop, not an attribute
|
||||
if !target.is_empty()
|
||||
|| (href.is_empty() && !a.has_attribute("state"))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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 Ok(());
|
||||
}
|
||||
|
||||
let base = window()
|
||||
.location()
|
||||
.origin()
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or(Cow::Borrowed(BASE));
|
||||
let url = parse_with_base(href.as_str(), &base).unwrap();
|
||||
let path_name = unescape(&url.path);
|
||||
ev.prevent_default();
|
||||
|
||||
// let browser handle this event if it leaves our domain
|
||||
// or our base path
|
||||
if url.origin != window().location().origin().unwrap_or_default()
|
||||
|| (!router_base.is_empty()
|
||||
&& !path_name.is_empty()
|
||||
&& !path_name
|
||||
.to_lowercase()
|
||||
.starts_with(&router_base.to_lowercase()))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let to = path_name
|
||||
+ if url.search.is_empty() { "" } else { "?" }
|
||||
+ &unescape(&url.search)
|
||||
+ &unescape(&url.hash);
|
||||
let state = Reflect::get(&a, &JsValue::from_str("state"))
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
if value == JsValue::UNDEFINED {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
});
|
||||
|
||||
ev.prevent_default();
|
||||
|
||||
let replace = Reflect::get(&a, &JsValue::from_str("replace"))
|
||||
.ok()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
let change = LocationChange {
|
||||
value: to,
|
||||
replace,
|
||||
scroll: true,
|
||||
state: State(state),
|
||||
};
|
||||
|
||||
Executor::spawn_local(navigate(url, change));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,37 +1,31 @@
|
|||
use super::{Location, LocationChange, Url};
|
||||
use alloc::string::{String, ToString};
|
||||
use core::fmt::Display;
|
||||
use super::{Url, BASE};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RequestUrl(String);
|
||||
pub struct RequestUrl(Arc<str>);
|
||||
|
||||
impl RequestUrl {
|
||||
/// Creates a server-side request URL from a path.
|
||||
pub fn new(path: impl Display) -> Self {
|
||||
Self(path.to_string())
|
||||
pub fn new(path: &str) -> Self {
|
||||
Self(path.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RequestUrl {
|
||||
fn default() -> Self {
|
||||
Self(String::from("/"))
|
||||
Self::new("/")
|
||||
}
|
||||
}
|
||||
|
||||
impl Location for RequestUrl {
|
||||
type Error = url::ParseError;
|
||||
|
||||
fn current(&self) -> Result<Url, Self::Error> {
|
||||
Self::parse(&self.0)
|
||||
impl RequestUrl {
|
||||
fn parse(url: &str) -> Result<Url, url::ParseError> {
|
||||
Self::parse_with_base(url, BASE)
|
||||
}
|
||||
|
||||
fn init(&self) {}
|
||||
|
||||
fn set_navigation_hook(&mut self, _cb: impl FnMut(Url) + 'static) {}
|
||||
|
||||
fn navigate(&self, _loc: &LocationChange) {}
|
||||
|
||||
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error> {
|
||||
pub fn parse_with_base(
|
||||
url: &str,
|
||||
base: &str,
|
||||
) -> Result<Url, url::ParseError> {
|
||||
let base = url::Url::parse(base)?;
|
||||
let url = url::Url::options().base_url(Some(&base)).parse(url)?;
|
||||
|
||||
|
@ -53,7 +47,6 @@ impl Location for RequestUrl {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::RequestUrl;
|
||||
use crate::location::Location;
|
||||
|
||||
#[test]
|
||||
pub fn should_parse_url_without_origin() {
|
||||
|
|
14
routing/src/params.rs
Normal file
14
routing/src/params.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Params(Vec<(String, String)>);
|
||||
|
||||
impl Params {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(String, String)> for Params {
|
||||
fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
use crate::generate_route_list::RouteList;
|
||||
use crate::{generate_route_list::RouteList, location::Location};
|
||||
use core::marker::PhantomData;
|
||||
use either_of::Either;
|
||||
use reactive_graph::{
|
||||
computed::ArcMemo,
|
||||
effect::RenderEffect,
|
||||
traits::{Read, Track},
|
||||
};
|
||||
use routing_utils::{
|
||||
location::Location,
|
||||
matching::{MatchInterface, MatchNestedRoutes, PossibleRouteMatch, Routes},
|
||||
MatchInterface, MatchNestedRoutes, PossibleRouteMatch, Routes,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use tachys::{
|
||||
|
@ -61,10 +65,6 @@ where
|
|||
rndr: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_location(&mut self, new_location: Loc) {
|
||||
self.location = new_location;
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rndr, Loc, Children, FallbackFn, Fallback>
|
||||
|
@ -114,51 +114,54 @@ impl<Rndr, Loc, FallbackFn, Fallback, Children, View> Render<Rndr>
|
|||
for Router<Rndr, Loc, Children, FallbackFn>
|
||||
where
|
||||
Loc: Location,
|
||||
FallbackFn: Fn() -> Fallback,
|
||||
FallbackFn: Fn() -> Fallback + 'static,
|
||||
Fallback: Render<Rndr>,
|
||||
for<'a> Children: MatchNestedRoutes<'a>,
|
||||
for<'a> Children: MatchNestedRoutes<'a> + 'static,
|
||||
for<'a> <<Children as MatchNestedRoutes<'a>>::Match as MatchInterface<'a>>::View:
|
||||
ChooseView<Output = View>,
|
||||
View: Render<Rndr>,
|
||||
Rndr: Renderer,
|
||||
View::State: 'static,
|
||||
Fallback::State: 'static,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
type State =
|
||||
EitherState<View::State, <Fallback as Render<Rndr>>::State, Rndr>;
|
||||
type State = RenderEffect<
|
||||
EitherState<View::State, <Fallback as Render<Rndr>>::State, Rndr>,
|
||||
>;
|
||||
type FallibleState = ();
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
match self
|
||||
.location
|
||||
.current()
|
||||
.as_ref()
|
||||
.map(|url| self.routes.match_route(url.path()))
|
||||
{
|
||||
Ok(Some(matched)) => {
|
||||
let view = matched.to_view();
|
||||
let view = view.choose();
|
||||
Either::Left(view)
|
||||
self.location.init(self.base);
|
||||
let url = self.location.as_url().clone();
|
||||
let path = ArcMemo::new({
|
||||
let url = url.clone();
|
||||
move |_| url.read().path().to_string()
|
||||
});
|
||||
let search_parans = ArcMemo::new({
|
||||
let url = url.clone();
|
||||
move |_| url.read().search_params().clone()
|
||||
});
|
||||
RenderEffect::new(move |prev| {
|
||||
tachys::dom::log(&format!("recalculating route"));
|
||||
let path = path.read();
|
||||
let new_view = match self.routes.match_route(&*path) {
|
||||
Some(matched) => {
|
||||
let view = matched.to_view();
|
||||
let view = view.choose();
|
||||
Either::Left(view)
|
||||
}
|
||||
_ => Either::Right((self.fallback)()),
|
||||
};
|
||||
|
||||
if let Some(mut prev) = prev {
|
||||
new_view.rebuild(&mut prev);
|
||||
prev
|
||||
} else {
|
||||
new_view.build()
|
||||
}
|
||||
_ => Either::Right((self.fallback)()),
|
||||
}
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
let new = match self
|
||||
.location
|
||||
.current()
|
||||
.as_ref()
|
||||
.map(|url| self.routes.match_route(url.path()))
|
||||
{
|
||||
Ok(Some(matched)) => {
|
||||
let view = matched.to_view();
|
||||
let view = view.choose();
|
||||
Either::Left(view)
|
||||
}
|
||||
_ => Either::Right((self.fallback)()),
|
||||
};
|
||||
new.rebuild(state);
|
||||
}
|
||||
fn rebuild(self, state: &mut Self::State) {}
|
||||
|
||||
fn try_build(self) -> tachys::error::Result<Self::FallibleState> {
|
||||
todo!()
|
||||
|
@ -176,13 +179,15 @@ impl<Rndr, Loc, FallbackFn, Fallback, Children, View> RenderHtml<Rndr>
|
|||
for Router<Rndr, Loc, Children, FallbackFn>
|
||||
where
|
||||
Loc: Location,
|
||||
FallbackFn: Fn() -> Fallback,
|
||||
FallbackFn: Fn() -> Fallback + 'static,
|
||||
Fallback: RenderHtml<Rndr>,
|
||||
for<'a> Children: MatchNestedRoutes<'a>,
|
||||
for<'a> Children: MatchNestedRoutes<'a> + 'static,
|
||||
for<'a> <<Children as MatchNestedRoutes<'a>>::Match as MatchInterface<'a>>::View:
|
||||
ChooseView<Output = View>,
|
||||
View: Render<Rndr>,
|
||||
Rndr: Renderer,
|
||||
View::State: 'static,
|
||||
Fallback::State: 'static,
|
||||
Rndr: Renderer + 'static,
|
||||
{
|
||||
// TODO probably pick a max length here
|
||||
const MIN_LENGTH: usize = Fallback::MIN_LENGTH;
|
||||
|
|
|
@ -4,43 +4,5 @@ edition = "2021"
|
|||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
const_str_slice_concat = { workspace = true }
|
||||
either_of = { workspace = true }
|
||||
next_tuple = { workspace = true }
|
||||
reactive_graph = { workspace = true, optional = true }
|
||||
url = "2"
|
||||
js-sys = { version = "0.3" }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Document",
|
||||
"Window",
|
||||
"console",
|
||||
# History/Routing
|
||||
"History",
|
||||
"HtmlAnchorElement",
|
||||
"Location",
|
||||
"MouseEvent",
|
||||
"Url",
|
||||
# Form
|
||||
"FormData",
|
||||
"HtmlButtonElement",
|
||||
"HtmlFormElement",
|
||||
"HtmlInputElement",
|
||||
"SubmitEvent",
|
||||
"Url",
|
||||
"UrlSearchParams",
|
||||
# Fetching in Hydrate Mode
|
||||
"Headers",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
]
|
||||
|
||||
[features]
|
||||
tracing = ["dep:tracing"]
|
||||
reactive_graph = ["dep:reactive_graph"]
|
||||
|
|
|
@ -9,7 +9,6 @@ pub use path_segment::*;
|
|||
mod horizontal;
|
||||
mod nested;
|
||||
mod vertical;
|
||||
use crate::PathSegment;
|
||||
use alloc::borrow::Cow;
|
||||
pub use horizontal::*;
|
||||
pub use nested::*;
|
||||
|
|
|
@ -37,6 +37,7 @@ pub mod ssr;
|
|||
pub mod svg;
|
||||
pub mod view;
|
||||
|
||||
pub use either_of as either;
|
||||
#[cfg(feature = "islands")]
|
||||
pub use wasm_bindgen;
|
||||
#[cfg(feature = "islands")]
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
use crate::{
|
||||
async_views::Suspend,
|
||||
error::AnyError,
|
||||
html::{attribute::AttributeValue, property::IntoProperty},
|
||||
html::{
|
||||
attribute::{Attribute, AttributeValue},
|
||||
property::IntoProperty,
|
||||
},
|
||||
hydration::Cursor,
|
||||
renderer::{DomRenderer, Renderer},
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
Mountable, Position, PositionState, Render, RenderHtml, ToTemplate,
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml, ToTemplate,
|
||||
},
|
||||
};
|
||||
use reactive_graph::{
|
||||
|
@ -267,6 +271,37 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<F, V, R> AddAnyAttr<R> for F
|
||||
where
|
||||
F: FnMut() -> V + 'static,
|
||||
V: RenderHtml<R>,
|
||||
V::State: 'static,
|
||||
V::FallibleState: 'static,
|
||||
R: Renderer + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<R>> = Self;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<R>>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<R>,
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
fn add_any_attr_by_ref<NewAttr: Attribute<R>>(
|
||||
self,
|
||||
attr: &NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<R>,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, R> Mountable<R> for RenderEffect<M>
|
||||
where
|
||||
M: Mountable<R> + 'static,
|
||||
|
|
Loading…
Reference in a new issue