Compare commits

..

1 Commits
v0.8.3 ... 3872

Author SHA1 Message Date
Greg Johnston
bd4140b7a3 fix: schedule ActionForm submit handler to run after user submit handlers (closes #3872) 2025-07-03 13:25:14 -04:00
32 changed files with 283 additions and 639 deletions

380
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.3"
version = "0.8.2"
edition = "2021"
rust-version = "1.80"
@@ -51,25 +51,25 @@ any_spawner = { path = "./any_spawner/", version = "0.3.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.5" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
leptos = { path = "./leptos", version = "0.8.3" }
leptos_config = { path = "./leptos_config", version = "0.8.3" }
leptos_dom = { path = "./leptos_dom", version = "0.8.3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.3" }
leptos_macro = { path = "./leptos_macro", version = "0.8.3" }
leptos_router = { path = "./router", version = "0.8.3" }
leptos_router_macro = { path = "./router_macro", version = "0.8.3" }
leptos_server = { path = "./leptos_server", version = "0.8.3" }
leptos_meta = { path = "./meta", version = "0.8.3" }
leptos = { path = "./leptos", version = "0.8.2" }
leptos_config = { path = "./leptos_config", version = "0.8.2" }
leptos_dom = { path = "./leptos_dom", version = "0.8.2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.2" }
leptos_macro = { path = "./leptos_macro", version = "0.8.2" }
leptos_router = { path = "./router", version = "0.8.2" }
leptos_router_macro = { path = "./router_macro", version = "0.8.2" }
leptos_server = { path = "./leptos_server", version = "0.8.2" }
leptos_meta = { path = "./meta", version = "0.8.2" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.0" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0" }
server_fn = { path = "./server_fn", version = "0.8.3" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.3" }
server_fn = { path = "./server_fn", version = "0.8.2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.2" }
tachys = { path = "./tachys", version = "0.2.0" }
# members deps
@@ -108,7 +108,7 @@ serde = { default-features = false, version = "1.0.219" }
parking_lot = { default-features = false, version = "0.12.4" }
axum = { default-features = false, version = "0.8.4" }
serde_qs = { default-features = false, version = "0.15.0" }
syn = { default-features = false, version = "2.0.104" }
syn = { default-features = false, version = "2.0.101" }
xxhash-rust = { default-features = false, version = "0.8.15" }
paste = { default-features = false, version = "1.0.15" }
quote = { default-features = false, version = "1.0.40" }
@@ -116,10 +116,10 @@ web-sys = { default-features = false, version = "0.3.77" }
js-sys = { default-features = false, version = "0.3.77" }
rand = { default-features = false, version = "0.9.1" }
serde-lite = { default-features = false, version = "0.5.0" }
tokio-tungstenite = { default-features = false, version = "0.27.0" }
tokio-tungstenite = { default-features = false, version = "0.26.2" }
serial_test = { default-features = false, version = "3.2.0" }
erased = { default-features = false, version = "0.1.2" }
glib = { default-features = false, version = "0.20.12" }
glib = { default-features = false, version = "0.20.10" }
async-trait = { default-features = false, version = "0.1.88" }
typed-builder-macro = { default-features = false, version = "0.21.0" }
linear-map = { default-features = false, version = "1.2.0" }
@@ -127,7 +127,7 @@ anyhow = { default-features = false, version = "1.0.98" }
walkdir = { default-features = false, version = "2.5.0" }
actix-ws = { default-features = false, version = "0.3.0" }
tower-http = { default-features = false, version = "0.6.4" }
prettyplease = { default-features = false, version = "0.2.35" }
prettyplease = { default-features = false, version = "0.2.33" }
inventory = { default-features = false, version = "0.3.20" }
config = { default-features = false, version = "0.15.11" }
camino = { default-features = false, version = "1.1.9" }

View File

@@ -118,7 +118,7 @@ The `nightly` feature enables the function call syntax for accessing and setting
```bash
cargo install cargo-leptos
cargo leptos new --git https://github.com/leptos-rs/start-axum
cargo leptos new --git https://github.com/leptos-rs/start
cd [your project name]
cargo leptos watch
```

View File

@@ -1,20 +0,0 @@
@check_issue_4088
Feature: Check that issue 4088 does not reappear
Scenario: I can see the navbar
Given I see the app
And I can access regression test 4088
Then I see the navbar
Scenario: The user info is shared via context
Given I see the app
And I can access regression test 4088
When I select the link Class 1
Then I see the result is the string Assignments for team of user with id 42
Scenario: The user info is shared via context
Given I see the app
And I can access regression test 4088
When I select the link Class 1
When I refresh the browser
Then I see the result is the string Assignments for team of user with id 42

View File

@@ -1,8 +0,0 @@
@check_pr_4015
Feature: Check that PR 4015 does not regress
Scenario: The correct text appears
Given I see the app
And I can access regression test 4015
Then I see the result is the string Some(42)

View File

@@ -24,25 +24,3 @@ Feature: Regression from pull request 4091
| test1 |
| 4091 Home |
Then I see the result is empty
Scenario: I can see the navbar
Given I see the app
And I can access regression test 4091
Then I see the navbar
Scenario: If I navigate to home and back, I can still see the navbar
Given I see the app
And I can access regression test 4091
When I select the following links
| Home |
| 4091 |
Then I see the navbar
Scenario: The signal is not disposed too early
Given I see the app
And I can access regression test 4091
When I select the following links
| test1 |
| Home |
| 4091 |
Then I see the navbar

View File

@@ -11,10 +11,3 @@ pub async fn result_text_is(
assert_eq!(&actual, expected_text);
Ok(())
}
pub async fn element_exists(client: &Client, id: &str) -> Result<()> {
find::element_by_id(client, id)
.await
.expect(&format!("could not find element with id `{id}`"));
Ok(())
}

View File

@@ -2,7 +2,9 @@ use anyhow::{Ok, Result};
use fantoccini::{elements::Element, Client, Locator};
pub async fn text_at_id(client: &Client, id: &str) -> Result<String> {
let element = element_by_id(client, id)
let element = client
.wait()
.for_element(Locator::Id(id))
.await
.expect(format!("no such element with id `{}`", id).as_str());
let text = element.text().await?;
@@ -17,7 +19,3 @@ pub async fn link_with_text(client: &Client, text: &str) -> Result<Element> {
.expect(format!("Link not found by `{}`", text).as_str());
Ok(link)
}
pub async fn element_by_id(client: &Client, id: &str) -> Result<Element> {
Ok(client.wait().for_element(Locator::Id(id)).await?)
}

View File

@@ -3,7 +3,9 @@ use anyhow::{Ok, Result};
use cucumber::then;
#[then(regex = r"^I see the result is empty$")]
async fn i_see_the_result_is_empty(world: &mut AppWorld) -> Result<()> {
async fn i_see_the_result_is_empty(
world: &mut AppWorld,
) -> Result<()> {
let client = &world.client;
check::result_text_is(client, "").await?;
Ok(())
@@ -18,10 +20,3 @@ async fn i_see_the_result_is_the_string(
check::result_text_is(client, &text).await?;
Ok(())
}
#[then(regex = r"^I see the navbar$")]
async fn i_see_the_navbar(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::element_exists(client, "nav").await?;
Ok(())
}

View File

@@ -1,4 +1,4 @@
use crate::{issue_4088::Routes4088, pr_4015::Routes4015, pr_4091::Routes4091};
use crate::pr_4091::Routes4091;
use leptos::prelude::*;
use leptos_meta::{MetaTags, *};
use leptos_router::{
@@ -35,8 +35,6 @@ pub fn App() -> impl IntoView {
<Routes fallback>
<Route path=path!("") view=HomePage/>
<Routes4091/>
<Routes4015/>
<Routes4088/>
</Routes>
</main>
</Router>
@@ -57,8 +55,6 @@ fn HomePage() -> impl IntoView {
<nav>
<ul>
<li><a href="/4091/">"4091"</a></li>
<li><a href="/4015/">"4015"</a></li>
<li><a href="/4088/">"4088"</a></li>
</ul>
</nav>
}

View File

@@ -1,119 +0,0 @@
use leptos::{either::Either, prelude::*};
#[allow(unused_imports)]
use leptos_router::{
components::{Outlet, ParentRoute, Redirect, Route},
path, MatchNestedRoutes, NavigateOptions,
};
use serde::{Deserialize, Serialize};
#[component]
pub fn Routes4088() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("4088") view=|| view!{ <LoggedIn/> }>
<ParentRoute path=path!("") view=||view!{<AssignmentsSelector/>}>
<Route path=path!("/:team_id") view=||view!{<AssignmentsForTeam/>} />
<Route path=path!("") view=||view!{ <p>No class selected</p> }/>
</ParentRoute>
</ParentRoute>
}
.into_inner()
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserInfo {
pub id: usize,
}
#[server]
pub async fn get_user_info() -> Result<Option<UserInfo>, ServerFnError> {
Ok(Some(UserInfo { id: 42 }))
}
#[component]
pub fn LoggedIn() -> impl IntoView {
let user_info_resource =
Resource::new(|| (), move |_| async { get_user_info().await });
view! {
<Transition fallback=move || view!{
"loading"
}
>
{move || {
user_info_resource.get()
.map(|a|
match a {
Ok(Some(a)) => Either::Left(view! {
<LoggedInContent user_info={a} />
}),
_ => Either::Right(view!{
<Redirect path="/not_logged_in"/>
})
})
}}
</Transition>
}
}
#[component]
/// Component which provides UserInfo and renders it's child
/// Can also contain some code to check for specific situations (e.g. privacy policies accepted or not? redirect if needed...)
pub fn LoggedInContent(user_info: UserInfo) -> impl IntoView {
provide_context(user_info.clone());
if user_info.id == 42 {
Either::Left(Outlet())
} else {
Either::Right(
view! { <Redirect path="/somewhere" options={NavigateOptions::default()}/> },
)
}
}
#[component]
/// This component also uses Outlet (so nested Outlet)
fn AssignmentsSelector() -> impl IntoView {
let user_info = use_context::<UserInfo>().expect("user info not provided");
view! {
<p>"Assignments for user with ID: "{user_info.id}</p>
<ul id="nav">
<li><a href="/4088/1">"Class 1"</a></li>
<li><a href="/4088/2">"Class 2"</a></li>
<li><a href="/4088/3">"Class 3"</a></li>
</ul>
<Outlet />
}
}
#[component]
fn AssignmentsForTeam() -> impl IntoView {
// THIS FAILS -> Because of the nested outlet in LoggedInContent > AssignmentsSelector?
// It did not fail when LoggedIn did not use a resource and transition (but a hardcoded UserInfo in the component)
let user_info = use_context::<UserInfo>().expect("user info not provided");
let items = vec!["Assignment 1", "Assignment 2", "Assignment 3"];
view! {
<p id="result">"Assignments for team of user with id " {user_info.id}</p>
<ul>
{
items.into_iter().map(|item| {
view! {
<Assignment name=item.to_string() />
}
}).collect_view()
}
</ul>
}
}
#[component]
fn Assignment(name: String) -> impl IntoView {
let user_info = use_context::<UserInfo>().expect("user info not provided");
view! {
<li>{name}" "{user_info.id}</li>
}
}

View File

@@ -1,6 +1,4 @@
pub mod app;
mod issue_4088;
mod pr_4015;
mod pr_4091;
#[cfg(feature = "hydrate")]

View File

@@ -1,29 +0,0 @@
use leptos::{context::Provider, prelude::*};
use leptos_router::{
components::{ParentRoute, Route},
nested_router::Outlet,
path,
};
#[component]
pub fn Routes4015() -> impl leptos_router::MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("4015") view=|| view! {
<Provider value=42i32>
<Outlet/>
</Provider>
}>
<Route path=path!("") view=Child/>
</ParentRoute>
}
.into_inner()
}
#[component]
fn Child() -> impl IntoView {
let value = use_context::<i32>();
view! {
<p id="result">{format!("{value:?}")}</p>
}
}

View File

@@ -28,9 +28,8 @@ fn Container() -> impl IntoView {
provide_context(rw_signal);
view! {
<nav id="nav">
<nav>
<ul>
<li><A href="/">"Home"</A></li>
<li><A href="./">"4091 Home"</A></li>
<li><A href="test1">"test1"</A></li>
</ul>

View File

@@ -159,7 +159,7 @@ fn TodoRow(
view! {
<li style:text-decoration=move || {
if status.done() { "line-through" } else { Default::default() }
status.done().then_some("line-through").unwrap_or_default()
}>
<p

View File

@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = "0.8.3"
version = "0.8.2"
rust-version.workspace = true
edition.workspace = true
@@ -13,7 +13,7 @@ any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { default-features = false, features = [
"matched-path",
], workspace = true }
] , workspace = true }
dashmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
leptos = { workspace = true, features = ["nonce", "ssr"] }
@@ -24,17 +24,14 @@ leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
tachys = { workspace = true }
parking_lot = { workspace = true, default-features = true }
tokio = { default-features = false, workspace = true }
tower = { features = ["util"], workspace = true, default-features = true }
tokio = { default-features = false , workspace = true }
tower = { features = ["util"] , workspace = true, default-features = true }
tower-http = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
axum = { workspace = true, default-features = true }
tokio = { features = [
"net",
"rt-multi-thread",
], workspace = true, default-features = true }
tokio = { features = ["net", "rt-multi-thread"] , workspace = true, default-features = true }
[features]
wasm = []

View File

@@ -111,28 +111,39 @@ where
let on_submit = {
move |ev: SubmitEvent| {
if ev.default_prevented() {
return;
}
ev.prevent_default();
match ServFn::from_event(&ev) {
Ok(new_input) => {
action.dispatch(new_input);
// request_animation_frame here schedules this event handler to run slightly later
// this means that this `submit` handler will run *after* any other `submit` handlers
// that have been added by the user. this is useful because it means that the user can
// add an `on:submit` handler and call `ev.prevent_default()` to prevent the form submission
//
// without this delay, this handler will always run before the user's handler (which was added
// later), which means the user can't prevent the form submission in the same way
//
// see https://github.com/leptos-rs/leptos/issues/3872
request_animation_frame(move || {
if ev.default_prevented() {
return;
}
Err(err) => {
crate::logging::error!(
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(Some(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)
.into_app_error())));
version.update(|n| *n += 1);
ev.prevent_default();
match ServFn::from_event(&ev) {
Ok(new_input) => {
action.dispatch(new_input);
}
Err(err) => {
crate::logging::error!(
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(Some(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)
.into_app_error())));
version.update(|n| *n += 1);
}
}
}
});
}
};

View File

@@ -90,7 +90,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
type Owned = View<T::Owned>;
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
const EXISTS: bool = <T as RenderHtml>::EXISTS;
async fn resolve(self) -> Self::AsyncOutput {
self.inner.resolve().await

View File

@@ -44,8 +44,6 @@ pub fn render_view(
view_marker: Option<String>,
disable_inert_html: bool,
) -> Option<TokenStream> {
let disable_inert_html = disable_inert_html || global_class.is_some();
let (base, should_add_view) = match nodes.len() {
0 => {
let span = Span::call_site();
@@ -403,9 +401,6 @@ fn inert_element_to_tokens(
}
}
/// # Note
/// Should not be used on top level `<svg>` elements.
/// Use [`inert_element_to_tokens`] instead.
fn inert_svg_element_to_tokens(
node: &Node<impl CustomNode>,
escape_text: bool,
@@ -709,7 +704,7 @@ fn node_to_tokens(
&& el_name != "textarea";
let el_name = el_node.name().to_string();
if is_svg_element(&el_name) && el_name != "svg" {
if is_svg_element(&el_name) {
Some(inert_svg_element_to_tokens(
node,
escape,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.8.3"
version = "0.8.2"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -13,8 +13,8 @@ leptos = { workspace = true }
or_poisoned = { workspace = true }
indexmap = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
wasm-bindgen = { workspace = true, default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
wasm-bindgen = { workspace = true , default-features = true }
futures = { workspace = true, default-features = true }
[dependencies.web-sys]

View File

@@ -413,7 +413,6 @@ where
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
const MIN_LENGTH: usize = 0;
const EXISTS: bool = false;
fn dry_resolve(&mut self) {
self.el.dry_resolve()

View File

@@ -322,7 +322,6 @@ impl RenderHtml for TitleView {
type Owned = Self;
const MIN_LENGTH: usize = 0;
const EXISTS: bool = false;
fn dry_resolve(&mut self) {}

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.3"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -16,26 +16,19 @@ futures = { workspace = true, default-features = true }
hydration_context = { workspace = true, optional = true }
pin-project-lite = { workspace = true, default-features = true }
rustc-hash = { workspace = true, default-features = true }
serde = { features = [
"derive",
], optional = true, workspace = true, default-features = true }
serde = { features = ["derive"], optional = true , workspace = true, default-features = true }
slotmap = { workspace = true, default-features = true }
thiserror = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
guardian = { workspace = true, default-features = true }
async-lock = { workspace = true, default-features = true }
send_wrapper = { features = [
"futures",
], workspace = true, default-features = true }
send_wrapper = { features = ["futures"] , workspace = true, default-features = true }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = { version = "0.3.77", features = ["console"] }
[dev-dependencies]
tokio = { features = [
"rt-multi-thread",
"macros",
], workspace = true, default-features = true }
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
tokio-test = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.2.3"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -11,7 +11,7 @@ edition.workspace = true
[dependencies]
guardian = { workspace = true, default-features = true }
itertools = { workspace = true, default-features = true }
itertools = { workspace = true , default-features = true }
or_poisoned = { workspace = true }
paste = { workspace = true, default-features = true }
reactive_graph = { workspace = true }
@@ -21,10 +21,7 @@ dashmap = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
[dev-dependencies]
tokio = { features = [
"rt-multi-thread",
"macros",
], workspace = true, default-features = true }
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
tokio-test = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
reactive_graph = { workspace = true, features = ["effects"] }

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores_macro"
version = "0.2.3"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -13,8 +13,8 @@ edition.workspace = true
proc-macro = true
[dependencies]
convert_case = { workspace = true, default-features = true }
convert_case = { workspace = true , default-features = true }
proc-macro-error2 = { workspace = true, default-features = true }
proc-macro2 = { workspace = true, default-features = true }
quote = { workspace = true, default-features = true }
syn = { features = ["full"], workspace = true, default-features = true }
syn = { features = ["full"] , workspace = true, default-features = true }

View File

@@ -111,8 +111,10 @@ impl ToTokens for Model {
} = &self;
let any_store_field = Ident::new("AnyStoreField", Span::call_site());
let trait_name = Ident::new(&format!("{name}StoreFields"), name.span());
let params = &generics.params;
let generics_with_orig = quote! { <#any_store_field, #params> };
let generics_with_orig = {
let params = &generics.params;
quote! { <#any_store_field, #params> }
};
let where_with_orig = {
generics
.where_clause
@@ -138,13 +140,13 @@ impl ToTokens for Model {
// read access
tokens.extend(quote! {
#vis trait #trait_name <AnyStoreField, #params>
#vis trait #trait_name <AnyStoreField>
#where_with_orig
{
#(#trait_fields)*
}
impl #generics_with_orig #trait_name <AnyStoreField, #params> for AnyStoreField
impl #generics_with_orig #trait_name <AnyStoreField> for AnyStoreField
#where_with_orig
{
#(#read_fields)*

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.8.3"
version = "0.8.2"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -20,11 +20,11 @@ tachys = { workspace = true, features = ["reactive_graph"] }
futures = { workspace = true, default-features = true }
url = { workspace = true, default-features = true }
js-sys = { workspace = true, default-features = true }
wasm-bindgen = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
wasm-bindgen = { workspace = true , default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
thiserror = { workspace = true, default-features = true }
percent-encoding = { optional = true, workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
percent-encoding = { optional = true , workspace = true, default-features = true }
gloo-net = { workspace = true, default-features = true }
[dependencies.web-sys]

View File

@@ -109,6 +109,7 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
drop(url);
@@ -179,6 +180,7 @@ where
&mut preloaders,
&mut full_loaders,
&mut state.outlets,
&self.outer_owner,
self.set_is_routing.is_some(),
0,
);
@@ -338,6 +340,7 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
// outlets will not send their views if the loaders are never polled
@@ -391,6 +394,7 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
// outlets will not send their views if the loaders are never polled
@@ -444,6 +448,7 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
drop(url);
@@ -478,10 +483,10 @@ pub(crate) struct RouteContext {
trigger: ArcTrigger,
url: ArcRwSignal<Url>,
params: ArcRwSignal<ParamsMap>,
owner: Owner,
pub matched: ArcRwSignal<String>,
base: Option<Oco<'static, str>>,
view_fn: Arc<Mutex<OutletViewFn>>,
owner: Arc<Mutex<Option<Owner>>>,
child: ChildRoute,
}
@@ -495,6 +500,7 @@ impl Debug for RouteContext {
.field("trigger", &self.trigger)
.field("url", &self.url)
.field("params", &self.params)
.field("owner", &self.owner.debug_id())
.field("matched", &self.matched)
.field("base", &self.base)
.finish_non_exhaustive()
@@ -508,10 +514,10 @@ impl Clone for RouteContext {
id: self.id,
trigger: self.trigger.clone(),
params: self.params.clone(),
owner: self.owner.clone(),
matched: self.matched.clone(),
base: self.base.clone(),
view_fn: Arc::clone(&self.view_fn),
owner: Arc::clone(&self.owner),
child: self.child.clone(),
}
}
@@ -524,6 +530,7 @@ trait AddNestedRoute {
base: Option<Oco<'static, str>>,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
);
#[allow(clippy::too_many_arguments)]
@@ -533,8 +540,9 @@ trait AddNestedRoute {
base: Option<Oco<'static, str>>,
items: &mut usize,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<Option<Owner>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
level: u8,
) -> u8;
@@ -550,9 +558,15 @@ where
base: Option<Oco<'static, str>>,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
) {
let orig_url = url;
// each Outlet gets its own owner, so it can inherit context from its parent route,
// a new owner will be constructed if a different route replaces this one in the outlet,
// so that any signals it creates or context it provides will be cleaned up
let owner = parent.child();
// the params signal can be updated to allow the same outlet to update to changes in the
// params, even if there's not a route match change
let params = ArcRwSignal::new(self.to_params().into_iter().collect());
@@ -610,13 +624,13 @@ where
url,
trigger: trigger.clone(),
params,
owner: owner.clone(),
matched,
view_fn: Arc::new(Mutex::new(Box::new(|_owner| {
Suspend::new(Box::pin(async { ().into_any() }))
}))),
base: base.clone(),
child: ChildRoute(Arc::new(Mutex::new(None))),
owner: Arc::new(Mutex::new(None)),
};
if !outlets.is_empty() {
let prev_index = outlets.len().saturating_sub(1);
@@ -632,7 +646,6 @@ where
let url = outlet.url.clone();
let matched = Matched(matched_including_parents);
let view_fn = Arc::clone(&outlet.view_fn);
let route_owner = Arc::clone(&outlet.owner);
let outlet = outlet.clone();
let params = params_including_parents.clone();
let url = url.clone();
@@ -642,8 +655,6 @@ where
let child = outlet.child.clone();
*view_fn.lock().or_poisoned() =
Box::new(move |owner_where_used| {
*route_owner.lock().or_poisoned() =
Some(owner_where_used.clone());
let view = view.clone();
let child = child.clone();
let params = params.clone();
@@ -685,7 +696,7 @@ where
// this is important because to build the view, we need access to the outlet
// and the outlet will be returned from building this child
if let Some(child) = child {
child.build_nested_route(orig_url, base, loaders, outlets);
child.build_nested_route(orig_url, base, loaders, outlets, &owner);
}
}
@@ -696,8 +707,9 @@ where
base: Option<Oco<'static, str>>,
items: &mut usize,
preloaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<Option<Owner>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
level: u8,
) -> u8 {
@@ -706,17 +718,11 @@ where
.take(*items)
.map(|route| (route.params.clone(), route.matched.clone()))
.unzip();
if outlets.get(*items).is_some() && *items > 0 {
*outlets[*items - 1].child.0.lock().or_poisoned() =
Some(outlets[*items].clone());
}
let current = outlets.get_mut(*items);
match current {
// if there's nothing currently in the routes at this point, build from here
None => {
self.build_nested_route(url, base, preloaders, outlets);
self.build_nested_route(url, base, preloaders, outlets, parent);
level
}
Some(current) => {
@@ -783,6 +789,9 @@ where
// assign a new owner, so that contexts and signals owned by the previous route
// in this outlet can be dropped
let mut old_owner =
Some(mem::replace(&mut current.owner, parent.child()));
let owner = current.owner.clone();
let (full_tx, full_rx) = oneshot::channel();
let full_tx = Mutex::new(Some(full_tx));
full_loaders.push(full_rx);
@@ -792,24 +801,22 @@ where
// and notify the trigger so that the reactive view inside the Outlet tracking
// the trigger runs again
preloaders.push(Box::pin(ScopedFuture::new({
let owner = owner.clone();
let trigger = current.trigger.clone();
let url = current.url.clone();
let matched = Matched(matched_including_parents);
let view_fn = Arc::clone(&current.view_fn);
let route_owner = Arc::clone(&current.owner);
let child = outlet.child.clone();
async move {
view.preload().await;
let child = child.clone();
*view_fn.lock().or_poisoned() =
Box::new(move |owner_where_used| {
let prev_owner = route_owner
.lock()
.or_poisoned()
.replace(owner_where_used.clone());
let owner = owner.clone();
let view = view.clone();
let full_tx =
full_tx.lock().or_poisoned().take();
let old_owner = old_owner.take();
let child = child.clone();
let params =
params_including_parents.clone();
@@ -834,13 +841,15 @@ where
})
}),
);
let view = view.await;
if let Some(old_owner) = old_owner {
old_owner.cleanup();
}
if let Some(tx) = full_tx {
_ = tx.send(prev_owner);
_ = tx.send(());
}
owner_where_used.with(|| {
owner.with(|| {
OwnedView::new(view).into_any()
})
}))
@@ -859,10 +868,9 @@ where
// if this children has matches, then rebuild the lower section of the tree
if let Some(child) = child {
child
.build_nested_route(url, base, preloaders, outlets);
} else {
*outlets[*items].child.0.lock().or_poisoned() = None;
child.build_nested_route(
url, base, preloaders, outlets, &owner,
);
}
return level;
@@ -874,6 +882,7 @@ where
current.params.set(new_params);
current.url.set(url.to_owned());
if let Some(child) = child {
let owner = current.owner.clone();
*items += 1;
child.rebuild_nested_route(
url,
@@ -882,11 +891,11 @@ where
preloaders,
full_loaders,
outlets,
&owner,
set_is_routing,
level + 1,
)
} else {
*current.child.0.lock().or_poisoned() = None;
level
}
}
@@ -924,13 +933,13 @@ fn top_level_outlet(outlets: &[RouteContext], outer_owner: &Owner) -> AnyView {
let child = outlet.child.clone();
let view_fn = outlet.view_fn.clone();
let trigger = outlet.trigger.clone();
let owner = outer_owner.child();
outer_owner.clone().with(|| {
provide_context(child.clone());
let outer_owner = outer_owner.clone();
(move || {
trigger.track();
let mut view_fn = view_fn.lock().or_poisoned();
view_fn(outer_owner.child())
view_fn(owner.clone())
})
.into_any()
})
@@ -944,13 +953,13 @@ where
{
let ChildRoute(child) = use_context()
.expect("<Outlet/> used without RouteContext being provided.");
let owner = Owner::new();
let child = child.lock().or_poisoned().clone();
let outer_owner = Owner::current().unwrap();
child.map(|child| {
move || {
child.trigger.track();
let mut view_fn = child.view_fn.lock().or_poisoned();
view_fn(outer_owner.child())
view_fn(owner.clone())
}
})
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.8.3"
version = "0.8.2"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -13,10 +13,10 @@ edition.workspace = true
proc-macro = true
[dependencies]
proc-macro-error2 = { default-features = false, workspace = true }
proc-macro-error2 = { default-features = false , workspace = true }
proc-macro2 = { workspace = true, default-features = true }
quote = { workspace = true, default-features = true }
syn = { features = ["full"], workspace = true, default-features = true }
syn = { features = ["full"] , workspace = true, default-features = true }
[dev-dependencies]
leptos_router = { path = "../router" }

View File

@@ -1,6 +1,6 @@
[package]
name = "tachys"
version = "0.2.4"
version = "0.2.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -12,7 +12,6 @@ use crate::{
Attribute,
},
hydration::Cursor,
renderer::Rndr,
ssr::StreamBuilder,
};
use futures::future::{join, join_all};
@@ -91,7 +90,6 @@ pub struct AnyViewState {
),
insert_before_this: fn(&ErasedLocal, child: &mut dyn Mountable) -> bool,
elements: fn(&ErasedLocal) -> Vec<crate::renderer::types::Element>,
placeholder: Option<crate::renderer::types::Placeholder>,
}
impl Debug for AnyViewState {
@@ -216,9 +214,6 @@ where
mark_branches,
extra_attrs,
);
if !T::EXISTS {
buf.push_str("<!>");
}
}
#[cfg(feature = "ssr")]
@@ -237,9 +232,6 @@ where
mark_branches,
extra_attrs,
);
if !T::EXISTS {
buf.push_sync("<!>");
}
}
#[cfg(feature = "ssr")]
@@ -258,14 +250,10 @@ where
mark_branches,
extra_attrs,
);
if !T::EXISTS {
buf.push_sync("<!>");
}
}
fn build<T: RenderHtml + 'static>(value: Erased) -> AnyViewState {
let state = ErasedLocal::new(value.into_inner::<T>().build());
let placeholder = (!T::EXISTS).then(Rndr::create_placeholder);
AnyViewState {
type_id: TypeId::of::<T>(),
state,
@@ -273,7 +261,6 @@ where
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
elements: elements::<T>,
placeholder,
}
}
@@ -286,8 +273,6 @@ where
let state = ErasedLocal::new(
value.into_inner::<T>().hydrate::<true>(cursor, position),
);
let placeholder =
(!T::EXISTS).then(|| cursor.next_placeholder(position));
AnyViewState {
type_id: TypeId::of::<T>(),
state,
@@ -295,7 +280,6 @@ where
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
elements: elements::<T>,
placeholder,
}
}
@@ -343,12 +327,7 @@ impl Render for AnyView {
(self.rebuild)(self.value, state)
} else {
let mut new = self.build();
if let Some(placeholder) = &mut state.placeholder {
placeholder.insert_before_this(&mut new);
placeholder.unmount();
} else {
state.insert_before_this(&mut new);
}
state.insert_before_this(&mut new);
state.unmount();
*state = new;
}
@@ -575,10 +554,7 @@ impl RenderHtml for AnyView {
impl Mountable for AnyViewState {
fn unmount(&mut self) {
(self.unmount)(&mut self.state);
if let Some(placeholder) = &mut self.placeholder {
placeholder.unmount();
}
(self.unmount)(&mut self.state)
}
fn mount(
@@ -586,23 +562,11 @@ impl Mountable for AnyViewState {
parent: &crate::renderer::types::Element,
marker: Option<&crate::renderer::types::Node>,
) {
(self.mount)(&mut self.state, parent, marker);
if let Some(placeholder) = &mut self.placeholder {
placeholder.mount(parent, marker);
}
(self.mount)(&mut self.state, parent, marker)
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
let before_view = (self.insert_before_this)(&self.state, child);
if before_view {
return true;
}
if let Some(placeholder) = &self.placeholder {
placeholder.insert_before_this(child)
} else {
false
}
(self.insert_before_this)(&self.state, child)
}
fn elements(&self) -> Vec<crate::renderer::types::Element> {

View File

@@ -134,7 +134,6 @@ where
type Owned = (A::Owned,);
const MIN_LENGTH: usize = A::MIN_LENGTH;
const EXISTS: bool = A::EXISTS;
fn html_len(&self) -> usize {
self.0.html_len()
@@ -240,6 +239,7 @@ macro_rules! impl_view_for_tuples {
{
type State = ($first::State, $($ty::State,)*);
fn build(self) -> Self::State {
#[allow(non_snake_case)]
let ($first, $($ty,)*) = self;
@@ -267,7 +267,7 @@ macro_rules! impl_view_for_tuples {
{
type AsyncOutput = ($first::AsyncOutput, $($ty::AsyncOutput,)*);
type Owned = ($first::Owned, $($ty::Owned,)*);
const EXISTS: bool = $first::EXISTS || $($ty::EXISTS || )* false;
const MIN_LENGTH: usize = $first::MIN_LENGTH $(+ $ty::MIN_LENGTH)*;
#[inline(always)]