mirror of
https://github.com/leptos-rs/leptos.git
synced 2026-01-08 14:43:48 -05:00
Compare commits
32 Commits
4492
...
leptos_0.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d23af8581 | ||
|
|
1d268ab3a9 | ||
|
|
55b3752983 | ||
|
|
53299e4599 | ||
|
|
2d6731e508 | ||
|
|
d8b371b26b | ||
|
|
8326e514c2 | ||
|
|
6eebd71868 | ||
|
|
dd507168fa | ||
|
|
8438b1633f | ||
|
|
b87c310046 | ||
|
|
692153ded2 | ||
|
|
bbb4e698ba | ||
|
|
0c44199bc9 | ||
|
|
4f0daada69 | ||
|
|
b45eb8f39f | ||
|
|
46f16ec9cf | ||
|
|
bfdf0bf4d1 | ||
|
|
e7e18b1995 | ||
|
|
81cff63455 | ||
|
|
61186c2432 | ||
|
|
75e42ccea5 | ||
|
|
85c7cc94ad | ||
|
|
270536adb1 | ||
|
|
cec0fb8d85 | ||
|
|
764b9cd57d | ||
|
|
6de2b4006a | ||
|
|
65940cbefa | ||
|
|
8f5c34de8a | ||
|
|
4faa340ba8 | ||
|
|
5af5fdeeed | ||
|
|
9dd52e6c15 |
2
.github/workflows/run-cargo-make-task.yml
vendored
2
.github/workflows/run-cargo-make-task.yml
vendored
@@ -103,7 +103,7 @@ jobs:
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
if: contains(inputs.directory, 'examples')
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
|
||||
1041
Cargo.lock
generated
1041
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
114
Cargo.toml
114
Cargo.toml
@@ -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.14" }
|
||||
leptos = { path = "./leptos", version = "0.8.15" }
|
||||
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.12" }
|
||||
leptos_router = { path = "./router", version = "0.8.10" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.13" }
|
||||
leptos_router = { path = "./router", version = "0.8.11" }
|
||||
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.11" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.3.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.12" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.3.1" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.8" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.9" }
|
||||
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.3" }
|
||||
async-once-cell = { default-features = false, version = "0.5.4" }
|
||||
itertools = { default-features = false, version = "0.14.0" }
|
||||
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" }
|
||||
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" }
|
||||
thiserror = { default-features = false, version = "2.0.17" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.100" }
|
||||
indexmap = { default-features = false, version = "2.11.0" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.106" }
|
||||
indexmap = { default-features = false, version = "2.12.1" }
|
||||
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.11.0" }
|
||||
tracing = { default-features = false, version = "0.1.41" }
|
||||
slotmap = { default-features = false, version = "1.0.7" }
|
||||
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" }
|
||||
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.4" }
|
||||
tokio = { default-features = false, version = "1.47.1" }
|
||||
url = { default-features = false, version = "2.5.7" }
|
||||
tokio = { default-features = false, version = "1.48.0" }
|
||||
base64 = { default-features = false, version = "0.22.1" }
|
||||
cfg-if = { default-features = false, version = "1.0.3" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
|
||||
cfg-if = { default-features = false, version = "1.0.4" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.56" }
|
||||
tower = { default-features = false, version = "0.5.2" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.101" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.96" }
|
||||
serde = { default-features = false, version = "1.0.219" }
|
||||
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" }
|
||||
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" }
|
||||
xxhash-rust = { default-features = false, version = "0.8.15" }
|
||||
paste = { default-features = false, version = "1.0.15" }
|
||||
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" }
|
||||
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" }
|
||||
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.20.12" }
|
||||
glib = { default-features = false, version = "0.21.5" }
|
||||
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.4" }
|
||||
tower-http = { default-features = false, version = "0.6.8" }
|
||||
prettyplease = { default-features = false, version = "0.2.37" }
|
||||
inventory = { default-features = false, version = "0.3.21" }
|
||||
config = { default-features = false, version = "0.15.14" }
|
||||
camino = { default-features = false, version = "1.2.1" }
|
||||
config = { default-features = false, version = "0.15.19" }
|
||||
camino = { default-features = false, version = "1.2.2" }
|
||||
ciborium = { default-features = false, version = "0.2.2" }
|
||||
bitcode = { default-features = false, version = "0.6.6" }
|
||||
bitcode = { default-features = false, version = "0.6.9" }
|
||||
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.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" }
|
||||
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" }
|
||||
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
|
||||
tempfile = { default-features = false, version = "3.23.0" }
|
||||
tempfile = { default-features = false, version = "3.24.0" }
|
||||
futures-lite = { default-features = false, version = "2.6.1" }
|
||||
log = { default-features = false, version = "0.4.27" }
|
||||
log = { default-features = false, version = "0.4.29" }
|
||||
percent-encoding = { default-features = false, version = "2.3.2" }
|
||||
async-executor = { default-features = false, version = "1.13.2" }
|
||||
const-str = { default-features = false, version = "0.6.4" }
|
||||
async-executor = { default-features = false, version = "1.13.3" }
|
||||
const-str = { default-features = false, version = "0.7.1" }
|
||||
http-body-util = { default-features = false, version = "0.1.3" }
|
||||
hyper = { default-features = false, version = "1.7.0" }
|
||||
hyper = { default-features = false, version = "1.8.1" }
|
||||
postcard = { default-features = false, version = "1.1.3" }
|
||||
rmp-serde = { default-features = false, version = "1.3.0" }
|
||||
reqwest = { default-features = false, version = "0.12.23" }
|
||||
rmp-serde = { default-features = false, version = "1.3.1" }
|
||||
reqwest = { default-features = false, version = "0.12.28" }
|
||||
tower-layer = { default-features = false, version = "0.3.3" }
|
||||
attribute-derive = { default-features = false, version = "0.10.5" }
|
||||
insta = { default-features = false, version = "1.43.1" }
|
||||
codee = { default-features = false, version = "0.3.0" }
|
||||
insta = { default-features = false, version = "1.45.1" }
|
||||
codee = { default-features = false, version = "0.3.5" }
|
||||
actix-http = { default-features = false, version = "3.11.2" }
|
||||
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
|
||||
wasm-bindgen-test = { default-features = false, version = "0.3.56" }
|
||||
rustversion = { default-features = false, version = "1.0.22" }
|
||||
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" }
|
||||
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" }
|
||||
base16 = { default-features = false, version = "0.2.1" }
|
||||
digest = { default-features = false, version = "0.10.7" }
|
||||
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" }
|
||||
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" }
|
||||
wasm_split_helpers = { default-features = false, version = "0.2.0" }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -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/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.
|
||||
- **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.
|
||||
- **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 signal’s 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!)
|
||||
|
||||
@@ -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) = create_signal(0);
|
||||
let (b, set_b) = create_signal(false);
|
||||
let (a, set_a) = signal(0);
|
||||
let (b, set_b) = signal(false);
|
||||
|
||||
create_effect(move |_| {
|
||||
if a() > 5 {
|
||||
set_b(true);
|
||||
Effect::new(move |_| {
|
||||
if a.get() > 5 {
|
||||
set_b.set(true);
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -24,56 +24,10 @@ 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) = create_signal(0);
|
||||
let b = move || a () > 5;
|
||||
let (a, set_a) = signal(0);
|
||||
let b = move || a.get() > 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
|
||||
@@ -83,8 +37,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) = create_signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
let (a, set_a) = signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a.set(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
|
||||
@@ -97,8 +51,8 @@ view! {
|
||||
```
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
let (a, set_a) = signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a.set(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"] }
|
||||
leptos = { path = "../../leptos", features = ["tracing", "lazy"] }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
|
||||
@@ -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(());
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ where
|
||||
};
|
||||
|
||||
// here, we return the handle
|
||||
set_interval_with_handle(
|
||||
set_interval(
|
||||
f.clone(),
|
||||
// this is the only reactive access, so this effect will only
|
||||
// re-run when the interval changes
|
||||
|
||||
@@ -684,7 +684,7 @@ where
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
let asyn = render_app_async_stream_with_context(
|
||||
let asyn = render_app_async_with_context(
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1019,73 +1019,6 @@ 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.
|
||||
@@ -2072,19 +2005,7 @@ where
|
||||
},
|
||||
move || shell(options),
|
||||
req,
|
||||
|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>
|
||||
})
|
||||
},
|
||||
async_stream_builder,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos"
|
||||
version = "0.8.14"
|
||||
version = "0.8.15"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -122,6 +122,7 @@ subsecond = [
|
||||
"web-sys/WebSocket",
|
||||
"web-sys/Window",
|
||||
]
|
||||
lazy = ["tachys/lazy"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = [
|
||||
|
||||
@@ -83,7 +83,7 @@ pub fn AnimatedShow(
|
||||
} else {
|
||||
cls.set(hide_class);
|
||||
|
||||
let h = leptos_dom::helpers::set_timeout_with_handle(
|
||||
let h = leptos_dom::helpers::set_timeout(
|
||||
move || show.set(false),
|
||||
hide_delay,
|
||||
)
|
||||
|
||||
@@ -287,7 +287,9 @@ 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(5, false).deserialize_str::<Self>(&data)
|
||||
serde_qs::Config::new()
|
||||
.use_form_encoding(true)
|
||||
.deserialize_str::<Self>(&data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,42 @@
|
||||
if (window.location.protocol === 'https:') {
|
||||
protocol = 'wss://';
|
||||
if (window.location.protocol === "https:") {
|
||||
protocol = "wss://";
|
||||
}
|
||||
|
||||
let host = window.location.hostname;
|
||||
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`);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
if(msg.view) {
|
||||
patch(msg.view);
|
||||
}
|
||||
};
|
||||
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
|
||||
|
||||
ws.onclose = () => {
|
||||
console.warn("Live-reload disconnected. Reconnecting...");
|
||||
setTimeout(connect, 1000);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.close();
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
|
||||
.checked()
|
||||
}
|
||||
|
||||
/// Handle that is generated by [request_animation_frame_with_handle] and can
|
||||
/// Handle that is generated by [request_animation_frame] and can
|
||||
/// be used to cancel the animation frame request.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct AnimationFrameRequestHandle(i32);
|
||||
@@ -129,18 +129,6 @@ 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
|
||||
@@ -169,7 +157,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_with_handle(
|
||||
pub fn request_animation_frame(
|
||||
cb: impl FnOnce() + 'static,
|
||||
) -> Result<AnimationFrameRequestHandle, JsValue> {
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -190,7 +178,7 @@ pub fn request_animation_frame_with_handle(
|
||||
raf(closure_once(cb))
|
||||
}
|
||||
|
||||
/// Handle that is generated by [request_idle_callback_with_handle] and can be
|
||||
/// Handle that is generated by [request_idle_callback] and can be
|
||||
/// used to cancel the idle callback.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct IdleCallbackHandle(u32);
|
||||
@@ -203,18 +191,6 @@ 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.
|
||||
@@ -224,7 +200,7 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
/// 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_with_handle(
|
||||
pub fn request_idle_callback(
|
||||
cb: impl Fn() + 'static,
|
||||
) -> Result<IdleCallbackHandle, JsValue> {
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -261,7 +237,7 @@ pub fn queue_microtask(task: impl FnOnce() + 'static) {
|
||||
tachys::renderer::dom::queue_microtask(task);
|
||||
}
|
||||
|
||||
/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
|
||||
/// Handle that is generated by [set_timeout] and can be used to clear the timeout.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct TimeoutHandle(i32);
|
||||
|
||||
@@ -273,20 +249,6 @@ 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).
|
||||
///
|
||||
@@ -298,7 +260,7 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn set_timeout_with_handle(
|
||||
pub fn set_timeout(
|
||||
cb: impl FnOnce() + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<TimeoutHandle, JsValue> {
|
||||
@@ -391,7 +353,7 @@ pub fn debounce<T: 'static>(
|
||||
if let Some(timer) = timer.write().unwrap().take() {
|
||||
timer.clear();
|
||||
}
|
||||
let handle = set_timeout_with_handle(
|
||||
let handle = set_timeout(
|
||||
{
|
||||
let cb = Arc::clone(&cb);
|
||||
move || {
|
||||
@@ -418,20 +380,6 @@ 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).
|
||||
@@ -444,7 +392,7 @@ pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn set_interval_with_handle(
|
||||
pub fn set_interval(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<IntervalHandle, JsValue> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.8.12"
|
||||
version = "0.8.14"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -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/struct.NodeRef.html) to use later.
|
||||
/// [NodeRef](https://docs.rs/leptos/latest/leptos/prelude/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/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)
|
||||
/// [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)
|
||||
/// 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
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
|
||||
let span = field.span();
|
||||
|
||||
quote_spanned! {
|
||||
span=> #ident: <#ty as ::leptos_router::params::IntoParam>::into_param(
|
||||
span=> #ident: ::leptos_router::params::macro_helpers::Wrapper::<#ty>::__into_param(
|
||||
map.get_str(#field_name_string),
|
||||
#field_name_string
|
||||
)?
|
||||
@@ -39,6 +39,8 @@ 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,)*
|
||||
})
|
||||
|
||||
@@ -176,7 +176,9 @@ pub(crate) fn component_to_tokens(
|
||||
let spreads = (!(spreads.is_empty())).then(|| {
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
quote! {
|
||||
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
|
||||
.add_any_attr({
|
||||
vec![#(::leptos::attr::any_attribute::IntoAnyAttribute::into_any_attr(#spreads),)*]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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_with_handle(
|
||||
let handle = set_timeout(
|
||||
move || {
|
||||
refresh_token.dispatch(RefreshToken {
|
||||
email: email.get_untracked().unwrap(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.2.11"
|
||||
version = "0.2.12"
|
||||
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.77", features = ["console"] }
|
||||
web-sys = { version = "0.3.83", features = ["console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = [
|
||||
|
||||
@@ -23,10 +23,11 @@ pin_project! {
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ScopedFuture<Fut> {
|
||||
pub owner: Owner,
|
||||
pub observer: Option<AnySubscriber>,
|
||||
owner: Owner,
|
||||
observer: Option<AnySubscriber>,
|
||||
diagnostics: bool,
|
||||
#[pin]
|
||||
pub fut: Fut,
|
||||
fut: Fut,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +40,7 @@ impl<Fut> ScopedFuture<Fut> {
|
||||
Self {
|
||||
owner,
|
||||
observer,
|
||||
diagnostics: true,
|
||||
fut,
|
||||
}
|
||||
}
|
||||
@@ -51,19 +53,19 @@ impl<Fut> ScopedFuture<Fut> {
|
||||
Self {
|
||||
owner,
|
||||
observer: None,
|
||||
diagnostics: false,
|
||||
fut,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn new_untracked_with_diagnostics(
|
||||
fut: Fut,
|
||||
) -> ScopedFutureUntrackedWithDiagnostics<Fut> {
|
||||
pub fn new_untracked_with_diagnostics(fut: Fut) -> Self {
|
||||
let owner = Owner::current().unwrap_or_default();
|
||||
ScopedFutureUntrackedWithDiagnostics {
|
||||
Self {
|
||||
owner,
|
||||
observer: None,
|
||||
diagnostics: true,
|
||||
fut,
|
||||
}
|
||||
}
|
||||
@@ -75,41 +77,19 @@ 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(|| {
|
||||
#[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))
|
||||
this.observer.with_observer(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
let _maybe_guard = if *this.diagnostics {
|
||||
None
|
||||
} else {
|
||||
Some(crate::diagnostics::SpecialNonReactiveZone::enter())
|
||||
};
|
||||
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::{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -29,6 +29,7 @@ where
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
path: Arc<dyn Fn() -> StorePath + Send + Sync>,
|
||||
path_unkeyed: Arc<dyn Fn() -> StorePath + Send + Sync>,
|
||||
get_trigger: Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
|
||||
get_trigger_unkeyed:
|
||||
Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
|
||||
@@ -113,6 +114,10 @@ impl<T> StoreField for ArcField<T> {
|
||||
(self.path)()
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
(self.path_unkeyed)()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
(self.read)().map(StoreFieldReader::new)
|
||||
}
|
||||
@@ -137,6 +142,9 @@ where
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
path: Arc::new(move || value.path().into_iter().collect()),
|
||||
path_unkeyed: Arc::new(move || {
|
||||
value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new(move |path| value.get_trigger(path)),
|
||||
get_trigger_unkeyed: Arc::new(move |path| {
|
||||
value.get_trigger_unkeyed(path)
|
||||
@@ -163,6 +171,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -211,6 +223,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -258,6 +274,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -306,6 +326,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -358,6 +382,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
path_unkeyed: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path_unkeyed().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -396,6 +424,7 @@ impl<T> Clone for ArcField<T> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: self.defined_at,
|
||||
path: self.path.clone(),
|
||||
path_unkeyed: self.path_unkeyed.clone(),
|
||||
get_trigger: Arc::clone(&self.get_trigger),
|
||||
get_trigger_unkeyed: Arc::clone(&self.get_trigger_unkeyed),
|
||||
read: Arc::clone(&self.read),
|
||||
|
||||
@@ -76,6 +76,11 @@ where
|
||||
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner.path()
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner.path_unkeyed()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
let inner = self.inner.reader()?;
|
||||
Some(Mapped::new_with_guard(inner, |n| n.deref()))
|
||||
|
||||
@@ -73,6 +73,13 @@ where
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.try_get_value()
|
||||
.map(|inner| inner.path_unkeyed().into_iter().collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
self.inner.try_get_value().and_then(|inner| inner.reader())
|
||||
}
|
||||
|
||||
@@ -80,6 +80,13 @@ where
|
||||
.chain(iter::once(self.index.into()))
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.path_unkeyed()
|
||||
.into_iter()
|
||||
.chain(iter::once(self.index.into()))
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
|
||||
@@ -106,6 +106,13 @@ where
|
||||
.chain(iter::once(self.path_segment))
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.path_unkeyed()
|
||||
.into_iter()
|
||||
.chain(iter::once(self.path_segment))
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
@@ -133,13 +140,20 @@ where
|
||||
}
|
||||
|
||||
fn track_field(&self) {
|
||||
let inner = self
|
||||
.inner
|
||||
.get_trigger(self.inner.path().into_iter().collect());
|
||||
inner.this.track();
|
||||
let mut full_path = self.path().into_iter().collect::<StorePath>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +183,7 @@ where
|
||||
{
|
||||
inner: KeyedSubfield<Inner, Prev, K, T>,
|
||||
guard: Option<Guard>,
|
||||
untracked: bool,
|
||||
}
|
||||
|
||||
impl<Inner, Prev, K, T, Guard> Deref
|
||||
@@ -220,6 +235,7 @@ 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();
|
||||
}
|
||||
@@ -244,7 +260,10 @@ 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();
|
||||
self.inner.notify();
|
||||
|
||||
if !self.untracked {
|
||||
self.inner.notify();
|
||||
}
|
||||
|
||||
// reactive updates happen on the next tick
|
||||
}
|
||||
@@ -337,6 +356,7 @@ where
|
||||
Some(KeyedSubfieldWriteGuard {
|
||||
inner: self.clone(),
|
||||
guard: Some(guard),
|
||||
untracked: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -348,6 +368,7 @@ where
|
||||
Some(KeyedSubfieldWriteGuard {
|
||||
inner: self.clone(),
|
||||
guard: Some(guard),
|
||||
untracked: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -444,6 +465,24 @@ where
|
||||
inner.into_iter().chain(this)
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
let inner =
|
||||
self.inner.path_unkeyed().into_iter().collect::<StorePath>();
|
||||
let keys = self
|
||||
.inner
|
||||
.keys()
|
||||
.expect("using keys on a store with no keys");
|
||||
let this = keys
|
||||
.with_field_keys(
|
||||
inner.clone(),
|
||||
|keys| (keys.get(&self.key), vec![]),
|
||||
|| self.inner.latest_keys(),
|
||||
)
|
||||
.flatten()
|
||||
.map(|(_, idx)| StorePathSegment(idx));
|
||||
inner.into_iter().chain(this)
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
@@ -721,18 +760,19 @@ mod tests {
|
||||
effect::Effect,
|
||||
traits::{GetUntracked, ReadUntracked, Set, Track, Write},
|
||||
};
|
||||
use reactive_stores::Patch;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Store, Default)]
|
||||
#[derive(Debug, Store, Default, Patch)]
|
||||
struct Todos {
|
||||
#[store(key: usize = |todo| todo.id)]
|
||||
todos: Vec<Todo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Store, Default, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Store, Default, Clone, PartialEq, Eq, Patch)]
|
||||
struct Todo {
|
||||
id: usize,
|
||||
label: String,
|
||||
@@ -853,4 +893,41 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!("todos: {:?}", &*store.todos().read());
|
||||
//! println!("user: {:?}", &*store.user().read());
|
||||
//! });
|
||||
//! # }
|
||||
//!
|
||||
//! // won't notify the effect that listens to `todos`
|
||||
//! // won't notify the effect that listens to `user`
|
||||
//! 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, Update, Write},
|
||||
traits::{Read, ReadUntracked, Set, Track, Update, Write},
|
||||
};
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
@@ -1375,4 +1375,34 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ where
|
||||
type Value = T::Value;
|
||||
|
||||
fn patch(&self, new: Self::Value) {
|
||||
let path = self.path().into_iter().collect::<StorePath>();
|
||||
let path = self.path_unkeyed().into_iter().collect::<StorePath>();
|
||||
if let Some(mut writer) = self.writer() {
|
||||
// don't track the writer for the whole store
|
||||
writer.untrack();
|
||||
|
||||
@@ -38,6 +38,13 @@ pub trait StoreField: Sized {
|
||||
#[track_caller]
|
||||
fn path(&self) -> impl IntoIterator<Item = StorePathSegment>;
|
||||
|
||||
/// The path of this field (see [`StorePath`]). Uses unkeyed indices for any keyed fields.
|
||||
#[track_caller]
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
// TODO remove default impl next time we do a breaking release
|
||||
self.path()
|
||||
}
|
||||
|
||||
/// Reactively tracks this field.
|
||||
#[track_caller]
|
||||
fn track_field(&self) {
|
||||
@@ -129,7 +136,9 @@ where
|
||||
trigger
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
let caller = std::panic::Location::caller();
|
||||
let orig_path = path.clone();
|
||||
|
||||
let mut path = StorePath::with_capacity(orig_path.len());
|
||||
@@ -140,7 +149,13 @@ where
|
||||
let key = self
|
||||
.keys
|
||||
.get_key_for_index(&(path.clone(), segment.0))
|
||||
.expect("could not find key for index");
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"could not find key for index {:?} at {}",
|
||||
&(path.clone(), segment.0),
|
||||
caller
|
||||
)
|
||||
});
|
||||
path.push(key);
|
||||
} else {
|
||||
path.push(*segment);
|
||||
@@ -154,6 +169,11 @@ where
|
||||
iter::empty()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
iter::empty()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
Plain::try_new(Arc::clone(&self.value))
|
||||
@@ -205,6 +225,14 @@ where
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.try_get_value()
|
||||
.map(|n| n.path_unkeyed().into_iter().collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
self.inner.try_get_value().and_then(|n| n.reader())
|
||||
|
||||
@@ -84,6 +84,13 @@ where
|
||||
.chain(iter::once(self.path_segment))
|
||||
}
|
||||
|
||||
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.path_unkeyed()
|
||||
.into_iter()
|
||||
.chain(iter::once(self.path_segment))
|
||||
}
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.8.10"
|
||||
version = "0.8.11"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -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_with_handle(
|
||||
set_interval(
|
||||
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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -199,31 +199,56 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// 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};
|
||||
/// Helpers for the `Params` derive macro to allow specialization without nightly.
|
||||
pub mod macro_helpers {
|
||||
use crate::params::{IntoParam, ParamsError};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
auto trait NotOption {}
|
||||
impl<T> !NotOption for Option<T> {}
|
||||
/// This struct is never actually created; it just exists so that we can impl associated
|
||||
/// functions on it.
|
||||
pub struct Wrapper<T>(T);
|
||||
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr + NotOption,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(
|
||||
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<Self, ParamsError> {
|
||||
let value = value
|
||||
.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
|
||||
) -> 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
|
||||
where
|
||||
T: FromStr,
|
||||
<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(
|
||||
value: Option<&str>,
|
||||
name: &str,
|
||||
) -> Result<T, ParamsError> {
|
||||
let value = value
|
||||
.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
T::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 {
|
||||
|
||||
@@ -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.8"
|
||||
version = "0.8.9"
|
||||
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 }
|
||||
actix-web = { optional = true, workspace = true, default-features = false, features = ["ws"] }
|
||||
actix-ws = { optional = true, workspace = true, default-features = true }
|
||||
|
||||
# axum
|
||||
|
||||
@@ -58,7 +58,8 @@ 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(5, false)
|
||||
let args = serde_qs::Config::new()
|
||||
.use_form_encoding(true)
|
||||
.deserialize_str::<Self>(string_data)
|
||||
.map_err(|e| {
|
||||
ServerFnErrorErr::Args(e.to_string()).into_app_error()
|
||||
@@ -97,7 +98,8 @@ where
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, E> {
|
||||
let string_data = req.try_into_string().await?;
|
||||
let args = serde_qs::Config::new(5, false)
|
||||
let args = serde_qs::Config::new()
|
||||
.use_form_encoding(true)
|
||||
.deserialize_str::<Self>(&string_data)
|
||||
.map_err(|e| {
|
||||
ServerFnErrorErr::Args(e.to_string()).into_app_error()
|
||||
@@ -136,7 +138,8 @@ 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(5, false)
|
||||
let args = serde_qs::Config::new()
|
||||
.use_form_encoding(true)
|
||||
.deserialize_str::<Self>(string_data)
|
||||
.map_err(|e| {
|
||||
ServerFnErrorErr::Args(e.to_string()).into_app_error()
|
||||
@@ -175,7 +178,8 @@ where
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, E> {
|
||||
let string_data = req.try_into_string().await?;
|
||||
let args = serde_qs::Config::new(5, false)
|
||||
let args = serde_qs::Config::new()
|
||||
.use_form_encoding(true)
|
||||
.deserialize_str::<Self>(&string_data)
|
||||
.map_err(|e| {
|
||||
ServerFnErrorErr::Args(e.to_string()).into_app_error()
|
||||
@@ -214,7 +218,8 @@ where
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, E> {
|
||||
let string_data = req.try_into_string().await?;
|
||||
let args = serde_qs::Config::new(5, false)
|
||||
let args = serde_qs::Config::new()
|
||||
.use_form_encoding(true)
|
||||
.deserialize_str::<Self>(&string_data)
|
||||
.map_err(|e| {
|
||||
ServerFnErrorErr::Args(e.to_string()).into_app_error()
|
||||
|
||||
@@ -309,6 +309,16 @@ 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()
|
||||
}
|
||||
@@ -388,6 +398,8 @@ 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
|
||||
@@ -410,6 +422,7 @@ impl ServerFnCall {
|
||||
#docs
|
||||
#[derive(Debug, #derives)]
|
||||
#addl_path
|
||||
#lint_attrs
|
||||
#vis struct #struct_name {
|
||||
#(#fields),*
|
||||
}
|
||||
@@ -1465,6 +1478,8 @@ 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>,
|
||||
}
|
||||
@@ -1522,6 +1537,27 @@ 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| {
|
||||
@@ -1557,6 +1593,7 @@ impl Parse for ServerFnBody {
|
||||
block,
|
||||
attrs,
|
||||
docs,
|
||||
lint_attrs,
|
||||
middlewares,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ web-sys = { features = [
|
||||
"SecurityPolicyViolationEvent",
|
||||
"StorageEvent",
|
||||
"SubmitEvent",
|
||||
"ToggleEvent",
|
||||
"TouchEvent",
|
||||
"TransitionEvent",
|
||||
"UiEvent",
|
||||
@@ -179,6 +180,7 @@ default = ["testing"]
|
||||
delegation = [] # enables event delegation
|
||||
error-hook = []
|
||||
hydrate = []
|
||||
lazy = []
|
||||
islands = ["dep:serde", "dep:serde_json"]
|
||||
ssr = []
|
||||
oco = ["dep:oco_ref"]
|
||||
|
||||
@@ -608,7 +608,7 @@ generate_event_types! {
|
||||
animation start: AnimationEvent,
|
||||
aux click: MouseEvent,
|
||||
before input: InputEvent,
|
||||
before toggle: Event, // web_sys does not include `ToggleEvent`
|
||||
before toggle: 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: Event,
|
||||
toggle: ToggleEvent,
|
||||
touch cancel: TouchEvent,
|
||||
touch end: TouchEvent,
|
||||
touch move: TouchEvent,
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
};
|
||||
use futures::future::{join, join_all};
|
||||
use std::{any::TypeId, fmt::Debug};
|
||||
#[cfg(any(feature = "ssr", feature = "hydrate"))]
|
||||
#[cfg(any(feature = "ssr", all(feature = "hydrate", feature = "lazy")))]
|
||||
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(feature = "hydrate")]
|
||||
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
hydrate_from_server: fn(Erased, &Cursor, &PositionState) -> AnyViewState,
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(all(feature = "hydrate", feature = "lazy"))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
hydrate_async: fn(
|
||||
Erased,
|
||||
@@ -291,7 +291,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
|
||||
fn hydrate_from_server<T: RenderHtml + 'static>(
|
||||
value: Erased,
|
||||
cursor: &Cursor,
|
||||
@@ -313,7 +313,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(all(feature = "hydrate", feature = "lazy"))]
|
||||
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(feature = "hydrate")]
|
||||
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
|
||||
hydrate_from_server: hydrate_from_server::<T::Owned>,
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(all(feature = "hydrate", feature = "lazy"))]
|
||||
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(feature = "hydrate")]
|
||||
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
|
||||
{
|
||||
if FROM_SERVER {
|
||||
(self.hydrate_from_server)(self.value, cursor, position)
|
||||
@@ -583,6 +583,14 @@ 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;
|
||||
@@ -599,12 +607,22 @@ impl RenderHtml for AnyView {
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(all(feature = "hydrate", feature = "lazy"))]
|
||||
{
|
||||
#[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;
|
||||
@@ -693,13 +711,18 @@ impl Render for AnyViewWithAttrs {
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.view.rebuild(&mut state.view);
|
||||
|
||||
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),
|
||||
));
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -824,6 +847,7 @@ 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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,32 @@ 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 + 'static,
|
||||
K: Eq + Hash + SerializableKey + 'static,
|
||||
KF: Fn(&T) -> K,
|
||||
V: Render,
|
||||
VF: Fn(usize, T) -> (VFS, V),
|
||||
VFS: Fn(usize),
|
||||
{
|
||||
Keyed {
|
||||
items,
|
||||
#[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<_>>(),
|
||||
key_fn,
|
||||
view_fn,
|
||||
}
|
||||
@@ -45,7 +63,9 @@ where
|
||||
VF: Fn(usize, T) -> (VFS, V),
|
||||
VFS: Fn(usize),
|
||||
{
|
||||
items: I,
|
||||
items: Option<I>,
|
||||
#[cfg(feature = "ssr")]
|
||||
ssr_items: Vec<(String, V)>,
|
||||
key_fn: KF,
|
||||
view_fn: VF,
|
||||
}
|
||||
@@ -106,14 +126,13 @@ 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();
|
||||
let items = self.items.into_iter().flatten();
|
||||
let (capacity, _) = items.size_hint();
|
||||
let mut hashed_items =
|
||||
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
|
||||
let mut rendered_items = Vec::new();
|
||||
let mut rendered_items = Vec::with_capacity(capacity);
|
||||
for (index, item) in items.enumerate() {
|
||||
hashed_items.insert((self.key_fn)(&item));
|
||||
let (set_index, view) = (self.view_fn)(index, item);
|
||||
@@ -134,7 +153,7 @@ where
|
||||
hashed_items,
|
||||
ref mut rendered_items,
|
||||
} = state;
|
||||
let new_items = self.items.into_iter();
|
||||
let new_items = self.items.into_iter().flatten();
|
||||
let (capacity, _) = new_items.size_hint();
|
||||
let mut new_hashed_items =
|
||||
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
|
||||
@@ -198,6 +217,8 @@ where
|
||||
{
|
||||
let Keyed {
|
||||
items,
|
||||
#[cfg(feature = "ssr")]
|
||||
ssr_items,
|
||||
key_fn,
|
||||
view_fn,
|
||||
} = self;
|
||||
@@ -205,6 +226,11 @@ 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()))
|
||||
@@ -229,21 +255,39 @@ where
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
// TODO...
|
||||
#[cfg(feature = "ssr")]
|
||||
for view in &mut self.ssr_items {
|
||||
view.dry_resolve();
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
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<_>>()
|
||||
#[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<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn to_html_with_buf(
|
||||
self,
|
||||
buf: &mut String,
|
||||
@@ -255,8 +299,9 @@ where
|
||||
if mark_branches && escape {
|
||||
buf.open_branch("for");
|
||||
}
|
||||
for (index, item) in self.items.into_iter().enumerate() {
|
||||
let (_, item) = (self.view_fn)(index, item);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
for item in self.ssr_items {
|
||||
if mark_branches && escape {
|
||||
buf.open_branch("item");
|
||||
}
|
||||
@@ -278,6 +323,7 @@ where
|
||||
buf.push_str("<!>");
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
self,
|
||||
buf: &mut StreamBuilder,
|
||||
@@ -289,13 +335,10 @@ where
|
||||
if mark_branches && escape {
|
||||
buf.open_branch("for");
|
||||
}
|
||||
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);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
for (key, item) in self.ssr_items {
|
||||
let branch_name = mark_branches.then(|| format!("item-{key}"));
|
||||
if mark_branches && escape {
|
||||
buf.open_branch(branch_name.as_ref().unwrap());
|
||||
}
|
||||
@@ -311,6 +354,7 @@ where
|
||||
}
|
||||
*position = Position::NextChild;
|
||||
}
|
||||
|
||||
if mark_branches && escape {
|
||||
buf.close_branch("for");
|
||||
}
|
||||
@@ -334,11 +378,11 @@ where
|
||||
.expect("parent of keyed list should be an element");
|
||||
|
||||
// build list
|
||||
let items = self.items.into_iter();
|
||||
let items = self.items.into_iter().flatten();
|
||||
let (capacity, _) = items.size_hint();
|
||||
let mut hashed_items =
|
||||
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
|
||||
let mut rendered_items = Vec::new();
|
||||
let mut rendered_items = Vec::with_capacity(capacity);
|
||||
for (index, item) in items.enumerate() {
|
||||
hashed_items.insert((self.key_fn)(&item));
|
||||
let (set_index, view) = (self.view_fn)(index, item);
|
||||
@@ -373,11 +417,11 @@ where
|
||||
.expect("parent of keyed list should be an element");
|
||||
|
||||
// build list
|
||||
let items = self.items.into_iter();
|
||||
let items = self.items.into_iter().flatten();
|
||||
let (capacity, _) = items.size_hint();
|
||||
let mut hashed_items =
|
||||
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
|
||||
let mut rendered_items = Vec::new();
|
||||
let mut rendered_items = Vec::with_capacity(capacity);
|
||||
for (index, item) in items.enumerate() {
|
||||
hashed_items.insert((self.key_fn)(&item));
|
||||
let (set_index, view) = (self.view_fn)(index, item);
|
||||
|
||||
Reference in New Issue
Block a user