Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
23529b2e0b fix: use unkeyed paths for all store patching (closes #4486) 2025-12-14 17:12:21 -05:00
52 changed files with 1019 additions and 1222 deletions

View File

@@ -103,7 +103,7 @@ jobs:
id: pnpm-cache
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
- uses: actions/cache@v4
if: contains(inputs.directory, 'examples')
name: Setup pnpm cache
with:

1043
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,45 +49,45 @@ 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.6" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
leptos = { path = "./leptos", version = "0.8.15" }
leptos = { path = "./leptos", version = "0.8.14" }
leptos_config = { path = "./leptos_config", version = "0.8.8" }
leptos_dom = { path = "./leptos_dom", version = "0.8.7" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.7" }
leptos_macro = { path = "./leptos_macro", version = "0.8.13" }
leptos_router = { path = "./router", version = "0.8.11" }
leptos_macro = { path = "./leptos_macro", version = "0.8.12" }
leptos_router = { path = "./router", version = "0.8.10" }
leptos_router_macro = { path = "./router_macro", version = "0.8.6" }
leptos_server = { path = "./leptos_server", version = "0.8.6" }
leptos_meta = { path = "./meta", version = "0.8.5" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.1" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.12" }
reactive_stores = { path = "./reactive_stores", version = "0.3.1" }
reactive_graph = { path = "./reactive_graph", version = "0.2.11" }
reactive_stores = { path = "./reactive_stores", version = "0.3.0" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" }
server_fn = { path = "./server_fn", version = "0.8.9" }
server_fn = { path = "./server_fn", version = "0.8.8" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.8" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" }
tachys = { path = "./tachys", version = "0.2.11" }
# members deps
async-once-cell = { default-features = false, version = "0.5.4" }
async-once-cell = { default-features = false, version = "0.5.3" }
itertools = { default-features = false, version = "0.14.0" }
convert_case = { default-features = false, version = "0.10.0" }
serde_json = { default-features = false, version = "1.0.148" }
trybuild = { default-features = false, version = "1.0.114" }
typed-builder = { default-features = false, version = "0.23.2" }
typed-builder-macro = { default-features = false, version = "0.23.2" }
convert_case = { default-features = false, version = "0.8.0" }
serde_json = { default-features = false, version = "1.0.143" }
trybuild = { default-features = false, version = "1.0.110" }
typed-builder = { default-features = false, version = "0.22.0" }
typed-builder-macro = { default-features = false, version = "0.22.0" }
thiserror = { default-features = false, version = "2.0.17" }
wasm-bindgen = { default-features = false, version = "0.2.106" }
indexmap = { default-features = false, version = "2.12.1" }
wasm-bindgen = { default-features = false, version = "0.2.100" }
indexmap = { default-features = false, version = "2.11.0" }
rstml = { default-features = false, version = "0.12.1" }
rustc_version = { default-features = false, version = "0.4.1" }
guardian = { default-features = false, version = "1.3.0" }
rustc-hash = { default-features = false, version = "2.1.1" }
actix-web = { default-features = false, version = "4.12.1" }
tracing = { default-features = false, version = "0.1.44" }
slotmap = { default-features = false, version = "1.1.1" }
actix-web = { default-features = false, version = "4.11.0" }
tracing = { default-features = false, version = "0.1.41" }
slotmap = { default-features = false, version = "1.0.7" }
futures = { default-features = false, version = "0.3.31" }
dashmap = { default-features = false, version = "6.1.0" }
pin-project-lite = { default-features = false, version = "0.2.16" }
@@ -97,41 +97,41 @@ html-escape = { default-features = false, version = "0.2.13" }
proc-macro-error2 = { default-features = false, version = "2.0.1" }
const_format = { default-features = false, version = "0.2.35" }
gloo-net = { default-features = false, version = "0.6.0" }
url = { default-features = false, version = "2.5.7" }
tokio = { default-features = false, version = "1.48.0" }
url = { default-features = false, version = "2.5.4" }
tokio = { default-features = false, version = "1.47.1" }
base64 = { default-features = false, version = "0.22.1" }
cfg-if = { default-features = false, version = "1.0.4" }
wasm-bindgen-futures = { default-features = false, version = "0.4.56" }
cfg-if = { default-features = false, version = "1.0.3" }
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
tower = { default-features = false, version = "0.5.2" }
proc-macro2 = { default-features = false, version = "1.0.96" }
proc-macro2 = { default-features = false, version = "1.0.101" }
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 = "1.0.0-rc.3" }
syn = { default-features = false, version = "2.0.104" }
parking_lot = { default-features = false, version = "0.12.5" }
axum = { default-features = false, version = "0.8.6" }
serde_qs = { default-features = false, version = "0.15.0" }
syn = { default-features = false, version = "2.0.106" }
xxhash-rust = { default-features = false, version = "0.8.15" }
paste = { default-features = false, version = "1.0.15" }
quote = { default-features = false, version = "1.0.42" }
web-sys = { default-features = false, version = "0.3.83" }
js-sys = { default-features = false, version = "0.3.83" }
rand = { default-features = false, version = "0.9.2" }
serde-lite = { default-features = false, version = "0.5.1" }
quote = { default-features = false, version = "1.0.41" }
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.28.0" }
serial_test = { default-features = false, version = "3.2.0" }
erased = { default-features = false, version = "0.1.2" }
glib = { default-features = false, version = "0.21.5" }
glib = { default-features = false, version = "0.20.12" }
async-trait = { default-features = false, version = "0.1.89" }
linear-map = { default-features = false, version = "1.2.0" }
anyhow = { default-features = false, version = "1.0.100" }
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.8" }
tower-http = { default-features = false, version = "0.6.4" }
prettyplease = { default-features = false, version = "0.2.37" }
inventory = { default-features = false, version = "0.3.21" }
config = { default-features = false, version = "0.15.19" }
camino = { default-features = false, version = "1.2.2" }
config = { default-features = false, version = "0.15.14" }
camino = { default-features = false, version = "1.2.1" }
ciborium = { default-features = false, version = "0.2.2" }
bitcode = { default-features = false, version = "0.6.9" }
bitcode = { default-features = false, version = "0.6.6" }
multer = { default-features = false, version = "3.1.0" }
leptos-spin-macro = { default-features = false, version = "0.2.0" }
sledgehammer_utils = { default-features = false, version = "0.3.1" }
@@ -139,38 +139,38 @@ sledgehammer_bindgen = { default-features = false, version = "0.6.0" }
wasm-streams = { default-features = false, version = "0.4.2" }
rkyv = { default-features = false, version = "0.8.12" }
temp-env = { default-features = false, version = "0.3.6" }
uuid = { default-features = false, version = "1.19.0" }
bytes = { default-features = false, version = "1.11.0" }
http = { default-features = false, version = "1.4.0" }
regex = { default-features = false, version = "1.12.2" }
uuid = { default-features = false, version = "1.18.0" }
bytes = { default-features = false, version = "1.10.1" }
http = { default-features = false, version = "1.3.1" }
regex = { default-features = false, version = "1.11.3" }
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
tempfile = { default-features = false, version = "3.24.0" }
tempfile = { default-features = false, version = "3.23.0" }
futures-lite = { default-features = false, version = "2.6.1" }
log = { default-features = false, version = "0.4.29" }
log = { default-features = false, version = "0.4.27" }
percent-encoding = { default-features = false, version = "2.3.2" }
async-executor = { default-features = false, version = "1.13.3" }
const-str = { default-features = false, version = "0.7.1" }
async-executor = { default-features = false, version = "1.13.2" }
const-str = { default-features = false, version = "0.6.4" }
http-body-util = { default-features = false, version = "0.1.3" }
hyper = { default-features = false, version = "1.8.1" }
hyper = { default-features = false, version = "1.7.0" }
postcard = { default-features = false, version = "1.1.3" }
rmp-serde = { default-features = false, version = "1.3.1" }
reqwest = { default-features = false, version = "0.12.28" }
rmp-serde = { default-features = false, version = "1.3.0" }
reqwest = { default-features = false, version = "0.12.23" }
tower-layer = { default-features = false, version = "0.3.3" }
attribute-derive = { default-features = false, version = "0.10.5" }
insta = { default-features = false, version = "1.45.1" }
codee = { default-features = false, version = "0.3.5" }
insta = { default-features = false, version = "1.43.1" }
codee = { default-features = false, version = "0.3.0" }
actix-http = { default-features = false, version = "3.11.2" }
wasm-bindgen-test = { default-features = false, version = "0.3.56" }
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
rustversion = { default-features = false, version = "1.0.22" }
getrandom = { default-features = false, version = "0.3.4" }
actix-files = { default-features = false, version = "0.6.9" }
async-lock = { default-features = false, version = "3.4.2" }
getrandom = { default-features = false, version = "0.3.3" }
actix-files = { default-features = false, version = "0.6.6" }
async-lock = { default-features = false, version = "3.4.1" }
base16 = { default-features = false, version = "0.2.1" }
digest = { default-features = false, version = "0.10.7" }
sha2 = { default-features = false, version = "0.10.9" }
subsecond = { default-features = false, version = "0.7.2" }
dioxus-cli-config = { default-features = false, version = "0.7.2" }
dioxus-devtools = { default-features = false, version = "0.7.2" }
sha2 = { default-features = false, version = "0.10.8" }
subsecond = { default-features = false, version = "0.7.0-rc.0" }
dioxus-cli-config = { default-features = false, version = "0.7.0-rc.0" }
dioxus-devtools = { default-features = false, version = "0.7.0-rc.0" }
wasm_split_helpers = { default-features = false, version = "0.2.0" }
[profile.release]

View File

@@ -74,8 +74,8 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
## What does that mean?
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/prelude/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/suspense/fn.Suspense.html) components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/server_fn/latest/server_fn/), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)

