mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
18 Commits
chore-warn
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7e18b1995 | ||
|
|
81cff63455 | ||
|
|
61186c2432 | ||
|
|
75e42ccea5 | ||
|
|
85c7cc94ad | ||
|
|
270536adb1 | ||
|
|
cec0fb8d85 | ||
|
|
764b9cd57d | ||
|
|
6de2b4006a | ||
|
|
65940cbefa | ||
|
|
8f5c34de8a | ||
|
|
4faa340ba8 | ||
|
|
5af5fdeeed | ||
|
|
9dd52e6c15 | ||
|
|
8535a10bd7 | ||
|
|
7864a12967 | ||
|
|
9733cdcfe1 | ||
|
|
1aaa716dfc |
2
.github/workflows/autofix.yml
vendored
2
.github/workflows/autofix.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
autofix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: "nightly-2025-07-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
- name: Install Glib
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -63,6 +63,6 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
|
||||
2
.github/workflows/get-example-changed.yml
vendored
2
.github/workflows/get-example-changed.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
matrix: ${{ steps.set-example-changed.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get example files that changed
|
||||
|
||||
2
.github/workflows/get-examples-matrix.yml
vendored
2
.github/workflows/get-examples-matrix.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
EXCLUDED_EXAMPLES: cargo-make
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Set Matrix
|
||||
|
||||
2
.github/workflows/get-leptos-changed.yml
vendored
2
.github/workflows/get-leptos-changed.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get source files that changed
|
||||
|
||||
2
.github/workflows/get-leptos-matrix.yml
vendored
2
.github/workflows/get-leptos-matrix.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- name: Set Matrix
|
||||
|
||||
2
.github/workflows/publish-book.yml
vendored
2
.github/workflows/publish-book.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
contents: write # To push a branch
|
||||
pull-requests: write # To create a PR from that branch
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install mdbook
|
||||
|
||||
4
.github/workflows/run-cargo-make-task.yml
vendored
4
.github/workflows/run-cargo-make-task.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
@@ -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:
|
||||
|
||||
977
Cargo.lock
generated
977
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
106
Cargo.toml
106
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.145" }
|
||||
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" }
|
||||
serde = { default-features = false, version = "1.0.219" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.103" }
|
||||
serde = { default-features = false, version = "1.0.228" }
|
||||
parking_lot = { default-features = false, version = "0.12.5" }
|
||||
axum = { default-features = false, version = "0.8.6" }
|
||||
axum = { default-features = false, version = "0.8.7" }
|
||||
serde_qs = { default-features = false, version = "0.15.0" }
|
||||
syn = { default-features = false, version = "2.0.106" }
|
||||
syn = { default-features = false, version = "2.0.111" }
|
||||
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" }
|
||||
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" }
|
||||
reqwest = { default-features = false, version = "0.12.26" }
|
||||
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.0" }
|
||||
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" }
|
||||
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.1" }
|
||||
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]
|
||||
|
||||
@@ -27,7 +27,7 @@ tokio = { version = "1.39", features = [
|
||||
], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen = "0.2.105"
|
||||
web-sys = { version = "0.3.69", features = [
|
||||
"AddEventListenerOptions",
|
||||
"Document",
|
||||
|
||||
@@ -510,11 +510,9 @@ if (window.hljs) {
|
||||
});
|
||||
view! {
|
||||
<pre><code class="language-rust">{code.await}</code></pre>
|
||||
{
|
||||
move || script.get().map(|script| {
|
||||
view! { <Script>{script}</Script> }
|
||||
})
|
||||
}
|
||||
<ShowLet some=script let:script>
|
||||
<Script>{script}</Script>
|
||||
</ShowLet>
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -567,11 +565,9 @@ if (window.hljs) {
|
||||
});
|
||||
view! {
|
||||
<pre><code class="language-rust">{code.await}</code></pre>
|
||||
{
|
||||
move || script.get().map(|script| {
|
||||
view! { <Script>{script}</Script> }
|
||||
})
|
||||
}
|
||||
<ShowLet some=script let:script>
|
||||
<Script>{script}</Script>
|
||||
</ShowLet>
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen = "0.2.105"
|
||||
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
@@ -46,12 +46,12 @@ denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "hackernews"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
|
||||
@@ -145,14 +145,11 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
}
|
||||
})} {format!(" {} | ", story.time_ago)}
|
||||
"by "
|
||||
<ShowLet some=story.user let:user>
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
</ShowLet>
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!(
|
||||
"/stories/{}",
|
||||
story.id,
|
||||
|
||||
@@ -30,17 +30,13 @@ pub fn Story() -> impl IntoView {
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">"(" {story.domain} ")"</span>
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
}
|
||||
})}
|
||||
<ShowLet some=story.user let:user>
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
</ShowLet>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
|
||||
@@ -26,7 +26,7 @@ tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1", optional = true }
|
||||
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen = "0.2.105"
|
||||
send_wrapper = { version = "0.6.0", features = ["futures"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -133,7 +133,9 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||
<ShowLet some=story.user let:user>
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
</ShowLet>
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!("/stories/{}", story.id)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
|
||||
@@ -40,18 +40,20 @@ impl LazyRoute for StoryRoute {
|
||||
<Meta name="description" content=story.title.clone()/>
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
<ShowLet some=story.user let:user>
|
||||
<p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
</ShowLet>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
|
||||
@@ -143,8 +143,10 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||
{if story.story_type != "job" {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||
"by "
|
||||
<ShowLet some=story.user let:user>
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
</ShowLet>
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!("/stories/{}", story.id)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
|
||||
@@ -32,18 +32,20 @@ pub fn Story() -> impl IntoView {
|
||||
<Meta name="description" content=story.title.clone()/>
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
<ShowLet some=story.user let:user>
|
||||
<p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
</ShowLet>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
|
||||
@@ -139,14 +139,11 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
}
|
||||
})} {format!(" {} | ", story.time_ago)}
|
||||
"by "
|
||||
<ShowLet some=story.user let:user>
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
</ShowLet>
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!(
|
||||
"/stories/{}",
|
||||
story.id,
|
||||
|
||||
@@ -35,17 +35,13 @@ pub fn Story() -> impl IntoView {
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">"("{story.domain}")"</span>
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
}
|
||||
})}
|
||||
<ShowLet some=story.user let:user>
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
</ShowLet>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
|
||||
@@ -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.92"
|
||||
wasm-bindgen = "0.2.106"
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
|
||||
38
examples/regression/e2e/features/issue_4492.feature
Normal file
38
examples/regression/e2e/features/issue_4492.feature
Normal file
@@ -0,0 +1,38 @@
|
||||
@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
|
||||
@@ -15,3 +15,9 @@ 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(())
|
||||
}
|
||||
|
||||
10
examples/regression/e2e/tests/fixtures/check.rs
vendored
10
examples/regression/e2e/tests/fixtures/check.rs
vendored
@@ -7,7 +7,15 @@ pub async fn result_text_is(
|
||||
client: &Client,
|
||||
expected_text: &str,
|
||||
) -> Result<()> {
|
||||
let actual = find::text_at_id(client, "result").await?;
|
||||
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?;
|
||||
assert_eq!(&actual, expected_text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ 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(
|
||||
@@ -54,3 +62,10 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -19,6 +19,17 @@ 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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217,
|
||||
issue_4285::Routes4285, issue_4296::Routes4296, issue_4324::Routes4324,
|
||||
pr_4015::Routes4015, pr_4091::Routes4091,
|
||||
issue_4492::Routes4492, pr_4015::Routes4015, pr_4091::Routes4091,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{MetaTags, *};
|
||||
@@ -48,6 +48,7 @@ pub fn App() -> impl IntoView {
|
||||
<Routes4285/>
|
||||
<Routes4296/>
|
||||
<Routes4324/>
|
||||
<Routes4492/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -75,6 +76,7 @@ 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>
|
||||
}
|
||||
|
||||
114
examples/regression/src/issue_4492.rs
Normal file
114
examples/regression/src/issue_4492.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
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(())
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod issue_4217;
|
||||
mod issue_4285;
|
||||
mod issue_4296;
|
||||
mod issue_4324;
|
||||
mod issue_4492;
|
||||
mod pr_4015;
|
||||
mod pr_4091;
|
||||
|
||||
|
||||
@@ -564,17 +564,12 @@ pub fn FileUploadWithProgress() -> impl IntoView {
|
||||
<input type="submit" />
|
||||
</form>
|
||||
{move || filename.get().map(|filename| view! { <p>Uploading {filename}</p> })}
|
||||
{move || {
|
||||
max.get()
|
||||
.map(|max| {
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=move || current.get().unwrap_or_default()
|
||||
></progress>
|
||||
}
|
||||
})
|
||||
}}
|
||||
<ShowLet some=max let:max>
|
||||
<progress
|
||||
max=max
|
||||
value=move || current.get().unwrap_or_default()
|
||||
></progress>
|
||||
</ShowLet>
|
||||
}
|
||||
}
|
||||
#[component]
|
||||
|
||||
@@ -663,26 +663,24 @@ impl From<Vec<FieldNavItem>> for FieldNavCtx {
|
||||
#[component]
|
||||
pub fn FieldNavPortlet() -> impl IntoView {
|
||||
let ctx = expect_context::<ReadSignal<Option<FieldNavCtx>>>();
|
||||
move || {
|
||||
let ctx = ctx.get();
|
||||
ctx.map(|ctx| {
|
||||
view! {
|
||||
<div id="FieldNavPortlet">
|
||||
<span>"FieldNavPortlet:"</span>
|
||||
<nav>
|
||||
{ctx
|
||||
.0
|
||||
.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! { <A href=href>{text}</A> }
|
||||
})
|
||||
.collect_view()
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
|
||||
view! {
|
||||
<ShowLet some=ctx let:ctx>
|
||||
<div id="FieldNavPortlet">
|
||||
<span>"FieldNavPortlet:"</span>
|
||||
<nav>
|
||||
{ctx
|
||||
.0
|
||||
.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! { <A href=href>{text}</A> }
|
||||
})
|
||||
.collect_view()
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</ShowLet>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -160,3 +160,16 @@ where
|
||||
OptionGetter(Arc::new(move || cloned.get()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker type for creating an `OptionGetter` from a static value.
|
||||
/// Used so that the compiler doesn't complain about double implementations of the trait `IntoOptionGetter`.
|
||||
pub struct StaticMarker;
|
||||
|
||||
impl<T> IntoOptionGetter<T, StaticMarker> for Option<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
fn into_option_getter(self) -> OptionGetter<T> {
|
||||
OptionGetter(Arc::new(move || self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ use reactive_graph::{
|
||||
effect::RenderEffect,
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{Dispose, Get, Read, ReadUntracked, Track, With, WriteValue},
|
||||
traits::{
|
||||
Dispose, Get, Read, ReadUntracked, Track, With, WithUntracked,
|
||||
WriteValue,
|
||||
},
|
||||
};
|
||||
use slotmap::{DefaultKey, SlotMap};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -119,14 +122,19 @@ where
|
||||
provide_context(SuspenseContext {
|
||||
tasks: tasks.clone(),
|
||||
});
|
||||
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 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 has_tasks =
|
||||
Arc::new(move || !tasks.with_untracked(SlotMap::is_empty));
|
||||
|
||||
OwnedView::new(SuspenseBoundary::<false, _, _> {
|
||||
id,
|
||||
@@ -134,6 +142,7 @@ where
|
||||
fallback,
|
||||
children,
|
||||
error_boundary_parent,
|
||||
has_tasks,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -156,6 +165,7 @@ 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
|
||||
@@ -192,12 +202,26 @@ where
|
||||
outer_owner.clone(),
|
||||
);
|
||||
|
||||
if let Some(mut state) = prev {
|
||||
let state = 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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -235,6 +259,7 @@ where
|
||||
fallback,
|
||||
children,
|
||||
error_boundary_parent,
|
||||
has_tasks,
|
||||
} = self;
|
||||
SuspenseBoundary {
|
||||
id,
|
||||
@@ -242,6 +267,7 @@ where
|
||||
fallback,
|
||||
children: children.add_any_attr(attr),
|
||||
error_boundary_parent,
|
||||
has_tasks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ use reactive_graph::{
|
||||
effect::Effect,
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{Get, Set, Track, With},
|
||||
traits::{Get, Set, Track, With, WithUntracked},
|
||||
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
|
||||
@@ -104,14 +105,19 @@ where
|
||||
provide_context(SuspenseContext {
|
||||
tasks: tasks.clone(),
|
||||
});
|
||||
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 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 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();
|
||||
@@ -127,6 +133,7 @@ where
|
||||
fallback,
|
||||
children,
|
||||
error_boundary_parent,
|
||||
has_tasks,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -15,7 +15,7 @@ use codee::{
|
||||
Decoder, Encoder,
|
||||
};
|
||||
use core::{fmt::Debug, marker::PhantomData};
|
||||
use futures::Future;
|
||||
use futures::{Future, FutureExt};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
computed::{
|
||||
@@ -258,11 +258,17 @@ where
|
||||
if let Some(suspense_context) = use_context::<SuspenseContext>() {
|
||||
if self.value.read().or_poisoned().is_none() {
|
||||
let handle = suspense_context.task_id();
|
||||
let ready = SpecialNonReactiveFuture::new(self.ready());
|
||||
reactive_graph::spawn(async move {
|
||||
ready.await;
|
||||
drop(handle);
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
self.suspenses.write().or_poisoned().push(suspense_context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -632,12 +632,29 @@ 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();
|
||||
let ready = SpecialNonReactiveFuture::new(self.ready());
|
||||
crate::spawn(async move {
|
||||
ready.await;
|
||||
drop(handle);
|
||||
});
|
||||
|
||||
// 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
|
||||
self.inner
|
||||
.write()
|
||||
.or_poisoned()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -169,6 +176,7 @@ where
|
||||
{
|
||||
inner: KeyedSubfield<Inner, Prev, K, T>,
|
||||
guard: Option<Guard>,
|
||||
untracked: bool,
|
||||
}
|
||||
|
||||
impl<Inner, Prev, K, T, Guard> Deref
|
||||
@@ -220,6 +228,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 +253,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 +349,7 @@ where
|
||||
Some(KeyedSubfieldWriteGuard {
|
||||
inner: self.clone(),
|
||||
guard: Some(guard),
|
||||
untracked: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -348,6 +361,7 @@ where
|
||||
Some(KeyedSubfieldWriteGuard {
|
||||
inner: self.clone(),
|
||||
guard: Some(guard),
|
||||
untracked: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -444,6 +458,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)
|
||||
}
|
||||
@@ -713,3 +745,182 @@ where
|
||||
.map(|key| AtKeyed::new(self.inner.clone(), key))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{self as reactive_stores, tests::tick, AtKeyed, Store};
|
||||
use reactive_graph::{
|
||||
effect::Effect,
|
||||
traits::{GetUntracked, ReadUntracked, Set, Track, Write},
|
||||
};
|
||||
use reactive_stores::Patch;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Store, Default, Patch)]
|
||||
struct Todos {
|
||||
#[store(key: usize = |todo| todo.id)]
|
||||
todos: Vec<Todo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Store, Default, Clone, PartialEq, Eq, Patch)]
|
||||
struct Todo {
|
||||
id: usize,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(id: usize, label: impl ToString) -> Self {
|
||||
Self {
|
||||
id,
|
||||
label: label.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn data() -> Todos {
|
||||
Todos {
|
||||
todos: vec![
|
||||
Todo {
|
||||
id: 10,
|
||||
label: "A".to_string(),
|
||||
},
|
||||
Todo {
|
||||
id: 11,
|
||||
label: "B".to_string(),
|
||||
},
|
||||
Todo {
|
||||
id: 12,
|
||||
label: "C".to_string(),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn keyed_fields_can_be_moved() {
|
||||
_ = any_spawner::Executor::init_tokio();
|
||||
|
||||
let store = Store::new(data());
|
||||
assert_eq!(store.read_untracked().todos.len(), 3);
|
||||
|
||||
// create an effect to read from each keyed field
|
||||
let a_count = Arc::new(AtomicUsize::new(0));
|
||||
let b_count = Arc::new(AtomicUsize::new(0));
|
||||
let c_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let a = AtKeyed::new(store.todos(), 10);
|
||||
let b = AtKeyed::new(store.todos(), 11);
|
||||
let c = AtKeyed::new(store.todos(), 12);
|
||||
|
||||
Effect::new_sync({
|
||||
let a_count = Arc::clone(&a_count);
|
||||
move || {
|
||||
a.track();
|
||||
a_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
Effect::new_sync({
|
||||
let b_count = Arc::clone(&b_count);
|
||||
move || {
|
||||
b.track();
|
||||
b_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
Effect::new_sync({
|
||||
let c_count = Arc::clone(&c_count);
|
||||
move || {
|
||||
c.track();
|
||||
c_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(b_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(c_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
// writing at a key doesn't notify siblings
|
||||
*a.label().write() = "Foo".into();
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(b_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(c_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
// the keys can be reorganized
|
||||
store.todos().write().swap(0, 2);
|
||||
let after = store.todos().get_untracked();
|
||||
assert_eq!(
|
||||
after,
|
||||
vec![Todo::new(12, "C"), Todo::new(11, "B"), Todo::new(10, "Foo")]
|
||||
);
|
||||
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(b_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(c_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
// and after we move the keys around, they still update the moved items
|
||||
a.label().set("Bar".into());
|
||||
let after = store.todos().get_untracked();
|
||||
assert_eq!(
|
||||
after,
|
||||
vec![Todo::new(12, "C"), Todo::new(11, "B"), Todo::new(10, "Bar")]
|
||||
);
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 3);
|
||||
assert_eq!(b_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(c_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
// we can remove a key and add a new one
|
||||
store.todos().write().pop();
|
||||
store.todos().write().push(Todo::new(13, "New"));
|
||||
let after = store.todos().get_untracked();
|
||||
assert_eq!(
|
||||
after,
|
||||
vec![Todo::new(12, "C"), Todo::new(11, "B"), Todo::new(13, "New")]
|
||||
);
|
||||
tick().await;
|
||||
assert_eq!(a_count.load(Ordering::Relaxed), 3);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
//! move || params.read().get("id").unwrap_or_default(),
|
||||
//! move |id| contact_data(id)
|
||||
//! );
|
||||
//! todo!()
|
||||
//! // ... return some view
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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