Compare commits

..

29 Commits
3872 ... v0.8.3

Author SHA1 Message Date
Greg Johnston
91fb315fe0 v0.8.3 2025-07-12 20:10:21 -04:00
mskorkowski
6954b77b62 fix: generics on stores (closes #4136) (#4142)
Fixes the case when struct had a generic arguments by adding missing generic arguments into the generated trait and the said trait implementation.
2025-07-12 20:04:48 -04:00
Saber Haj Rabiee
77176f8395 fix(examples): remove redundant cf-worker example (#4140)
Cloudflare has an official template for leptos https://github.com/cloudflare/workers-rs/blob/main/templates/leptos
2025-07-11 10:35:14 -04:00
Greg Johnston
344b79a01b chore: fix cargo-leptos command in README (closes #4134) 2025-07-06 08:51:20 -04:00
Greg Johnston
051059c761 Merge pull request #4115 from leptos-rs/4114-fix
Clean up nested routing ownership and add regression tests
2025-07-01 08:32:52 -04:00
Ryo Hirayama
3c540dd858 Add an example to show server_fn is capable to serve on Cloudflare Workers (#4052)
* Add reqwest-no-ws feature to server_fn

* Add dep:tokio to server_fn/reqwest-no-ws

* Fix

* Refactor reqwest-no-ws feature in server_fn crate for wasm32 support

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Ad cf-worker example

* Fix error messages for trybuild

* Revert "Fix error messages for trybuild"

This reverts commit 42658dd031.

* Fix CI error by disabling on reqwest-no-ws aslike other feature

* Compact deps and add ci

* Revert all server_fn changes as main

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-30 23:44:12 -07:00
Greg Johnston
4125688a0a fix: don't create an extra intermediate outlet (messes with context) 2025-06-30 16:55:19 -04:00
Greg Johnston
bd3b962cfb fix: dispose of all previous owners simultaneously when all routes are complete 2025-06-30 09:51:20 -04:00
Greg Johnston
5dd3c217c4 fix: don't dispose of view owners immediately when outlets rerun 2025-06-30 09:51:02 -04:00
Greg Johnston
ae00e5ae13 test: add regression test for nested context on server 2025-06-30 09:49:16 -04:00
Greg Johnston
1ce671ba08 test: fix signal disposal test 2025-06-30 09:46:22 -04:00
Greg Johnston
ec9f26bd9f chore: remove unused variable 2025-06-30 09:06:18 -04:00
Greg Johnston
831eae31bc fix: much better solution for nested route disposal 2025-06-30 09:05:22 -04:00
Greg Johnston
ff6ae5de25 test: add regression test for signal disposal issue 2025-06-30 08:49:25 -04:00
Greg Johnston
c21712ba04 chore: simplify element_by_id (see #4121) 2025-06-29 17:16:51 -04:00
Greg Johnston
45771b6fd3 fix: correctly rebuild AnyView when the current view doesn't appear in the DOM (closes #4122) 2025-06-29 17:10:32 -04:00
Greg Johnston
f3557970a7 fix: uses EXISTS to mark things that don't exist in the DOM 2025-06-29 17:10:05 -04:00
Greg Johnston
c87ef331b0 fix: fix: correctly construct child links during rebuild 2025-06-29 14:07:48 -04:00
martin frances
e767518142 chore: updated clippy rule affecting stores example (#4120)
status.done().then_some("line-through").unwrap_or_default()
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `if status.done() { "line-through" } else { Default::default() }`
2025-06-28 14:53:27 -04:00
Greg Johnston
f94b681118 fix: correctly clear child route data 2025-06-28 14:31:57 -04:00
Greg Johnston
9c50e49253 test: add regression test for #4088 2025-06-28 14:15:49 -04:00
Greg Johnston
57c7097ede fix: disable InertElement when global class is provided (closes #4116) (#4119) 2025-06-28 13:53:58 -04:00
dependabot[bot]
1a06e0eee8 chore(deps): bump the rust-dependencies group across 1 directory with 18 updates (#4110)
---
updated-dependencies:
- dependency-name: syn
  dependency-version: 2.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glib
  dependency-version: 0.20.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: prettyplease
  dependency-version: 0.2.35
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: autocfg
  dependency-version: 1.5.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: derive-where
  dependency-version: 1.5.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: errno
  dependency-version: 0.3.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glib-macros
  dependency-version: 0.20.12
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-version: 0.2.174
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: munge
  dependency-version: 0.4.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: munge_macro
  dependency-version: 0.4.5
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: quinn-udp
  dependency-version: 0.5.13
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: r-efi
  dependency-version: 5.3.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rustls
  dependency-version: 0.23.28
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: slab
  dependency-version: 0.4.10
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tracing-attributes
  dependency-version: 0.1.30
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: webpki-roots
  dependency-version: 1.0.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zerocopy
  dependency-version: 0.8.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zerocopy-derive
  dependency-version: 0.8.26
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-28 13:53:18 -04:00
bicarlsen
ce9af4a685 fix: use HTML-namespaced InertElement for top-level <svg> elements. (#4109) 2025-06-28 13:53:02 -04:00
martin frances
e0c79eb8d8 chore: bump syn and tokio-tungsenite. (#4117) 2025-06-28 13:52:20 -04:00
Greg Johnston
9fd972971e test: add regression test for back/forward behavior mentioned in #4114 2025-06-27 18:50:28 -04:00
Greg Johnston
9473220639 test: add regression test for #4015 2025-06-27 18:42:30 -04:00
Greg Johnston
ae11812dc6 fix: ensure cleanups run when navigating between sibling Routes in Outlet 2025-06-27 17:59:09 -04:00
Greg Johnston
4c55c25445 chore: clean up unused owner manipulation 2025-06-27 17:59:09 -04:00
32 changed files with 638 additions and 282 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.2"
version = "0.8.3"
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.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" }
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" }
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.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" }
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" }
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.101" }
syn = { default-features = false, version = "2.0.104" }
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.26.2" }
tokio-tungstenite = { default-features = false, version = "0.27.0" }
serial_test = { default-features = false, version = "3.2.0" }
erased = { default-features = false, version = "0.1.2" }
glib = { default-features = false, version = "0.20.10" }
glib = { default-features = false, version = "0.20.12" }
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.33" }
prettyplease = { default-features = false, version = "0.2.35" }
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
cargo leptos new --git https://github.com/leptos-rs/start-axum
cd [your project name]
cargo leptos watch
```

View File

@@ -0,0 +1,20 @@
@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

@@ -0,0 +1,8 @@
@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,3 +24,25 @@ 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,3 +11,10 @@ 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,9 +2,7 @@ use anyhow::{Ok, Result};
use fantoccini::{elements::Element, Client, Locator};
pub async fn text_at_id(client: &Client, id: &str) -> Result<String> {
let element = client
.wait()
.for_element(Locator::Id(id))
let element = element_by_id(client, id)
.await
.expect(format!("no such element with id `{}`", id).as_str());
let text = element.text().await?;
@@ -19,3 +17,7 @@ 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,9 +3,7 @@ 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(())
@@ -20,3 +18,10 @@ 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::pr_4091::Routes4091;
use crate::{issue_4088::Routes4088, pr_4015::Routes4015, pr_4091::Routes4091};
use leptos::prelude::*;
use leptos_meta::{MetaTags, *};
use leptos_router::{
@@ -35,6 +35,8 @@ pub fn App() -> impl IntoView {
<Routes fallback>
<Route path=path!("") view=HomePage/>
<Routes4091/>
<Routes4015/>
<Routes4088/>
</Routes>
</main>
</Router>
@@ -55,6 +57,8 @@ 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

@@ -0,0 +1,119 @@
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,4 +1,6 @@
pub mod app;
mod issue_4088;
mod pr_4015;
mod pr_4091;
#[cfg(feature = "hydrate")]

View File

@@ -0,0 +1,29 @@
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,8 +28,9 @@ fn Container() -> impl IntoView {
provide_context(rw_signal);
view! {
<nav>
<nav id="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 || {
status.done().then_some("line-through").unwrap_or_default()
if status.done() { "line-through" } else { Default::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.2"
version = "0.8.3"
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,14 +24,17 @@ 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,39 +111,28 @@ where
let on_submit = {
move |ev: SubmitEvent| {
// 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;
}
if ev.default_prevented() {
return;
}
ev.prevent_default();
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);
}
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,6 +90,7 @@ 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,6 +44,8 @@ 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();
@@ -401,6 +403,9 @@ 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,
@@ -704,7 +709,7 @@ fn node_to_tokens(
&& el_name != "textarea";
let el_name = el_node.name().to_string();
if is_svg_element(&el_name) {
if is_svg_element(&el_name) && el_name != "svg" {
Some(inert_svg_element_to_tokens(
node,
escape,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.8.2"
version = "0.8.3"
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,6 +413,7 @@ 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,6 +322,7 @@ 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.2"
version = "0.2.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -16,19 +16,26 @@ 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.2"
version = "0.2.3"
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,7 +21,10 @@ 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.2"
version = "0.2.3"
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,10 +111,8 @@ 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 generics_with_orig = {
let params = &generics.params;
quote! { <#any_store_field, #params> }
};
let params = &generics.params;
let generics_with_orig = quote! { <#any_store_field, #params> };
let where_with_orig = {
generics
.where_clause
@@ -140,13 +138,13 @@ impl ToTokens for Model {
// read access
tokens.extend(quote! {
#vis trait #trait_name <AnyStoreField>
#vis trait #trait_name <AnyStoreField, #params>
#where_with_orig
{
#(#trait_fields)*
}
impl #generics_with_orig #trait_name <AnyStoreField> for AnyStoreField
impl #generics_with_orig #trait_name <AnyStoreField, #params> for AnyStoreField
#where_with_orig
{
#(#read_fields)*

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.8.2"
version = "0.8.3"
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,7 +109,6 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
drop(url);
@@ -180,7 +179,6 @@ where
&mut preloaders,
&mut full_loaders,
&mut state.outlets,
&self.outer_owner,
self.set_is_routing.is_some(),
0,
);
@@ -340,7 +338,6 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
// outlets will not send their views if the loaders are never polled
@@ -394,7 +391,6 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
// outlets will not send their views if the loaders are never polled
@@ -448,7 +444,6 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
drop(url);
@@ -483,10 +478,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,
}
@@ -500,7 +495,6 @@ 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()
@@ -514,10 +508,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(),
}
}
@@ -530,7 +524,6 @@ 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)]
@@ -540,9 +533,8 @@ 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<()>>,
full_loaders: &mut Vec<oneshot::Receiver<Option<Owner>>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
level: u8,
) -> u8;
@@ -558,15 +550,9 @@ 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());
@@ -624,13 +610,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);
@@ -646,6 +632,7 @@ 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();
@@ -655,6 +642,8 @@ 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();
@@ -696,7 +685,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, &owner);
child.build_nested_route(orig_url, base, loaders, outlets);
}
}
@@ -707,9 +696,8 @@ where
base: Option<Oco<'static, str>>,
items: &mut usize,
preloaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
full_loaders: &mut Vec<oneshot::Receiver<Option<Owner>>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
level: u8,
) -> u8 {
@@ -718,11 +706,17 @@ 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, parent);
self.build_nested_route(url, base, preloaders, outlets);
level
}
Some(current) => {
@@ -789,9 +783,6 @@ 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);
@@ -801,22 +792,24 @@ 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 owner = owner.clone();
let prev_owner = route_owner
.lock()
.or_poisoned()
.replace(owner_where_used.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();
@@ -841,15 +834,13 @@ where
})
}),
);
let view = view.await;
if let Some(old_owner) = old_owner {
old_owner.cleanup();
}
if let Some(tx) = full_tx {
_ = tx.send(());
_ = tx.send(prev_owner);
}
owner.with(|| {
owner_where_used.with(|| {
OwnedView::new(view).into_any()
})
}))
@@ -868,9 +859,10 @@ 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, &owner,
);
child
.build_nested_route(url, base, preloaders, outlets);
} else {
*outlets[*items].child.0.lock().or_poisoned() = None;
}
return level;
@@ -882,7 +874,6 @@ 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,
@@ -891,11 +882,11 @@ where
preloaders,
full_loaders,
outlets,
&owner,
set_is_routing,
level + 1,
)
} else {
*current.child.0.lock().or_poisoned() = None;
level
}
}
@@ -933,13 +924,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(owner.clone())
view_fn(outer_owner.child())
})
.into_any()
})
@@ -953,13 +944,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(owner.clone())
view_fn(outer_owner.child())
}
})
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.8.2"
version = "0.8.3"
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.3"
version = "0.2.4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

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

View File

@@ -134,6 +134,7 @@ 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()
@@ -239,7 +240,6 @@ 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)]