View File

@@ -9,12 +9,12 @@ This document is intended as a running list of common issues, with example code
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
```rust
let (a, set_a) = signal(0);
let (b, set_b) = signal(false);
let (a, set_a) = create_signal(0);
let (b, set_b) = create_signal(false);
Effect::new(move |_| {
if a.get() > 5 {
set_b.set(true);
create_effect(move |_| {
if a() > 5 {
set_b(true);
}
});
```
@@ -24,10 +24,56 @@ This creates an inefficient chain of updates, and can easily lead to infinite lo
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
```rust
let (a, set_a) = signal(0);
let b = move || a.get() > 5;
let (a, set_a) = create_signal(0);
let b = move || a () > 5;
```
### Nested signal updates/reads triggering panic
Sometimes you have nested signals: for example, hash-map that can change over time, each of whose values can also change over time:
```rust
#[component]
pub fn App() -> impl IntoView {
let resources = create_rw_signal(HashMap::new());
let update = move |id: usize| {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
};
view! {
<div>
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
<button on:click=move |_| update(1)>"+"</button>
</div>
}
}
```
Clicking the button twice will cause a panic, because of the nested signal _read_. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
You can fix this fairly easily by using the [`batch()`](https://docs.rs/leptos/latest/leptos/fn.batch.html) method:
```rust
let update = move |id: usize| {
batch(move || {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
});
};
```
This delays running any effects until after both updates are made, preventing the conflict entirely without requiring any other restructuring.
## Templates and the DOM
### `<input value=...>` doesn't update or stops updating
@@ -37,8 +83,8 @@ Many DOM attributes can be updated either by setting an attribute on the DOM nod
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
```rust
let (a, set_a) = signal("Starting value".to_string());
let on_input = move |ev| set_a.set(event_target_value(&ev));
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
view! {
@@ -51,8 +97,8 @@ view! {
```
```rust
let (a, set_a) = signal("Starting value".to_string());
let on_input = move |ev| set_a.set(event_target_value(&ev));
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
view! {

View File

@@ -670,7 +670,7 @@ fn CodeDemoWasm(mode: WasmDemo) -> impl IntoView {
leptos::logging::log!("wasm csr_listener listener added");
// Dispatch the event when this view is finally mounted onto the DOM.
_ = request_animation_frame(move || {
request_animation_frame(move || {
let event = web_sys::Event::new("hljs_hook")
.expect("error creating hljs_hook event");
document.dispatch_event(&event)
@@ -689,7 +689,7 @@ fn CodeDemoWasm(mode: WasmDemo) -> impl IntoView {
<Suspense fallback=move || view! { <p>"Loading code example..."</p> }>{
move || Suspend::new(async move {
Effect::new(move |_| {
_ = request_animation_frame(move || {
request_animation_frame(move || {
leptos::logging::log!("request_animation_frame invoking hljs::highlight_all");
// under SSR this is an noop, but it wouldn't be called under there anyway because
// it isn't the isomorphic version, i.e. Effect::new_isomorphic(...).
@@ -815,7 +815,7 @@ fn WasmBindgenJSHookReadyEvent() -> impl IntoView {
leptos::logging::log!("wasm csr_listener listener added");
// Dispatch the event when this view is finally mounted onto the DOM.
_ = request_animation_frame(move || {
request_animation_frame(move || {
let event = web_sys::Event::new("hljs_hook")
.expect("error creating hljs_hook event");
document.dispatch_event(&event)
@@ -866,7 +866,7 @@ fn WasmBindgenEffect() -> impl IntoView {
let example = r#"<Suspense fallback=move || view! { <p>"Loading code example..."</p> }>{
move || Suspend::new(async move {
Effect::new(move |_| {
_ = request_animation_frame(move || {
request_animation_frame(move || {
leptos::logging::log!("request_animation_frame invoking hljs::highlight_all");
// under SSR this is an noop.
crate::hljs::highlight_all();

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
axum = { version = "0.8.1", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
leptos = { path = "../../leptos", features = ["tracing", "lazy"] }
leptos = { path = "../../leptos", features = ["tracing"] }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }

View File

@@ -17,7 +17,7 @@ leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.12"
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
wasm-bindgen = "0.2.106"
wasm-bindgen = "0.2.92"
[features]
hydrate = [

View File

@@ -1,38 +0,0 @@
@check_issue_4492
Feature: Regression test for issue #4492
Scenario: Scenario A should show Loading once on first load.
Given I see the app
And I can access regression test 4492
When I click the button a-toggle
Then I see a-result has the text Loading...
When I wait 100ms
Then I see a-result has the text 0
When I click the button a-button
Then I see a-result has the text 0
When I wait 100ms
Then I see a-result has the text 1
Scenario: Scenario B should never show Loading
Given I see the app
And I can access regression test 4492
When I click the button b-toggle
Then I see b-result has the text 0
When I click the button b-button
Then I see b-result has the text 0
When I wait 100ms
Then I see b-result has the text 1
When I click the button b-button
Then I see b-result has the text 1
When I wait 100ms
Then I see b-result has the text 2
Scenario: Scenario C should never show Loading
Given I see the app
And I can access regression test 4492
When I click the button c-toggle
Then I see c-result has the text 0
When I click the button c-button
Then I see c-result has the text 42
When I wait 100ms
Then I see c-result has the text 1

View File

@@ -15,9 +15,3 @@ pub async fn click_link(client: &Client, text: &str) -> Result<()> {
link.click().await?;
Ok(())
}
pub async fn click_button(client: &Client, id: &str) -> Result<()> {
let btn = find::element_by_id(&client, &id).await?;
btn.click().await?;
Ok(())
}

View File

@@ -7,15 +7,7 @@ pub async fn result_text_is(
client: &Client,
expected_text: &str,
) -> Result<()> {
element_text_is(client, "result", expected_text).await
}
pub async fn element_text_is(
client: &Client,
id: &str,
expected_text: &str,
) -> Result<()> {
let actual = find::text_at_id(client, id).await?;
let actual = find::text_at_id(client, "result").await?;
assert_eq!(&actual, expected_text);
Ok(())
}

View File

@@ -20,14 +20,6 @@ async fn i_select_the_link(world: &mut AppWorld, text: String) -> Result<()> {
Ok(())
}
#[when(regex = "^I click the button (.*)$")]
async fn i_click_the_button(world: &mut AppWorld, id: String) -> Result<()> {
let client = &world.client;
action::click_button(client, &id).await?;
Ok(())
}
#[given(expr = "I select the following links")]
#[when(expr = "I select the following links")]
async fn i_select_the_following_links(
@@ -62,10 +54,3 @@ async fn i_go_back(world: &mut AppWorld) -> Result<()> {
Ok(())
}
#[when(regex = r"^I wait (\d+)ms$")]
async fn i_wait_ms(_world: &mut AppWorld, ms: u64) -> Result<()> {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
Ok(())
}

View File

@@ -19,17 +19,6 @@ async fn i_see_the_result_is_the_string(
Ok(())
}
#[then(regex = r"^I see ([\w-]+) has the text (.*)$")]
async fn i_see_element_has_text(
world: &mut AppWorld,
id: String,
text: String,
) -> Result<()> {
let client = &world.client;
check::element_text_is(client, &id, &text).await?;
Ok(())
}
#[then(regex = r"^I see the navbar$")]
async fn i_see_the_navbar(world: &mut AppWorld) -> Result<()> {
let client = &world.client;

View File

@@ -1,7 +1,7 @@
use crate::{
issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217,
issue_4285::Routes4285, issue_4296::Routes4296, issue_4324::Routes4324,
issue_4492::Routes4492, pr_4015::Routes4015, pr_4091::Routes4091,
pr_4015::Routes4015, pr_4091::Routes4091,
};
use leptos::prelude::*;
use leptos_meta::{MetaTags, *};
@@ -48,7 +48,6 @@ pub fn App() -> impl IntoView {
<Routes4285/>
<Routes4296/>
<Routes4324/>
<Routes4492/>
</Routes>
</main>
</Router>
@@ -76,7 +75,6 @@ fn HomePage() -> impl IntoView {
<li><a href="/4285/">"4285"</a></li>
<li><a href="/4296/">"4296"</a></li>
<li><a href="/4324/">"4324"</a></li>
<li><a href="/4492/">"4492"</a></li>
</ul>
</nav>
}

View File

@@ -1,114 +0,0 @@
use leptos::prelude::*;
#[allow(unused_imports)]
use leptos_router::{
components::Route, path, MatchNestedRoutes, NavigateOptions,
};
#[component]
pub fn Routes4492() -> impl MatchNestedRoutes + Clone {
view! {
<Route path=path!("4492") view=Issue4492/>
}
.into_inner()
}
#[component]
fn Issue4492() -> impl IntoView {
let show_a = RwSignal::new(false);
let show_b = RwSignal::new(false);
let show_c = RwSignal::new(false);
view! {
<button id="a-toggle" on:click=move |_| show_a.set(!show_a.get())>"Toggle A"</button>
<button id="b-toggle" on:click=move |_| show_b.set(!show_b.get())>"Toggle B"</button>
<button id="c-toggle" on:click=move |_| show_c.set(!show_c.get())>"Toggle C"</button>
<Show when=move || show_a.get()>
<ScenarioA/>
</Show>
<Show when=move || show_b.get()>
<ScenarioB/>
</Show>
<Show when=move || show_c.get()>
<ScenarioC/>
</Show>
}
}
#[component]
fn ScenarioA() -> impl IntoView {
// scenario A: one truly-async resource is read on click
let counter = RwSignal::new(0);
let resource = Resource::new(
move || counter.get(),
|count| async move {
sleep(50).await.unwrap();
count
},
);
view! {
<Transition fallback=|| view! { <p id="a-result">"Loading..."</p> }>
<p id="a-result">{resource}</p>
</Transition>
<button id="a-button" on:click=move |_| *counter.write() += 1>"+1"</button>
}
}
#[component]
fn ScenarioB() -> impl IntoView {
// scenario B: resource immediately available first time, then after 250ms
let counter = RwSignal::new(0);
let resource = Resource::new(
move || counter.get(),
|count| async move {
if count == 0 {
count
} else {
sleep(50).await.unwrap();
count
}
},
);
view! {
<Transition fallback=|| view! { <p id="b-result">"Loading..."</p> }>
<p id="b-result">{resource}</p>
</Transition>
<button id="b-button" on:click=move |_| *counter.write() += 1>"+1"</button>
}
}
#[component]
fn ScenarioC() -> impl IntoView {
// scenario C: not even a resource on the first run, just a value
// see https://github.com/leptos-rs/leptos/issues/3868
let counter = RwSignal::new(0);
let s_res = StoredValue::new(None::<ArcLocalResource<i32>>);
let resource = move || {
let count = counter.get();
if count == 0 {
count
} else {
let r = s_res.get_value().unwrap_or_else(|| {
let res = ArcLocalResource::new(move || async move {
sleep(50).await.unwrap();
count
});
s_res.set_value(Some(res.clone()));
res
});
r.get().unwrap_or(42)
}
};
view! {
<Transition fallback=|| view! { <p id="c-result">"Loading..."</p> }>
<p id="c-result">{resource}</p>
</Transition>
<button id="c-button" on:click=move |_| *counter.write() += 1>"+1"</button>
}
}
#[server]
async fn sleep(ms: u64) -> Result<(), ServerFnError> {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
Ok(())
}

View File

@@ -5,7 +5,6 @@ mod issue_4217;
mod issue_4285;
mod issue_4296;
mod issue_4324;
mod issue_4492;
mod pr_4015;
mod pr_4091;

View File

@@ -97,7 +97,7 @@ fn delay(
duration: Duration,
) -> impl Future<Output = Result<(), Canceled>> + Send {
let (tx, rx) = oneshot::channel();
_ = set_timeout(
set_timeout(
move || {
_ = tx.send(());
},

View File

@@ -50,7 +50,7 @@ where
};
// here, we return the handle
set_interval(
set_interval_with_handle(
f.clone(),
// this is the only reactive access, so this effect will only
// re-run when the interval changes

View File

@@ -684,7 +684,7 @@ where
additional_context.clone(),
app_fn.clone(),
);
let asyn = render_app_async_with_context(
let asyn = render_app_async_stream_with_context(
additional_context.clone(),
app_fn.clone(),
);
@@ -1019,6 +1019,73 @@ where
render_app_async_with_context(|| {}, app_fn)
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
/// Path(id): Path<String>,
/// req: Request<Body>,
/// ) -> Response {
/// let handler = leptos_axum::render_app_async_with_context(
/// move || {
/// provide_context(id.clone());
/// },
/// || { /* your application here */ },
/// );
/// handler(req).await.into_response()
/// }
/// ```
/// Otherwise, this function is identical to [render_app_to_stream].
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
})
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
@@ -2005,7 +2072,19 @@ where
},
move || shell(options),
req,
async_stream_builder,
|app, chunks, _supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
},
)
.await;

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos"
version = "0.8.15"
version = "0.8.14"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -122,7 +122,6 @@ subsecond = [
"web-sys/WebSocket",
"web-sys/Window",
]
lazy = ["tachys/lazy"]
[dev-dependencies]
tokio = { features = [

View File

@@ -83,7 +83,7 @@ pub fn AnimatedShow(
} else {
cls.set(hide_class);
let h = leptos_dom::helpers::set_timeout(
let h = leptos_dom::helpers::set_timeout_with_handle(
move || show.set(false),
hide_delay,
)

View File

@@ -287,9 +287,7 @@ where
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
.unwrap_throw();
let data = data.to_string().as_string().unwrap_or_default();
serde_qs::Config::new()
.use_form_encoding(true)
.deserialize_str::<Self>(&data)
serde_qs::Config::new(5, false).deserialize_str::<Self>(&data)
}
}

View File

@@ -1,42 +1,25 @@
if (window.location.protocol === "https:") {
protocol = "wss://";
if (window.location.protocol === 'https:') {
protocol = 'wss://';
}
let host = window.location.hostname;
function connect() {
let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`);
ws.onmessage = (ev) => {
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute("href").includes(msg.css)) {
let newHref = "/" + msg.css + "?version=" + Date.now();
link.setAttribute("href", newHref);
found = true;
}
});
if (!found)
console.warn(
`CSS hot-reload: Could not find a <link href=/\"${msg.css}\"> element`,
);
}
if (msg.view) {
patch(msg.view);
}
let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`);
ws.onmessage = (ev) => {
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute('href').includes(msg.css)) {
let newHref = '/' + msg.css + '?version=' + Date.now();
link.setAttribute('href', newHref);
found = true;
}
});
if (!found) console.warn(`CSS hot-reload: Could not find a <link href=/\"${msg.css}\"> element`);
};
ws.onclose = () => {
console.warn("Live-reload disconnected. Reconnecting...");
setTimeout(connect, 1000);
};
ws.onerror = () => {
ws.close();
};
}
connect();
if(msg.view) {
patch(msg.view);
}
};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');

View File

@@ -15,10 +15,7 @@ use reactive_graph::{
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{
Dispose, Get, Read, ReadUntracked, Track, With, WithUntracked,
WriteValue,
},
traits::{Dispose, Get, Read, ReadUntracked, Track, With, WriteValue},
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::{Arc, Mutex};
@@ -122,19 +119,14 @@ where
provide_context(SuspenseContext {
tasks: tasks.clone(),
});
let none_pending = ArcMemo::new({
let tasks = tasks.clone();
move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
});
let has_tasks =
Arc::new(move || !tasks.with_untracked(SlotMap::is_empty));
OwnedView::new(SuspenseBoundary::<false, _, _> {
id,
@@ -142,7 +134,6 @@ where
fallback,
children,
error_boundary_parent,
has_tasks,
})
})
}
@@ -165,7 +156,6 @@ pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
pub fallback: Fal,
pub children: Chil,
pub error_boundary_parent: Option<ErrorBoundarySuspendedChildren>,
pub has_tasks: Arc<dyn Fn() -> bool + Send + Sync>,
}
impl<const TRANSITION: bool, Fal, Chil> Render
@@ -202,26 +192,12 @@ where
outer_owner.clone(),
);
let state = if let Some(mut state) = prev {
if let Some(mut state) = prev {
this.rebuild(&mut state);
state
} else {
this.build()
};
if nth_run == 1 && !(self.has_tasks)() {
// if this is the first run, and there are no pending resources at this point,
// it means that there were no actually-async resources read while rendering the children
// this means that we're effectively on the settled second run: none_pending
// won't change false => true and cause this to rerender (and therefore increment nth_run)
//
// we increment it manually here so that future resource changes won't cause the transition fallback
// to be displayed for the first time
// see https://github.com/leptos-rs/leptos/issues/3868, https://github.com/leptos-rs/leptos/issues/4492
nth_run += 1;
}
state
})
}
@@ -259,7 +235,6 @@ where
fallback,
children,
error_boundary_parent,
has_tasks,
} = self;
SuspenseBoundary {
id,
@@ -267,7 +242,6 @@ where
fallback,
children: children.add_any_attr(attr),
error_boundary_parent,
has_tasks,
}
}
}

View File

@@ -10,11 +10,10 @@ use reactive_graph::{
effect::Effect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Get, Set, Track, With, WithUntracked},
traits::{Get, Set, Track, With},
wrappers::write::SignalSetter,
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
use tachys::reactive_graph::OwnedView;
/// If any [`Resource`](crate::prelude::Resource) is read in the `children` of this
@@ -105,19 +104,14 @@ where
provide_context(SuspenseContext {
tasks: tasks.clone(),
});
let none_pending = ArcMemo::new({
let tasks = tasks.clone();
move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
});
let has_tasks =
Arc::new(move || !tasks.with_untracked(SlotMap::is_empty));
if let Some(set_pending) = set_pending {
Effect::new_isomorphic({
let none_pending = none_pending.clone();
@@ -133,7 +127,6 @@ where
fallback,
children,
error_boundary_parent,
has_tasks,
})
})
}

View File

@@ -73,14 +73,14 @@ where
let to = to.into_iter().collect::<Vec<_>>();
let (list, set_list) = create_signal(from.clone());
_ = request_animation_frame({
request_animation_frame({
let to = to.clone();
let then = then.clone();
move || {
set_list(to);
if let Some(then) = then {
_ = request_animation_frame({
request_animation_frame({
move || {
set_list(then);
}

View File

@@ -116,7 +116,7 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
.checked()
}
/// Handle that is generated by [request_animation_frame] and can
/// Handle that is generated by [request_animation_frame_with_handle] and can
/// be used to cancel the animation frame request.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AnimationFrameRequestHandle(i32);
@@ -129,6 +129,18 @@ impl AnimationFrameRequestHandle {
}
}
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
_ = request_animation_frame_with_handle(cb);
}
// Closure::once_into_js only frees the callback when it's actually
// called, so this instead uses into_js_value, which can be freed by
// the host JS engine's GC if it supports weak references (which all
@@ -157,7 +169,7 @@ fn closure_once(cb: impl FnOnce() + 'static) -> JsValue {
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame(
pub fn request_animation_frame_with_handle(
cb: impl FnOnce() + 'static,
) -> Result<AnimationFrameRequestHandle, JsValue> {
#[cfg(feature = "tracing")]
@@ -178,7 +190,7 @@ pub fn request_animation_frame(
raf(closure_once(cb))
}
/// Handle that is generated by [request_idle_callback] and can be
/// Handle that is generated by [request_idle_callback_with_handle] and can be
/// used to cancel the idle callback.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IdleCallbackHandle(u32);
@@ -191,6 +203,18 @@ impl IdleCallbackHandle {
}
}
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
_ = request_idle_callback_with_handle(cb);
}
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
/// returning a cancelable handle.
@@ -200,7 +224,7 @@ impl IdleCallbackHandle {
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback(
pub fn request_idle_callback_with_handle(
cb: impl Fn() + 'static,
) -> Result<IdleCallbackHandle, JsValue> {
#[cfg(feature = "tracing")]
@@ -237,7 +261,7 @@ pub fn queue_microtask(task: impl FnOnce() + 'static) {
tachys::renderer::dom::queue_microtask(task);
}
/// Handle that is generated by [set_timeout] and can be used to clear the timeout.
/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TimeoutHandle(i32);
@@ -249,6 +273,20 @@ impl TimeoutHandle {
}
}
/// Executes the given function after the given duration of time has passed.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
_ = set_timeout_with_handle(cb, duration);
}
/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
///
@@ -260,7 +298,7 @@ impl TimeoutHandle {
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[inline(always)]
pub fn set_timeout(
pub fn set_timeout_with_handle(
cb: impl FnOnce() + 'static,
duration: Duration,
) -> Result<TimeoutHandle, JsValue> {
@@ -353,7 +391,7 @@ pub fn debounce<T: 'static>(
if let Some(timer) = timer.write().unwrap().take() {
timer.clear();
}
let handle = set_timeout(
let handle = set_timeout_with_handle(
{
let cb = Arc::clone(&cb);
move || {
@@ -380,6 +418,20 @@ impl IntervalHandle {
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
_ = set_interval_with_handle(cb, duration);
}
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
@@ -392,7 +444,7 @@ impl IntervalHandle {
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[inline(always)]
pub fn set_interval(
pub fn set_interval_with_handle(
cb: impl Fn() + 'static,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.8.14"
version = "0.8.12"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -199,7 +199,7 @@ mod slot;
/// ```
///
/// 9. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](https://docs.rs/leptos/latest/leptos/prelude/struct.NodeRef.html) to use later.
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
/// ```rust
/// # use leptos::prelude::*;
///
@@ -505,9 +505,9 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
///
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
/// you could apply `#[prop(into)]` to a prop that takes
/// [Signal](https://docs.rs/leptos/latest/leptos/prelude/struct.Signal.html), which would
/// allow users to pass a [ReadSignal](https://docs.rs/leptos/latest/leptos/prelude/struct.ReadSignal.html) or
/// [RwSignal](https://docs.rs/leptos/latest/leptos/prelude/struct.RwSignal.html)
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
/// allow users to pass a [ReadSignal](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) or
/// [RwSignal](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html)
/// and automatically convert it.)
/// * `#[prop(optional)]`: If the user does not specify this property when they use the component,
/// it will be set to its default value. If the property type is `Option<T>`, values should be passed

View File

@@ -25,7 +25,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let span = field.span();
quote_spanned! {
span=> #ident: ::leptos_router::params::macro_helpers::Wrapper::<#ty>::__into_param(
span=> #ident: <#ty as ::leptos_router::params::IntoParam>::into_param(
map.get_str(#field_name_string),
#field_name_string
)?
@@ -39,8 +39,6 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let gen = quote! {
impl Params for #name {
fn from_map(map: &::leptos_router::params::ParamsMap) -> ::core::result::Result<Self, ::leptos_router::params::ParamsError> {
use ::leptos_router::params::macro_helpers::Fallback as _;
Ok(Self {
#(#fields,)*
})

View File

@@ -176,9 +176,7 @@ pub(crate) fn component_to_tokens(
let spreads = (!(spreads.is_empty())).then(|| {
if cfg!(feature = "__internal_erase_components") {
quote! {
.add_any_attr({
vec![#(::leptos::attr::any_attribute::IntoAnyAttribute::into_any_attr(#spreads),)*]
})
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
}
} else {
quote! {

View File

@@ -15,7 +15,7 @@ use codee::{
Decoder, Encoder,
};
use core::{fmt::Debug, marker::PhantomData};
use futures::{Future, FutureExt};
use futures::Future;
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{
@@ -258,17 +258,11 @@ where
if let Some(suspense_context) = use_context::<SuspenseContext>() {
if self.value.read().or_poisoned().is_none() {
let handle = suspense_context.task_id();
let mut ready =
Box::pin(SpecialNonReactiveFuture::new(self.ready()));
match ready.as_mut().now_or_never() {
Some(_) => drop(handle),
None => {
reactive_graph::spawn(async move {
ready.await;
drop(handle);
});
}
}
let ready = SpecialNonReactiveFuture::new(self.ready());
reactive_graph::spawn(async move {
ready.await;
drop(handle);
});
self.suspenses.write().or_poisoned().push(suspense_context);
}
}

View File

@@ -20,7 +20,7 @@ pub fn Demo1() -> impl IntoView {
// We need to add the 3D view onto the canvas post render.
Effect::new(move |_| {
_ = request_animation_frame(move || {
request_animation_frame(move || {
scene_sig.get_untracked().setup();
});
});

View File

@@ -95,7 +95,7 @@ pub fn App() -> impl IntoView {
// if expires_in isn't 0, then set a timeout that rerfresh a minute short of the refresh.
let expires_in = rw_expires_in.get();
if expires_in != 0 && email.get_untracked().is_some() {
let handle = set_timeout(
let handle = set_timeout_with_handle(
move || {
refresh_token.dispatch(RefreshToken {
email: email.get_untracked().unwrap(),

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.12"
version = "0.2.11"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -32,7 +32,7 @@ indexmap = { workspace = true, default-features = true }
paste = { workspace = true, default-features = true }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = { version = "0.3.83", features = ["console"] }
web-sys = { version = "0.3.77", features = ["console"] }
[dev-dependencies]
tokio = { features = [

View File

@@ -632,29 +632,12 @@ impl<T: 'static> ReadUntracked for ArcAsyncDerived<T> {
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(suspense_context) = use_context::<SuspenseContext>() {
// create a handle to register it with suspense
let handle = suspense_context.task_id();
// check if the task is *already* ready
let mut ready =
Box::pin(SpecialNonReactiveFuture::new(self.ready()));
match ready.as_mut().now_or_never() {
Some(_) => {
// if it's already ready, drop the handle immediately
// this will immediately notify the suspense context that it's complete
drop(handle);
}
None => {
// otherwise, spawn a task to wait for it to be ready, then drop the handle,
// which will notify the suspense
crate::spawn(async move {
ready.await;
drop(handle);
});
}
}
// register the suspense context with our list of them, to be notified later if this re-runs
let ready = SpecialNonReactiveFuture::new(self.ready());
crate::spawn(async move {
ready.await;
drop(handle);
});
self.inner
.write()
.or_poisoned()

View File

@@ -23,11 +23,10 @@ pin_project! {
#[derive(Clone)]
#[allow(missing_docs)]
pub struct ScopedFuture<Fut> {
owner: Owner,
observer: Option<AnySubscriber>,
diagnostics: bool,
pub owner: Owner,
pub observer: Option<AnySubscriber>,
#[pin]
fut: Fut,
pub fut: Fut,
}
}
@@ -40,7 +39,6 @@ impl<Fut> ScopedFuture<Fut> {
Self {
owner,
observer,
diagnostics: true,
fut,
}
}
@@ -53,19 +51,19 @@ impl<Fut> ScopedFuture<Fut> {
Self {
owner,
observer: None,
diagnostics: false,
fut,
}
}
#[doc(hidden)]
#[track_caller]
pub fn new_untracked_with_diagnostics(fut: Fut) -> Self {
pub fn new_untracked_with_diagnostics(
fut: Fut,
) -> ScopedFutureUntrackedWithDiagnostics<Fut> {
let owner = Owner::current().unwrap_or_default();
Self {
ScopedFutureUntrackedWithDiagnostics {
owner,
observer: None,
diagnostics: true,
fut,
}
}
@@ -77,19 +75,41 @@ impl<Fut: Future> Future for ScopedFuture<Fut> {
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
this.owner.with(|| {
this.observer.with_observer(|| {
#[cfg(debug_assertions)]
let _maybe_guard = if *this.diagnostics {
None
} else {
Some(crate::diagnostics::SpecialNonReactiveZone::enter())
};
this.fut.poll(cx)
})
#[cfg(debug_assertions)]
let _maybe_guard = if this.observer.is_none() {
Some(crate::diagnostics::SpecialNonReactiveZone::enter())
} else {
None
};
this.observer.with_observer(|| this.fut.poll(cx))
})
}
}
pin_project! {
/// A [`Future`] wrapper that sets the [`Owner`] and [`Observer`] before polling the inner
/// `Future`, output of [`ScopedFuture::new_untracked_with_diagnostics`].
///
/// In leptos 0.9 this will be replaced with `ScopedFuture` itself.
#[derive(Clone)]
pub struct ScopedFutureUntrackedWithDiagnostics<Fut> {
owner: Owner,
observer: Option<AnySubscriber>,
#[pin]
fut: Fut,
}
}
impl<Fut: Future> Future for ScopedFutureUntrackedWithDiagnostics<Fut> {
type Output = Fut::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
this.owner
.with(|| this.observer.with_observer(|| this.fut.poll(cx)))
}
}
/// Utilities used to track whether asynchronous computeds are currently loading.
pub mod suspense {
use crate::{

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.3.1"
version = "0.3.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -140,20 +140,13 @@ where
}
fn track_field(&self) {
let mut full_path = self.path().into_iter().collect::<StorePath>();
let inner = self
.inner
.get_trigger(self.inner.path().into_iter().collect());
inner.this.track();
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.track();
trigger.children.track();
// tracks `this` for all ancestors: i.e., it will track any change that is made
// directly to one of its ancestors, but not a change made to a *child* of an ancestor
// (which would end up with every subfield tracking its own siblings, because they are
// children of its parent)
while !full_path.is_empty() {
full_path.pop();
let inner = self.get_trigger(full_path.clone());
inner.this.track();
}
}
}
@@ -183,7 +176,6 @@ where
{
inner: KeyedSubfield<Inner, Prev, K, T>,
guard: Option<Guard>,
untracked: bool,
}
impl<Inner, Prev, K, T, Guard> Deref
@@ -235,7 +227,6 @@ where
K: Debug + Send + Sync + PartialEq + Eq + Hash + 'static,
{
fn untrack(&mut self) {
self.untracked = true;
if let Some(inner) = self.guard.as_mut() {
inner.untrack();
}
@@ -260,10 +251,7 @@ where
// now that the write lock is release, we can get a read lock to refresh this keyed field
// based on the new value
self.inner.update_keys();
if !self.untracked {
self.inner.notify();
}
self.inner.notify();
// reactive updates happen on the next tick
}
@@ -356,7 +344,6 @@ where
Some(KeyedSubfieldWriteGuard {
inner: self.clone(),
guard: Some(guard),
untracked: false,
})
}
@@ -368,7 +355,6 @@ where
Some(KeyedSubfieldWriteGuard {
inner: self.clone(),
guard: Some(guard),
untracked: true,
})
}
}
@@ -893,41 +879,4 @@ mod tests {
assert_eq!(b_count.load(Ordering::Relaxed), 1);
assert_eq!(c_count.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn untracked_write_on_keyed_subfield_shouldnt_notify() {
_ = any_spawner::Executor::init_tokio();
let store = Store::new(data());
assert_eq!(store.read_untracked().todos.len(), 3);
// create an effect to read from the keyed subfield
let todos_count = Arc::new(AtomicUsize::new(0));
Effect::new_sync({
let todos_count = Arc::clone(&todos_count);
move || {
store.todos().track();
todos_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
assert_eq!(todos_count.load(Ordering::Relaxed), 1);
// writing to keyed subfield notifies the iterator
store.todos().write().push(Todo {
id: 13,
label: "D".into(),
});
tick().await;
assert_eq!(todos_count.load(Ordering::Relaxed), 2);
// but an untracked write doesn't
store.todos().write_untracked().push(Todo {
id: 14,
label: "E".into(),
});
tick().await;
assert_eq!(todos_count.load(Ordering::Relaxed), 2);
}
}

View File

@@ -57,11 +57,11 @@
//! # if false { // don't run effect in doctests
//! Effect::new(move |_| {
//! // you can access individual store fields with a getter
//! println!("user: {:?}", &*store.user().read());
//! println!("todos: {:?}", &*store.todos().read());
//! });
//! # }
//!
//! // won't notify the effect that listens to `user`
//! // won't notify the effect that listens to `todos`
//! store.todos().write().push(Todo {
//! label: "Test".to_string(),
//! completed: false,
@@ -833,7 +833,7 @@ mod tests {
use reactive_graph::{
effect::Effect,
owner::StoredValue,
traits::{Read, ReadUntracked, Set, Track, Update, Write},
traits::{Read, ReadUntracked, Set, Update, Write},
};
use std::sync::{
atomic::{AtomicUsize, Ordering},
@@ -1375,34 +1375,4 @@ mod tests {
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
#[tokio::test]
async fn untracked_write_on_subfield_shouldnt_notify() {
_ = any_spawner::Executor::init_tokio();
let name_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
let tracked_field = store.user();
Effect::new_sync({
let name_count = Arc::clone(&name_count);
move |_| {
tracked_field.track();
name_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 1);
tracked_field.write().push('!');
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 2);
tracked_field.write_untracked().push('!');
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 2);
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.8.11"
version = "0.8.10"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -658,7 +658,7 @@ pub fn RoutingProgress(
move |prev: Option<Option<IntervalHandle>>| {
if is_routing.get() && !is_showing.get() {
set_is_showing.set(true);
set_interval(
set_interval_with_handle(
move || {
set_progress.update(|n| *n += percent_per_increment);
},
@@ -670,7 +670,7 @@ pub fn RoutingProgress(
prev?
} else {
set_progress.set(100.0);
_ = set_timeout(
set_timeout(
move || {
set_progress.set(0.0);
set_is_showing.set(false);

View File

@@ -131,14 +131,14 @@ where
if !IS_NAVIGATING.load(Ordering::Relaxed) {
IS_NAVIGATING.store(true, Ordering::Relaxed);
_ = request_animation_frame({
request_animation_frame({
let navigate = navigate.clone();
let nav_options = nav_options.clone();
move || {
navigate(&new_url, nav_options.clone());
IS_NAVIGATING.store(false, Ordering::Relaxed)
}
});
})
}
});

View File

@@ -259,7 +259,7 @@ impl LocationProvider for BrowserUrl {
if url.origin() == current_origin {
let navigate = navigate.clone();
// delay by a tick here, so that the Action updates *before* the redirect
_ = request_animation_frame(move || {
request_animation_frame(move || {
navigate(&url.href(), Default::default());
});
// Use set_href() if the conditions for client-side navigation were not satisfied

View File

@@ -199,56 +199,31 @@ where
}
}
/// Helpers for the `Params` derive macro to allow specialization without nightly.
pub mod macro_helpers {
use crate::params::{IntoParam, ParamsError};
// TODO can we support Option<T> and T in a non-nightly way?
#[cfg(all(feature = "nightly", rustc_nightly))]
mod option_param {
use super::{IntoParam, ParamsError};
use std::{str::FromStr, sync::Arc};
/// This struct is never actually created; it just exists so that we can impl associated
/// functions on it.
pub struct Wrapper<T>(T);
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}
impl<T: IntoParam> Wrapper<T> {
/// This is the 'preferred' impl to be used for all `T` that implement `IntoParam`.
/// Because it is directly on the struct, the compiler will pick this over the impl from
/// the `Fallback` trait.
#[inline]
pub fn __into_param(
value: Option<&str>,
name: &str,
) -> Result<T, ParamsError> {
T::into_param(value, name)
}
}
/// If the Fallback trait is in scope, then the compiler has two possible implementations for
/// `__into_params`. It will pick the one from this trait if the inherent one doesn't exist.
/// (which it won't if `T` does not implement `IntoParam`)
pub trait Fallback<T>: Sized
impl<T> IntoParam for T
where
T: FromStr,
T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
/// Fallback function in case the inherent impl on the Wrapper struct does not exist for
/// `T`
#[inline]
fn __into_param(
fn into_param(
value: Option<&str>,
name: &str,
) -> Result<T, ParamsError> {
) -> Result<Self, ParamsError> {
let value = value
.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
T::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
Self::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
}
}
impl<T> Fallback<T> for Wrapper<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
}
}
/// Errors that can occur while parsing params using [`Params`].
#[derive(Error, Debug, Clone)]
pub enum ParamsError {

View File

@@ -5,7 +5,7 @@ license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "RPC for any web framework."
readme = "../README.md"
version = "0.8.9"
version = "0.8.8"
rust-version.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ dashmap = { workspace = true, default-features = true }
## servers
# actix
actix-web = { optional = true, workspace = true, default-features = false, features = ["ws"] }
actix-web = { optional = true, workspace = true, default-features = false }
actix-ws = { optional = true, workspace = true, default-features = true }
# axum

View File

@@ -58,8 +58,7 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.as_query().unwrap_or_default();
let args = serde_qs::Config::new()
.use_form_encoding(true)
let args = serde_qs::Config::new(5, false)
.deserialize_str::<Self>(string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
@@ -98,8 +97,7 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.try_into_string().await?;
let args = serde_qs::Config::new()
.use_form_encoding(true)
let args = serde_qs::Config::new(5, false)
.deserialize_str::<Self>(&string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
@@ -138,8 +136,7 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.as_query().unwrap_or_default();
let args = serde_qs::Config::new()
.use_form_encoding(true)
let args = serde_qs::Config::new(5, false)
.deserialize_str::<Self>(string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
@@ -178,8 +175,7 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.try_into_string().await?;
let args = serde_qs::Config::new()
.use_form_encoding(true)
let args = serde_qs::Config::new(5, false)
.deserialize_str::<Self>(&string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
@@ -218,8 +214,7 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.try_into_string().await?;
let args = serde_qs::Config::new()
.use_form_encoding(true)
let args = serde_qs::Config::new(5, false)
.deserialize_str::<Self>(&string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()

View File

@@ -309,16 +309,6 @@ impl ServerFnCall {
.collect::<TokenStream2>()
}
/// Get the lint attributes for the server function.
pub fn lint_attrs(&self) -> TokenStream2 {
// pass through lint attributes from the function body
self.body
.lint_attrs
.iter()
.map(|lint_attr| quote!(#lint_attr))
.collect::<TokenStream2>()
}
fn fn_name_as_str(&self) -> String {
self.body.ident.to_string()
}
@@ -398,8 +388,6 @@ impl ServerFnCall {
PathInfo::None => quote! {},
};
let lint_attrs = self.lint_attrs();
let vis = &self.body.vis;
let struct_name = self.struct_name();
let fields = self
@@ -422,7 +410,6 @@ impl ServerFnCall {
#docs
#[derive(Debug, #derives)]
#addl_path
#lint_attrs
#vis struct #struct_name {
#(#fields),*
}
@@ -1478,8 +1465,6 @@ pub struct ServerFnBody {
pub block: TokenStream2,
/// The documentation of the server function.
pub docs: Vec<(String, Span)>,
/// The lint attributes of the server function.
pub lint_attrs: Vec<Attribute>,
/// The middleware attributes applied to the server function.
pub middlewares: Vec<Middleware>,
}
@@ -1537,27 +1522,6 @@ impl Parse for ServerFnBody {
};
!attr.path.is_ident("doc")
});
let lint_attrs = attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("allow")
|| attr.path().is_ident("warn")
|| attr.path().is_ident("deny")
|| attr.path().is_ident("forbid")
{
return Some(attr.clone());
}
None
})
.collect();
attrs.retain(|attr| {
!attr.path().is_ident("allow")
&& !attr.path().is_ident("warn")
&& !attr.path().is_ident("deny")
&& !attr.path().is_ident("forbid")
});
// extract all #[middleware] attributes, removing them from signature of dummy
let mut middlewares: Vec<Middleware> = vec![];
attrs.retain(|attr| {
@@ -1593,7 +1557,6 @@ impl Parse for ServerFnBody {
block,
attrs,
docs,
lint_attrs,
middlewares,
})
}

View File

@@ -72,7 +72,6 @@ web-sys = { features = [
"SecurityPolicyViolationEvent",
"StorageEvent",
"SubmitEvent",
"ToggleEvent",
"TouchEvent",
"TransitionEvent",
"UiEvent",
@@ -180,7 +179,6 @@ default = ["testing"]
delegation = [] # enables event delegation
error-hook = []
hydrate = []
lazy = []
islands = ["dep:serde", "dep:serde_json"]
ssr = []
oco = ["dep:oco_ref"]

View File

@@ -608,7 +608,7 @@ generate_event_types! {
animation start: AnimationEvent,
aux click: MouseEvent,
before input: InputEvent,
before toggle: ToggleEvent,
before toggle: Event, // web_sys does not include `ToggleEvent`
#[does_not_bubble]
blur: FocusEvent,
#[does_not_bubble]
@@ -719,7 +719,7 @@ generate_event_types! {
#[does_not_bubble]
time update: Event,
#[does_not_bubble]
toggle: ToggleEvent,
toggle: Event,
touch cancel: TouchEvent,
touch end: TouchEvent,
touch move: TouchEvent,

View File

@@ -17,7 +17,7 @@ use crate::{
};
use futures::future::{join, join_all};
use std::{any::TypeId, fmt::Debug};
#[cfg(any(feature = "ssr", all(feature = "hydrate", feature = "lazy")))]
#[cfg(any(feature = "ssr", feature = "hydrate"))]
use std::{future::Future, pin::Pin};
/// A type-erased view. This can be used if control flow requires that multiple different types of
@@ -67,10 +67,10 @@ pub struct AnyView {
resolve: fn(Erased) -> Pin<Box<dyn Future<Output = AnyView> + Send>>,
#[cfg(feature = "ssr")]
dry_resolve: fn(&mut Erased),
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
#[cfg(feature = "hydrate")]
#[allow(clippy::type_complexity)]
hydrate_from_server: fn(Erased, &Cursor, &PositionState) -> AnyViewState,
#[cfg(all(feature = "hydrate", feature = "lazy"))]
#[cfg(feature = "hydrate")]
#[allow(clippy::type_complexity)]
hydrate_async: fn(
Erased,
@@ -291,7 +291,7 @@ where
}
}
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
#[cfg(feature = "hydrate")]
fn hydrate_from_server<T: RenderHtml + 'static>(
value: Erased,
cursor: &Cursor,
@@ -313,7 +313,7 @@ where
}
}
#[cfg(all(feature = "hydrate", feature = "lazy"))]
#[cfg(feature = "hydrate")]
fn hydrate_async<T: RenderHtml + 'static>(
value: Erased,
cursor: &Cursor,
@@ -367,9 +367,9 @@ where
to_html_async: to_html_async::<T::Owned>,
#[cfg(feature = "ssr")]
to_html_async_ooo: to_html_async_ooo::<T::Owned>,
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
#[cfg(feature = "hydrate")]
hydrate_from_server: hydrate_from_server::<T::Owned>,
#[cfg(all(feature = "hydrate", feature = "lazy"))]
#[cfg(feature = "hydrate")]
hydrate_async: hydrate_async::<T::Owned>,
value: Erased::new(value),
}
@@ -572,7 +572,7 @@ impl RenderHtml for AnyView {
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
#[cfg(feature = "hydrate")]
{
if FROM_SERVER {
(self.hydrate_from_server)(self.value, cursor, position)
@@ -583,14 +583,6 @@ impl RenderHtml for AnyView {
);
}
}
#[cfg(all(feature = "hydrate", feature = "lazy"))]
{
use futures::FutureExt;
(self.hydrate_async)(self.value, cursor, position)
.now_or_never()
.unwrap()
}
#[cfg(not(feature = "hydrate"))]
{
_ = cursor;
@@ -607,22 +599,12 @@ impl RenderHtml for AnyView {
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
#[cfg(all(feature = "hydrate", feature = "lazy"))]
#[cfg(feature = "hydrate")]
{
#[cfg(all(feature = "hydrate", feature = "lazy"))]
let state =
(self.hydrate_async)(self.value, cursor, position).await;
state
}
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
{
_ = cursor;
_ = position;
panic!(
"the `lazy` feature on `tachys` must be activated to use lazy \
hydration"
);
}
#[cfg(not(feature = "hydrate"))]
{
_ = cursor;
@@ -711,18 +693,13 @@ impl Render for AnyViewWithAttrs {
fn rebuild(self, state: &mut Self::State) {
self.view.rebuild(&mut state.view);
// at this point, we have rebuilt the inner view
// now we need to update attributes that were spread onto this
// this approach is not ideal, but it avoids two edge cases:
// 1) merging attributes from two unrelated views (https://github.com/leptos-rs/leptos/issues/4268)
// 2) failing to re-create attributes from the same view (https://github.com/leptos-rs/leptos/issues/4512)
for element in state.elements() {
// first, remove the previous set of attributes
self.attrs
.clone()
.rebuild(&mut (element.clone(), Vec::new()));
// then, add the new set of attributes
self.attrs.clone().build(&element);
let elements = state.elements();
// FIXME this seems wrong but I think the previous version was also broken!
if let Some(element) = elements.first() {
self.attrs.rebuild(&mut (
element.clone(),
std::mem::take(&mut state.attrs),
));
}
}
}
@@ -847,7 +824,6 @@ impl AddAnyAttr for AnyViewWithAttrs {
/// State for any view with attributes spread onto it.
pub struct AnyViewWithAttrsState {
view: AnyViewState,
#[allow(dead_code)] // keeps attribute states alive until dropped
attrs: Vec<AnyAttributeState>,
}

View File

@@ -23,32 +23,14 @@ pub fn keyed<T, I, K, KF, VF, VFS, V>(
) -> Keyed<T, I, K, KF, VF, VFS, V>
where
I: IntoIterator<Item = T>,
K: Eq + Hash + SerializableKey + 'static,
K: Eq + Hash + 'static,
KF: Fn(&T) -> K,
V: Render,
VF: Fn(usize, T) -> (VFS, V),
VFS: Fn(usize),
{
Keyed {
#[cfg(not(feature = "ssr"))]
items: Some(items),
#[cfg(feature = "ssr")]
items: None,
#[cfg(feature = "ssr")]
ssr_items: items
.into_iter()
.enumerate()
.map(|(i, t)| {
let key = if cfg!(feature = "islands") {
let key = (key_fn)(&t);
key.ser_key()
} else {
String::new()
};
let (_, view) = (view_fn)(i, t);
(key, view)
})
.collect::<Vec<_>>(),
items,
key_fn,
view_fn,
}
@@ -63,9 +45,7 @@ where
VF: Fn(usize, T) -> (VFS, V),
VFS: Fn(usize),
{
items: Option<I>,
#[cfg(feature = "ssr")]
ssr_items: Vec<(String, V)>,
items: I,
key_fn: KF,
view_fn: VF,
}
@@ -126,13 +106,14 @@ where
VFS: Fn(usize),
{
type State = KeyedState<K, VFS, V>;
// TODO fallible state and try_build()/try_rebuild() here
fn build(self) -> Self::State {
let items = self.items.into_iter().flatten();
let items = self.items.into_iter();
let (capacity, _) = items.size_hint();
let mut hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
let mut rendered_items = Vec::with_capacity(capacity);
let mut rendered_items = Vec::new();
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
@@ -153,7 +134,7 @@ where
hashed_items,
ref mut rendered_items,
} = state;
let new_items = self.items.into_iter().flatten();
let new_items = self.items.into_iter();
let (capacity, _) = new_items.size_hint();
let mut new_hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
@@ -217,8 +198,6 @@ where
{
let Keyed {
items,
#[cfg(feature = "ssr")]
ssr_items,
key_fn,
view_fn,
} = self;
@@ -226,11 +205,6 @@ where
Keyed {
items,
key_fn,
#[cfg(feature = "ssr")]
ssr_items: ssr_items
.into_iter()
.map(|(k, v)| (k, v.add_any_attr(attr.clone())))
.collect(),
view_fn: Box::new(move |index, item| {
let (index, view) = view_fn(index, item);
(index, view.add_any_attr(attr.clone()))
@@ -255,39 +229,21 @@ where
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
#[cfg(feature = "ssr")]
for view in &mut self.ssr_items {
view.dry_resolve();
}
// TODO...
}
async fn resolve(self) -> Self::AsyncOutput {
#[cfg(feature = "ssr")]
{
futures::future::join_all(
self.ssr_items.into_iter().map(|(_, view)| view.resolve()),
)
.await
.into_iter()
.collect::<Vec<_>>()
}
#[cfg(not(feature = "ssr"))]
{
futures::future::join_all(
self.items.into_iter().flatten().enumerate().map(
|(index, item)| {
let (_, view) = (self.view_fn)(index, item);
view.resolve()
},
),
)
.await
.into_iter()
.collect::<Vec<_>>()
}
futures::future::join_all(self.items.into_iter().enumerate().map(
|(index, item)| {
let (_, view) = (self.view_fn)(index, item);
view.resolve()
},
))
.await
.into_iter()
.collect::<Vec<_>>()
}
#[allow(unused)]
fn to_html_with_buf(
self,
buf: &mut String,
@@ -299,9 +255,8 @@ where
if mark_branches && escape {
buf.open_branch("for");
}
#[cfg(feature = "ssr")]
for item in self.ssr_items {
for (index, item) in self.items.into_iter().enumerate() {
let (_, item) = (self.view_fn)(index, item);
if mark_branches && escape {
buf.open_branch("item");
}
@@ -323,7 +278,6 @@ where
buf.push_str("<!>");
}
#[allow(unused)]
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
@@ -335,10 +289,13 @@ where
if mark_branches && escape {
buf.open_branch("for");
}
#[cfg(feature = "ssr")]
for (key, item) in self.ssr_items {
let branch_name = mark_branches.then(|| format!("item-{key}"));
for (index, item) in self.items.into_iter().enumerate() {
let branch_name = mark_branches.then(|| {
let key = (self.key_fn)(&item);
let key = key.ser_key();
format!("item-{key}")
});
let (_, item) = (self.view_fn)(index, item);
if mark_branches && escape {
buf.open_branch(branch_name.as_ref().unwrap());
}
@@ -354,7 +311,6 @@ where
}
*position = Position::NextChild;
}
if mark_branches && escape {
buf.close_branch("for");
}
@@ -378,11 +334,11 @@ where
.expect("parent of keyed list should be an element");
// build list
let items = self.items.into_iter().flatten();
let items = self.items.into_iter();
let (capacity, _) = items.size_hint();
let mut hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
let mut rendered_items = Vec::with_capacity(capacity);
let mut rendered_items = Vec::new();
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
@@ -417,11 +373,11 @@ where
.expect("parent of keyed list should be an element");
// build list
let items = self.items.into_iter().flatten();
let items = self.items.into_iter();
let (capacity, _) = items.size_hint();
let mut hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
let mut rendered_items = Vec::with_capacity(capacity);
let mut rendered_items = Vec::new();
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);