mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 18:22:34 -05:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1524386346 | ||
|
|
426b079709 | ||
|
|
c6f176e2b0 | ||
|
|
75662d08e7 | ||
|
|
4448b77cde | ||
|
|
956af8e466 | ||
|
|
8c469b85d6 | ||
|
|
7f93dd224d | ||
|
|
777b5e1e54 | ||
|
|
433f7284e6 | ||
|
|
4a8a212d84 | ||
|
|
1d7bc021af | ||
|
|
74055a7e13 | ||
|
|
c98082de74 | ||
|
|
b8d44e20a9 | ||
|
|
00e83e0d70 | ||
|
|
e89b1389ca | ||
|
|
bd454d03e2 | ||
|
|
d7f4457ea4 | ||
|
|
17d357bcec | ||
|
|
66d1bead9a | ||
|
|
69c918e813 | ||
|
|
2817a261ce | ||
|
|
972b1ff90b | ||
|
|
3a66a1f3d3 | ||
|
|
504f983996 | ||
|
|
0862385816 | ||
|
|
8319446d3f | ||
|
|
5fa31941bb | ||
|
|
f4bb87ea1e | ||
|
|
016fbf8da1 | ||
|
|
21fd995468 | ||
|
|
683e7177dd | ||
|
|
33b278c014 | ||
|
|
5fc56346f4 | ||
|
|
afb37aaf4b | ||
|
|
f8fd79725a | ||
|
|
131251b361 | ||
|
|
91fb315fe0 | ||
|
|
6954b77b62 | ||
|
|
77176f8395 | ||
|
|
344b79a01b | ||
|
|
051059c761 | ||
|
|
3c540dd858 | ||
|
|
4125688a0a | ||
|
|
bd3b962cfb | ||
|
|
5dd3c217c4 | ||
|
|
ae00e5ae13 | ||
|
|
1ce671ba08 | ||
|
|
ec9f26bd9f | ||
|
|
831eae31bc | ||
|
|
ff6ae5de25 | ||
|
|
c21712ba04 | ||
|
|
45771b6fd3 | ||
|
|
f3557970a7 | ||
|
|
c87ef331b0 | ||
|
|
e767518142 | ||
|
|
f94b681118 | ||
|
|
9c50e49253 | ||
|
|
57c7097ede | ||
|
|
1a06e0eee8 | ||
|
|
ce9af4a685 | ||
|
|
e0c79eb8d8 | ||
|
|
9fd972971e | ||
|
|
9473220639 | ||
|
|
ae11812dc6 | ||
|
|
4c55c25445 |
8
.github/workflows/autofix.yml
vendored
8
.github/workflows/autofix.yml
vendored
@@ -21,7 +21,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: "nightly-2025-04-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
with:
|
||||
{
|
||||
toolchain: "nightly-2025-07-16",
|
||||
components: "rustfmt, clippy",
|
||||
target: "wasm32-unknown-unknown",
|
||||
rustflags: "",
|
||||
}
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
||||
6
.github/workflows/run-cargo-make-task.yml
vendored
6
.github/workflows/run-cargo-make-task.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain: [stable, nightly-2025-04-16]
|
||||
toolchain: [stable, nightly-2025-07-16]
|
||||
erased_mode: [true, false]
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
@@ -169,7 +169,9 @@ jobs:
|
||||
cd '${{ inputs.directory }}'
|
||||
cargo make --no-workspace --profile=github-actions ci
|
||||
# check the direct-minimal-versions on release
|
||||
if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
COMMIT_MSG=$(git log -1 --pretty=format:'%s')
|
||||
# Supports: v1.2.3, v1.2.3-alpha, v1.2.3-beta1, v1.2.3-rc.1, etc.
|
||||
if [[ "$COMMIT_MSG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.?[0-9]+)?)?$ ]]; then
|
||||
cargo make --no-workspace --profile=github-actions check-minimal-versions
|
||||
fi
|
||||
# Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode
|
||||
|
||||
516
Cargo.lock
generated
516
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
58
Cargo.toml
58
Cargo.toml
@@ -40,43 +40,43 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.dependencies]
|
||||
# members
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
either_of = { path = "./either_of/", version = "0.1.6" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
leptos = { path = "./leptos", version = "0.8.2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.2" }
|
||||
leptos_router = { path = "./router", version = "0.8.2" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.2" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.2" }
|
||||
leptos = { path = "./leptos", version = "0.8.4" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.4" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.4" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.4" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.4" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.4" }
|
||||
leptos_router = { path = "./router", version = "0.8.4" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.4" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.4" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.4" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.2" }
|
||||
tachys = { path = "./tachys", version = "0.2.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.4" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.4" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.4" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.4" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.4" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.4" }
|
||||
tachys = { path = "./tachys", version = "0.2.5" }
|
||||
|
||||
# members deps
|
||||
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.140" }
|
||||
trybuild = { default-features = false, version = "1.0.105" }
|
||||
trybuild = { default-features = false, version = "1.0.106" }
|
||||
typed-builder = { default-features = false, version = "0.21.0" }
|
||||
thiserror = { default-features = false, version = "2.0.12" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.100" }
|
||||
@@ -98,7 +98,7 @@ proc-macro-error2 = { default-features = false, version = "2.0.1" }
|
||||
const_format = { default-features = false, version = "0.2.34" }
|
||||
gloo-net = { default-features = false, version = "0.6.0" }
|
||||
url = { default-features = false, version = "2.5.4" }
|
||||
tokio = { default-features = false, version = "1.45.1" }
|
||||
tokio = { default-features = false, version = "1.46.1" }
|
||||
base64 = { default-features = false, version = "0.22.1" }
|
||||
cfg-if = { default-features = false, version = "1.0.0" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
|
||||
@@ -108,7 +108,7 @@ serde = { default-features = false, version = "1.0.219" }
|
||||
parking_lot = { default-features = false, version = "0.12.4" }
|
||||
axum = { default-features = false, version = "0.8.4" }
|
||||
serde_qs = { default-features = false, version = "0.15.0" }
|
||||
syn = { default-features = false, version = "2.0.101" }
|
||||
syn = { default-features = false, version = "2.0.104" }
|
||||
xxhash-rust = { default-features = false, version = "0.8.15" }
|
||||
paste = { default-features = false, version = "1.0.15" }
|
||||
quote = { default-features = false, version = "1.0.40" }
|
||||
@@ -116,10 +116,10 @@ web-sys = { default-features = false, version = "0.3.77" }
|
||||
js-sys = { default-features = false, version = "0.3.77" }
|
||||
rand = { default-features = false, version = "0.9.1" }
|
||||
serde-lite = { default-features = false, version = "0.5.0" }
|
||||
tokio-tungstenite = { default-features = false, version = "0.26.2" }
|
||||
tokio-tungstenite = { default-features = false, version = "0.27.0" }
|
||||
serial_test = { default-features = false, version = "3.2.0" }
|
||||
erased = { default-features = false, version = "0.1.2" }
|
||||
glib = { default-features = false, version = "0.20.10" }
|
||||
glib = { default-features = false, version = "0.20.12" }
|
||||
async-trait = { default-features = false, version = "0.1.88" }
|
||||
typed-builder-macro = { default-features = false, version = "0.21.0" }
|
||||
linear-map = { default-features = false, version = "1.2.0" }
|
||||
@@ -127,9 +127,9 @@ anyhow = { default-features = false, version = "1.0.98" }
|
||||
walkdir = { default-features = false, version = "2.5.0" }
|
||||
actix-ws = { default-features = false, version = "0.3.0" }
|
||||
tower-http = { default-features = false, version = "0.6.4" }
|
||||
prettyplease = { default-features = false, version = "0.2.33" }
|
||||
prettyplease = { default-features = false, version = "0.2.35" }
|
||||
inventory = { default-features = false, version = "0.3.20" }
|
||||
config = { default-features = false, version = "0.15.11" }
|
||||
config = { default-features = false, version = "0.15.13" }
|
||||
camino = { default-features = false, version = "1.1.9" }
|
||||
ciborium = { default-features = false, version = "0.2.2" }
|
||||
multer = { default-features = false, version = "3.1.0" }
|
||||
@@ -149,12 +149,12 @@ futures-lite = { default-features = false, version = "2.6.0" }
|
||||
log = { default-features = false, version = "0.4.27" }
|
||||
percent-encoding = { default-features = false, version = "2.3.1" }
|
||||
async-executor = { default-features = false, version = "1.13.2" }
|
||||
const-str = { default-features = false, version = "0.6.2" }
|
||||
const-str = { default-features = false, version = "0.6.3" }
|
||||
http-body-util = { default-features = false, version = "0.1.3" }
|
||||
hyper = { default-features = false, version = "1.6.0" }
|
||||
postcard = { default-features = false, version = "1.1.1" }
|
||||
rmp-serde = { default-features = false, version = "1.3.0" }
|
||||
reqwest = { default-features = false, version = "0.12.18" }
|
||||
reqwest = { default-features = false, version = "0.12.22" }
|
||||
tower-layer = { default-features = false, version = "0.3.3" }
|
||||
attribute-derive = { default-features = false, version = "0.10.3" }
|
||||
insta = { default-features = false, version = "1.43.1" }
|
||||
|
||||
26
README.md
26
README.md
@@ -90,35 +90,13 @@ Here are some resources for learning more about Leptos:
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
|
||||
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` version of Rust and the `nightly` feature of Leptos. To use `nightly` Rust, you can either set your toolchain globally or on per-project basis.
|
||||
|
||||
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you haven’t already):
|
||||
|
||||
```
|
||||
rustup toolchain install nightly
|
||||
rustup default nightly
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
|
||||
|
||||
```toml
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
```
|
||||
|
||||
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
cargo leptos new --git https://github.com/leptos-rs/start
|
||||
cargo leptos new --git https://github.com/leptos-rs/start-axum
|
||||
cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
@@ -147,7 +125,7 @@ Yes, I’m sure there are. You can see from the state of our issue tracker over
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
There are several people in the community using Leptos right now for many websites at work, who have also become significant contributors. There may be missing features that you need, and you may end up building them! But, if you're willing to contribute a few missing pieces along the way, the framework is most definitely usable for production applications, especially given the ecosystem of libraries that have sprung up around it.
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
name = "benchmarks"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[dependencies]
|
||||
l0410 = { package = "leptos", version = "0.4.10", features = [
|
||||
|
||||
@@ -19,7 +19,7 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.39", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
name = "counter_isomorphic"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
@@ -18,7 +18,7 @@ tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1" }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4.22"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing-subscriber-wasm = "0.1.0"
|
||||
|
||||
@@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
|
||||
|
||||
20
examples/regression/e2e/features/issue_4088.feature
Normal file
20
examples/regression/e2e/features/issue_4088.feature
Normal file
@@ -0,0 +1,20 @@
|
||||
@check_issue_4088
|
||||
Feature: Check that issue 4088 does not reappear
|
||||
|
||||
Scenario: I can see the navbar
|
||||
Given I see the app
|
||||
And I can access regression test 4088
|
||||
Then I see the navbar
|
||||
|
||||
Scenario: The user info is shared via context
|
||||
Given I see the app
|
||||
And I can access regression test 4088
|
||||
When I select the link Class 1
|
||||
Then I see the result is the string Assignments for team of user with id 42
|
||||
|
||||
Scenario: The user info is shared via context
|
||||
Given I see the app
|
||||
And I can access regression test 4088
|
||||
When I select the link Class 1
|
||||
When I refresh the browser
|
||||
Then I see the result is the string Assignments for team of user with id 42
|
||||
8
examples/regression/e2e/features/pr_4015.feature
Normal file
8
examples/regression/e2e/features/pr_4015.feature
Normal file
@@ -0,0 +1,8 @@
|
||||
@check_pr_4015
|
||||
Feature: Check that PR 4015 does not regress
|
||||
|
||||
Scenario: The correct text appears
|
||||
Given I see the app
|
||||
And I can access regression test 4015
|
||||
Then I see the result is the string Some(42)
|
||||
|
||||
@@ -24,3 +24,25 @@ Feature: Regression from pull request 4091
|
||||
| test1 |
|
||||
| 4091 Home |
|
||||
Then I see the result is empty
|
||||
|
||||
Scenario: I can see the navbar
|
||||
Given I see the app
|
||||
And I can access regression test 4091
|
||||
Then I see the navbar
|
||||
|
||||
Scenario: If I navigate to home and back, I can still see the navbar
|
||||
Given I see the app
|
||||
And I can access regression test 4091
|
||||
When I select the following links
|
||||
| Home |
|
||||
| 4091 |
|
||||
Then I see the navbar
|
||||
|
||||
Scenario: The signal is not disposed too early
|
||||
Given I see the app
|
||||
And I can access regression test 4091
|
||||
When I select the following links
|
||||
| test1 |
|
||||
| Home |
|
||||
| 4091 |
|
||||
Then I see the navbar
|
||||
@@ -11,3 +11,10 @@ pub async fn result_text_is(
|
||||
assert_eq!(&actual, expected_text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn element_exists(client: &Client, id: &str) -> Result<()> {
|
||||
find::element_by_id(client, id)
|
||||
.await
|
||||
.expect(&format!("could not find element with id `{id}`"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ use anyhow::{Ok, Result};
|
||||
use fantoccini::{elements::Element, Client, Locator};
|
||||
|
||||
pub async fn text_at_id(client: &Client, id: &str) -> Result<String> {
|
||||
let element = client
|
||||
.wait()
|
||||
.for_element(Locator::Id(id))
|
||||
let element = element_by_id(client, id)
|
||||
.await
|
||||
.expect(format!("no such element with id `{}`", id).as_str());
|
||||
let text = element.text().await?;
|
||||
@@ -19,3 +17,7 @@ pub async fn link_with_text(client: &Client, text: &str) -> Result<Element> {
|
||||
.expect(format!("Link not found by `{}`", text).as_str());
|
||||
Ok(link)
|
||||
}
|
||||
|
||||
pub async fn element_by_id(client: &Client, id: &str) -> Result<Element> {
|
||||
Ok(client.wait().for_element(Locator::Id(id)).await?)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ use anyhow::{Ok, Result};
|
||||
use cucumber::then;
|
||||
|
||||
#[then(regex = r"^I see the result is empty$")]
|
||||
async fn i_see_the_result_is_empty(
|
||||
world: &mut AppWorld,
|
||||
) -> Result<()> {
|
||||
async fn i_see_the_result_is_empty(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::result_text_is(client, "").await?;
|
||||
Ok(())
|
||||
@@ -20,3 +18,10 @@ async fn i_see_the_result_is_the_string(
|
||||
check::result_text_is(client, &text).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = r"^I see the navbar$")]
|
||||
async fn i_see_the_navbar(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::element_exists(client, "nav").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::pr_4091::Routes4091;
|
||||
use crate::{issue_4088::Routes4088, pr_4015::Routes4015, pr_4091::Routes4091};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{MetaTags, *};
|
||||
use leptos_router::{
|
||||
@@ -35,6 +35,8 @@ pub fn App() -> impl IntoView {
|
||||
<Routes fallback>
|
||||
<Route path=path!("") view=HomePage/>
|
||||
<Routes4091/>
|
||||
<Routes4015/>
|
||||
<Routes4088/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -55,6 +57,8 @@ fn HomePage() -> impl IntoView {
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/4091/">"4091"</a></li>
|
||||
<li><a href="/4015/">"4015"</a></li>
|
||||
<li><a href="/4088/">"4088"</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
119
examples/regression/src/issue_4088.rs
Normal file
119
examples/regression/src/issue_4088.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use leptos::{either::Either, prelude::*};
|
||||
#[allow(unused_imports)]
|
||||
use leptos_router::{
|
||||
components::{Outlet, ParentRoute, Redirect, Route},
|
||||
path, MatchNestedRoutes, NavigateOptions,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[component]
|
||||
pub fn Routes4088() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("4088") view=|| view!{ <LoggedIn/> }>
|
||||
<ParentRoute path=path!("") view=||view!{<AssignmentsSelector/>}>
|
||||
<Route path=path!("/:team_id") view=||view!{<AssignmentsForTeam/>} />
|
||||
<Route path=path!("") view=||view!{ <p>No class selected</p> }/>
|
||||
</ParentRoute>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UserInfo {
|
||||
pub id: usize,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_user_info() -> Result<Option<UserInfo>, ServerFnError> {
|
||||
Ok(Some(UserInfo { id: 42 }))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoggedIn() -> impl IntoView {
|
||||
let user_info_resource =
|
||||
Resource::new(|| (), move |_| async { get_user_info().await });
|
||||
|
||||
view! {
|
||||
|
||||
<Transition fallback=move || view!{
|
||||
"loading"
|
||||
}
|
||||
>
|
||||
{move || {
|
||||
user_info_resource.get()
|
||||
.map(|a|
|
||||
match a {
|
||||
Ok(Some(a)) => Either::Left(view! {
|
||||
<LoggedInContent user_info={a} />
|
||||
}),
|
||||
_ => Either::Right(view!{
|
||||
<Redirect path="/not_logged_in"/>
|
||||
})
|
||||
})
|
||||
}}
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
/// Component which provides UserInfo and renders it's child
|
||||
/// Can also contain some code to check for specific situations (e.g. privacy policies accepted or not? redirect if needed...)
|
||||
pub fn LoggedInContent(user_info: UserInfo) -> impl IntoView {
|
||||
provide_context(user_info.clone());
|
||||
|
||||
if user_info.id == 42 {
|
||||
Either::Left(Outlet())
|
||||
} else {
|
||||
Either::Right(
|
||||
view! { <Redirect path="/somewhere" options={NavigateOptions::default()}/> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
/// This component also uses Outlet (so nested Outlet)
|
||||
fn AssignmentsSelector() -> impl IntoView {
|
||||
let user_info = use_context::<UserInfo>().expect("user info not provided");
|
||||
|
||||
view! {
|
||||
<p>"Assignments for user with ID: "{user_info.id}</p>
|
||||
<ul id="nav">
|
||||
<li><a href="/4088/1">"Class 1"</a></li>
|
||||
<li><a href="/4088/2">"Class 2"</a></li>
|
||||
<li><a href="/4088/3">"Class 3"</a></li>
|
||||
</ul>
|
||||
|
||||
<Outlet />
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn AssignmentsForTeam() -> impl IntoView {
|
||||
// THIS FAILS -> Because of the nested outlet in LoggedInContent > AssignmentsSelector?
|
||||
// It did not fail when LoggedIn did not use a resource and transition (but a hardcoded UserInfo in the component)
|
||||
let user_info = use_context::<UserInfo>().expect("user info not provided");
|
||||
|
||||
let items = vec!["Assignment 1", "Assignment 2", "Assignment 3"];
|
||||
view! {
|
||||
<p id="result">"Assignments for team of user with id " {user_info.id}</p>
|
||||
<ul>
|
||||
{
|
||||
items.into_iter().map(|item| {
|
||||
view! {
|
||||
<Assignment name=item.to_string() />
|
||||
}
|
||||
}).collect_view()
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Assignment(name: String) -> impl IntoView {
|
||||
let user_info = use_context::<UserInfo>().expect("user info not provided");
|
||||
|
||||
view! {
|
||||
<li>{name}" "{user_info.id}</li>
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod app;
|
||||
mod issue_4088;
|
||||
mod pr_4015;
|
||||
mod pr_4091;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
|
||||
29
examples/regression/src/pr_4015.rs
Normal file
29
examples/regression/src/pr_4015.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use leptos::{context::Provider, prelude::*};
|
||||
use leptos_router::{
|
||||
components::{ParentRoute, Route},
|
||||
nested_router::Outlet,
|
||||
path,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn Routes4015() -> impl leptos_router::MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("4015") view=|| view! {
|
||||
<Provider value=42i32>
|
||||
<Outlet/>
|
||||
</Provider>
|
||||
}>
|
||||
<Route path=path!("") view=Child/>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Child() -> impl IntoView {
|
||||
let value = use_context::<i32>();
|
||||
|
||||
view! {
|
||||
<p id="result">{format!("{value:?}")}</p>
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,9 @@ fn Container() -> impl IntoView {
|
||||
provide_context(rw_signal);
|
||||
|
||||
view! {
|
||||
<nav>
|
||||
<nav id="nav">
|
||||
<ul>
|
||||
<li><A href="/">"Home"</A></li>
|
||||
<li><A href="./">"4091 Home"</A></li>
|
||||
<li><A href="test1">"test1"</A></li>
|
||||
</ul>
|
||||
|
||||
@@ -29,7 +29,7 @@ tower-http = { version = "0.6.2", features = [
|
||||
"trace",
|
||||
], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "2.0.11"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde_toml = "0.0.1"
|
||||
toml = "0.8.19"
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
name = "ssr_modes"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -19,7 +17,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.39", features = ["time"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
name = "ssr_modes_axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
# std::sync::LazyLock is stabilized in Rust version 1.80.0
|
||||
rust-version = "1.80.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -19,7 +17,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
|
||||
@@ -17,7 +17,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
|
||||
@@ -159,7 +159,7 @@ fn TodoRow(
|
||||
|
||||
view! {
|
||||
<li style:text-decoration=move || {
|
||||
status.done().then_some("line-through").unwrap_or_default()
|
||||
if status.done() { "line-through" } else { Default::default() }
|
||||
}>
|
||||
|
||||
<p
|
||||
|
||||
@@ -20,7 +20,7 @@ tokio = { version = "1.39", features = [
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.93"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
http = "1.1"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
@@ -44,12 +44,12 @@ denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"]
|
||||
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 = "todo_app_sqlite"
|
||||
# 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"
|
||||
|
||||
@@ -20,11 +20,11 @@ axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -20,11 +20,11 @@ tower = { version = "0.5.1", features = ["util"], optional = true }
|
||||
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1" }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "2.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -17,7 +17,7 @@ simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "2.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.100"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -11,7 +11,7 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
actix-http = { workspace = true, default-features = true }
|
||||
actix-files = { workspace = true, default-features = true }
|
||||
actix-web = { workspace = true, default-features = true }
|
||||
actix-web = { workspace = true, default-features = false }
|
||||
futures = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
@@ -20,7 +20,7 @@ leptos_integration_utils = { workspace = true }
|
||||
leptos_macro = { workspace = true, features = ["actix"] }
|
||||
leptos_meta = { workspace = true, features = ["nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["actix"] }
|
||||
server_fn = { workspace = true, features = ["actix-no-default"] }
|
||||
tachys = { workspace = true }
|
||||
serde_json = { workspace = true , default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
@@ -33,6 +33,8 @@ dashmap = { workspace = true, default-features = true }
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
default = ["actix-default"]
|
||||
actix-default = ["actix-web/default"]
|
||||
islands-router = ["tachys/islands"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
|
||||
@@ -282,6 +282,7 @@ pub fn redirect(path: &str) {
|
||||
/// // call ServerFn::register() for each of the server functions you've defined
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(feature = "default")]
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// // make sure you actually register your server functions
|
||||
@@ -297,6 +298,8 @@ pub fn redirect(path: &str) {
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -442,6 +445,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(feature = "default")]
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -461,6 +465,8 @@ pub fn handle_server_fns_with_context(
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -499,6 +505,7 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(feature = "default")]
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -521,6 +528,9 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -557,6 +567,7 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(feature = "default")]
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -576,6 +587,8 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
|
||||
@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
version = "0.8.2"
|
||||
version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -13,7 +13,7 @@ any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
axum = { default-features = false, features = [
|
||||
"matched-path",
|
||||
] , workspace = true }
|
||||
], workspace = true }
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
@@ -24,14 +24,17 @@ leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tachys = { workspace = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
tokio = { default-features = false , workspace = true }
|
||||
tower = { features = ["util"] , workspace = true, default-features = true }
|
||||
tokio = { default-features = false, workspace = true }
|
||||
tower = { features = ["util"], workspace = true, default-features = true }
|
||||
tower-http = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
tracing = { optional = true, workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = { workspace = true, default-features = true }
|
||||
tokio = { features = ["net", "rt-multi-thread"] , workspace = true, default-features = true }
|
||||
tokio = { features = [
|
||||
"net",
|
||||
"rt-multi-thread",
|
||||
], workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
|
||||
@@ -111,39 +111,28 @@ where
|
||||
|
||||
let on_submit = {
|
||||
move |ev: SubmitEvent| {
|
||||
// request_animation_frame here schedules this event handler to run slightly later
|
||||
// this means that this `submit` handler will run *after* any other `submit` handlers
|
||||
// that have been added by the user. this is useful because it means that the user can
|
||||
// add an `on:submit` handler and call `ev.prevent_default()` to prevent the form submission
|
||||
//
|
||||
// without this delay, this handler will always run before the user's handler (which was added
|
||||
// later), which means the user can't prevent the form submission in the same way
|
||||
//
|
||||
// see https://github.com/leptos-rs/leptos/issues/3872
|
||||
request_animation_frame(move || {
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.prevent_default();
|
||||
ev.prevent_default();
|
||||
|
||||
match ServFn::from_event(&ev) {
|
||||
Ok(new_input) => {
|
||||
action.dispatch(new_input);
|
||||
}
|
||||
Err(err) => {
|
||||
crate::logging::error!(
|
||||
"Error converting form field into server function \
|
||||
arguments: {err:?}"
|
||||
);
|
||||
value.set(Some(Err(ServerFnErrorErr::Serialization(
|
||||
err.to_string(),
|
||||
)
|
||||
.into_app_error())));
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
match ServFn::from_event(&ev) {
|
||||
Ok(new_input) => {
|
||||
action.dispatch(new_input);
|
||||
}
|
||||
});
|
||||
Err(err) => {
|
||||
crate::logging::error!(
|
||||
"Error converting form field into server function \
|
||||
arguments: {err:?}"
|
||||
);
|
||||
value.set(Some(Err(ServerFnErrorErr::Serialization(
|
||||
err.to_string(),
|
||||
)
|
||||
.into_app_error())));
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
type Owned = View<T::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
|
||||
const EXISTS: bool = <T as RenderHtml>::EXISTS;
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self.inner.resolve().await
|
||||
@@ -107,9 +108,14 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
let vm = self.view_marker.to_owned();
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = if option_env!("LEPTOS_WATCH").is_some() {
|
||||
self.view_marker.to_owned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
|
||||
}
|
||||
@@ -122,7 +128,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_str(&format!("<!--hot-reload|{vm}|close-->"));
|
||||
}
|
||||
@@ -138,9 +144,14 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
let vm = self.view_marker.to_owned();
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = if option_env!("LEPTOS_WATCH").is_some() {
|
||||
self.view_marker.to_owned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_sync(&format!("<!--hot-reload|{vm}|open-->"));
|
||||
}
|
||||
@@ -153,7 +164,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))]
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_sync(&format!("<!--hot-reload|{vm}|close-->"));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use tachys::prelude::IntoAttributeValue;
|
||||
|
||||
/// Describes a value that is either a static or a reactive string, i.e.,
|
||||
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
|
||||
/// a [`String`], a [`&str`], a `Signal` or a reactive `Fn() -> String`.
|
||||
#[derive(Clone)]
|
||||
pub struct TextProp(Arc<dyn Fn() -> Oco<'static, str> + Send + Sync>);
|
||||
|
||||
@@ -82,3 +82,93 @@ impl IntoAttributeValue for TextProp {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! textprop_reactive {
|
||||
($name:ident, <$($gen:ident),*>, $v:ty, $( $where_clause:tt )*) =>
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
impl<$($gen),*> From<$name<$($gen),*>> for TextProp
|
||||
where
|
||||
$v: Into<Oco<'static, str>> + Clone + Send + Sync + 'static,
|
||||
$($where_clause)*
|
||||
{
|
||||
#[inline(always)]
|
||||
fn from(s: $name<$($gen),*>) -> Self {
|
||||
TextProp(Arc::new(move || s.get().into()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
mod stable {
|
||||
use super::TextProp;
|
||||
use oco_ref::Oco;
|
||||
#[allow(deprecated)]
|
||||
use reactive_graph::wrappers::read::MaybeSignal;
|
||||
use reactive_graph::{
|
||||
computed::{ArcMemo, Memo},
|
||||
owner::Storage,
|
||||
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
|
||||
traits::Get,
|
||||
wrappers::read::{ArcSignal, Signal},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
textprop_reactive!(
|
||||
RwSignal,
|
||||
<V, S>,
|
||||
V,
|
||||
RwSignal<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(
|
||||
ReadSignal,
|
||||
<V, S>,
|
||||
V,
|
||||
ReadSignal<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(
|
||||
Memo,
|
||||
<V, S>,
|
||||
V,
|
||||
Memo<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(
|
||||
Signal,
|
||||
<V, S>,
|
||||
V,
|
||||
Signal<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(
|
||||
MaybeSignal,
|
||||
<V, S>,
|
||||
V,
|
||||
MaybeSignal<V, S>: Get<Value = V>,
|
||||
S: Storage<V> + Storage<Option<V>>,
|
||||
S: Send + Sync + 'static,
|
||||
);
|
||||
textprop_reactive!(ArcRwSignal, <V>, V, ArcRwSignal<V>: Get<Value = V>);
|
||||
textprop_reactive!(ArcReadSignal, <V>, V, ArcReadSignal<V>: Get<Value = V>);
|
||||
textprop_reactive!(ArcMemo, <V>, V, ArcMemo<V>: Get<Value = V>);
|
||||
textprop_reactive!(ArcSignal, <V>, V, ArcSignal<V>: Get<Value = V>);
|
||||
}
|
||||
|
||||
/// Extension trait for `Option<TextProp>`
|
||||
pub trait OptionTextPropExt {
|
||||
/// Accesses the current value of the `Option<TextProp>` as an `Option<Oco<'static, str>>`.
|
||||
fn get(&self) -> Option<Oco<'static, str>>;
|
||||
}
|
||||
|
||||
impl OptionTextPropExt for Option<TextProp> {
|
||||
fn get(&self) -> Option<Oco<'static, str>> {
|
||||
self.as_ref().map(|text_prop| text_prop.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,19 @@ macro_rules! error {
|
||||
($($t:tt)*) => ($crate::logging::console_error(&format_args!($($t)*).to_string()))
|
||||
}
|
||||
|
||||
/// Uses `println!()`-style formatting to log something to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser), but only if it's a debug build.
|
||||
#[macro_export]
|
||||
macro_rules! debug_log {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
if cfg!(debug_assertions) {
|
||||
$crate::log!($($x)*)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses `println!()`-style formatting to log warnings to the console (in the browser)
|
||||
/// or via `eprintln!()` (if not in the browser), but only if it's a debug build.
|
||||
#[macro_export]
|
||||
@@ -36,6 +49,19 @@ macro_rules! debug_warn {
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses `println!()`-style formatting to log errors to the console (in the browser)
|
||||
/// or via `eprintln!()` (if not in the browser), but only if it's a debug build.
|
||||
#[macro_export]
|
||||
macro_rules! debug_error {
|
||||
($($x:tt)*) => {
|
||||
{
|
||||
if cfg!(debug_assertions) {
|
||||
$crate::error!($($x)*)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn log_to_stdout() -> bool {
|
||||
cfg!(not(all(
|
||||
target_arch = "wasm32",
|
||||
@@ -55,7 +81,7 @@ pub fn console_log(s: &str) {
|
||||
}
|
||||
|
||||
/// Log a warning to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
/// or via `eprintln!()` (if not in the browser).
|
||||
pub fn console_warn(s: &str) {
|
||||
if log_to_stdout() {
|
||||
eprintln!("{s}");
|
||||
@@ -65,7 +91,7 @@ pub fn console_warn(s: &str) {
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
/// or via `eprintln!()` (if not in the browser).
|
||||
#[inline(always)]
|
||||
pub fn console_error(s: &str) {
|
||||
if log_to_stdout() {
|
||||
@@ -75,21 +101,29 @@ pub fn console_error(s: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// Log a string to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser), but only in a debug build.
|
||||
#[inline(always)]
|
||||
pub fn console_debug_warn(s: &str) {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if log_to_stdout() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = s;
|
||||
pub fn console_debug_log(s: &str) {
|
||||
if cfg!(debug_assertions) {
|
||||
console_log(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a warning to the console (in the browser)
|
||||
/// or via `eprintln!()` (if not in the browser), but only in a debug build.
|
||||
#[inline(always)]
|
||||
pub fn console_debug_warn(s: &str) {
|
||||
if cfg!(debug_assertions) {
|
||||
console_warn(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// or via `eprintln!()` (if not in the browser), but only in a debug build.
|
||||
#[inline(always)]
|
||||
pub fn console_debug_error(s: &str) {
|
||||
if cfg!(debug_assertions) {
|
||||
console_error(s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,93 +251,67 @@ impl LNode {
|
||||
action: PatchAction::ClearChildren,
|
||||
}]
|
||||
} else {
|
||||
let mut a = 0;
|
||||
let mut b = std::cmp::max(old.len(), new.len()) - 1; // min is 0, have checked both have items
|
||||
let width = old.len() + 1;
|
||||
let height = new.len() + 1;
|
||||
let mut mat = vec![0; width * height];
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 1..width {
|
||||
mat[i] = i;
|
||||
}
|
||||
for i in 1..height {
|
||||
mat[i * width] = i;
|
||||
}
|
||||
for j in 1..height {
|
||||
for i in 1..width {
|
||||
if old[i - 1] == new[j - 1] {
|
||||
mat[j * width + i] = mat[(j - 1) * width + (i - 1)];
|
||||
} else {
|
||||
mat[j * width + i] = (mat[(j - 1) * width + i] + 1)
|
||||
.min(mat[j * width + (i - 1)] + 1)
|
||||
.min(mat[(j - 1) * width + (i - 1)] + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
let (mut i, mut j) = (old.len(), new.len());
|
||||
let mut patches = vec![];
|
||||
// common prefix
|
||||
while a < b {
|
||||
let old = old.get(a);
|
||||
let new = new.get(a);
|
||||
|
||||
match (old, new) {
|
||||
(None, Some(new)) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::InsertChild {
|
||||
before: a,
|
||||
child: new.to_replacement_node(old_children),
|
||||
},
|
||||
}),
|
||||
(Some(_), None) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::RemoveChild { at: a },
|
||||
}),
|
||||
(Some(old), Some(new)) if old != new => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
a += 1;
|
||||
}
|
||||
|
||||
// common suffix
|
||||
while b >= a {
|
||||
let old = old.get(b);
|
||||
let new = new.get(b);
|
||||
|
||||
match (old, new) {
|
||||
(None, Some(new)) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::InsertChildAfter {
|
||||
after: b - 1,
|
||||
child: new.to_replacement_node(old_children),
|
||||
},
|
||||
}),
|
||||
(Some(_), None) => patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::RemoveChild { at: b },
|
||||
}),
|
||||
(Some(old), Some(new)) if old != new => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if b == 0 {
|
||||
break;
|
||||
}
|
||||
b -= 1;
|
||||
}
|
||||
|
||||
// diffing in middle
|
||||
if b >= a {
|
||||
let old_slice_end =
|
||||
if b >= old.len() { old.len() - 1 } else { b };
|
||||
let new_slice_end =
|
||||
if b >= new.len() { new.len() - 1 } else { b };
|
||||
let old = &old[a..=old_slice_end];
|
||||
let new = &new[a..=new_slice_end];
|
||||
|
||||
for (new_idx, new_node) in new.iter().enumerate() {
|
||||
match old.get(new_idx) {
|
||||
Some(old_node) => {
|
||||
let mut new_path = path.to_vec();
|
||||
new_path.push(new_idx + a);
|
||||
let diffs = old_node.diff_at(
|
||||
new_node,
|
||||
&new_path,
|
||||
old_children,
|
||||
);
|
||||
patches.extend(&mut diffs.into_iter());
|
||||
}
|
||||
None => patches.push(Patch {
|
||||
while i > 0 || j > 0 {
|
||||
if i > 0 && j > 0 && old[i - 1] == new[j - 1] {
|
||||
i -= 1;
|
||||
j -= 1;
|
||||
} else {
|
||||
let current = mat[j * width + i];
|
||||
if i > 0
|
||||
&& j > 0
|
||||
&& mat[(j - 1) * width + i - 1] + 1 == current
|
||||
{
|
||||
let mut new_path = path.to_owned();
|
||||
new_path.push(i - 1);
|
||||
let diffs = old[i - 1].diff_at(
|
||||
&new[j - 1],
|
||||
&new_path,
|
||||
old_children,
|
||||
);
|
||||
patches.extend(&mut diffs.into_iter());
|
||||
i -= 1;
|
||||
j -= 1;
|
||||
} else if i > 0 && mat[j * width + i - 1] + 1 == current {
|
||||
patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::RemoveChild { at: i - 1 },
|
||||
});
|
||||
i -= 1;
|
||||
} else if j > 0 && mat[(j - 1) * width + i] + 1 == current {
|
||||
patches.push(Patch {
|
||||
path: path.to_owned(),
|
||||
action: PatchAction::InsertChild {
|
||||
before: new_idx,
|
||||
child: new_node
|
||||
before: i,
|
||||
child: new[j - 1]
|
||||
.to_replacement_node(old_children),
|
||||
},
|
||||
}),
|
||||
});
|
||||
j -= 1;
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -514,23 +488,17 @@ mod tests {
|
||||
let delta = a.diff(&b);
|
||||
assert_eq!(
|
||||
delta,
|
||||
vec![
|
||||
Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::InsertChildAfter {
|
||||
after: 0,
|
||||
child: ReplacementNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![ReplacementNode::Html("bar".into())]
|
||||
}
|
||||
vec![Patch {
|
||||
path: vec![],
|
||||
action: PatchAction::InsertChild {
|
||||
before: 0,
|
||||
child: ReplacementNode::Element {
|
||||
name: "button".into(),
|
||||
attrs: vec![],
|
||||
children: vec![ReplacementNode::Html("foo".into())]
|
||||
}
|
||||
},
|
||||
Patch {
|
||||
path: vec![0, 0],
|
||||
action: PatchAction::SetText("foo".into())
|
||||
}
|
||||
]
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,10 @@ impl ViewMacros {
|
||||
}
|
||||
diffs
|
||||
} else {
|
||||
// TODO: instead of simply returning no patches, when number of views differs,
|
||||
// we can compare views content to determine which views were shifted
|
||||
// or come up with another idea that will allow to send patches when views were shifted/removed/added
|
||||
lock.insert(path.clone(), new_views);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ impl LNode {
|
||||
LNode::parse_node(child, views)?;
|
||||
}
|
||||
}
|
||||
Node::RawText(text) => {
|
||||
views.push(LNode::Text(text.to_string_best()));
|
||||
}
|
||||
Node::Text(text) => {
|
||||
views.push(LNode::Text(text.value_string()));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
console.log("[HOT RELOADING] Connected to server.\n\nNote: `cargo-leptos watch --hot-reload` only works with the `nightly` feature enabled on Leptos.");
|
||||
function patch(json) {
|
||||
try {
|
||||
const views = JSON.parse(json);
|
||||
for (const [id, patches] of views) {
|
||||
console.log("[HOT RELOAD]", id, patches);
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_COMMENT,
|
||||
),
|
||||
open = `hot-reload|${id}|open`,
|
||||
close = `hot-reload|${id}|close`;
|
||||
let start, end;
|
||||
@@ -21,150 +23,200 @@ function patch(json) {
|
||||
}
|
||||
|
||||
for (const [start, end] of instances) {
|
||||
// build tree of current actual children
|
||||
const actualChildren = childrenFromRange(start.parentElement, start, end);
|
||||
const actions = [];
|
||||
|
||||
// build up the set of actions
|
||||
for (const patch of patches) {
|
||||
const actualChildren = childrenFromRange(
|
||||
start.parentElement,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
const child = childAtPath(
|
||||
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
|
||||
patch.path
|
||||
actualChildren.length > 1
|
||||
? { children: actualChildren }
|
||||
: actualChildren[0],
|
||||
patch.path,
|
||||
);
|
||||
const action = patch.action;
|
||||
if (action == "ClearChildren") {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > ClearChildren", child.node);
|
||||
console.log("[HOT RELOAD] > ClearChildren", child.node);
|
||||
if (child.node) {
|
||||
child.node.textContent = "";
|
||||
});
|
||||
} else if (action.ReplaceWith) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > ReplaceWith", child, action.ReplaceWith);
|
||||
const replacement = fromReplacementNode(action.ReplaceWith, actualChildren);
|
||||
if (child.node) {
|
||||
child.node.replaceWith(replacement);
|
||||
} else {
|
||||
const range = new Range();
|
||||
range.setStartAfter(child.start);
|
||||
range.setEndAfter(child.end);
|
||||
range.deleteContents();
|
||||
child.start.replaceWith(replacement);
|
||||
} else {
|
||||
for (const existingChild of child.children) {
|
||||
let parent = existingChild.node.parentElement;
|
||||
parent.removeChild(existingChild.node);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (action.ReplaceWith) {
|
||||
console.log(
|
||||
"[HOT RELOAD] > ReplaceWith",
|
||||
child,
|
||||
action.ReplaceWith,
|
||||
);
|
||||
const replacement = fromReplacementNode(
|
||||
action.ReplaceWith,
|
||||
actualChildren,
|
||||
);
|
||||
if (child.node) {
|
||||
child.node.replaceWith(replacement);
|
||||
} else {
|
||||
if (child.children) {
|
||||
child.children[0].node.parentElement.insertBefore(
|
||||
replacement,
|
||||
child.children[0].node,
|
||||
);
|
||||
for (const existingChild of child.children) {
|
||||
existingChild.node.parentElement.removeChild(
|
||||
existingChild.node,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (action.ChangeTagName) {
|
||||
const oldNode = child.node;
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > ChangeTagName", child.node, action.ChangeTagName);
|
||||
const newElement = document.createElement(action.ChangeTagName);
|
||||
for (const attr of oldNode.attributes) {
|
||||
newElement.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
for (const childNode of child.node.childNodes) {
|
||||
newElement.appendChild(childNode);
|
||||
}
|
||||
console.log(
|
||||
"[HOT RELOAD] > ChangeTagName",
|
||||
child.node,
|
||||
action.ChangeTagName,
|
||||
);
|
||||
const newElement = document.createElement(action.ChangeTagName);
|
||||
for (const attr of oldNode.attributes) {
|
||||
newElement.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
for (const childNode of child.node.childNodes) {
|
||||
newElement.appendChild(childNode);
|
||||
}
|
||||
|
||||
child.node.replaceWith(newElement);
|
||||
});
|
||||
child.node.replaceWith(newElement);
|
||||
} else if (action.RemoveAttribute) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > RemoveAttribute", child.node, action.RemoveAttribute);
|
||||
child.node.removeAttribute(action.RemoveAttribute);
|
||||
});
|
||||
console.log(
|
||||
"[HOT RELOAD] > RemoveAttribute",
|
||||
child.node,
|
||||
action.RemoveAttribute,
|
||||
);
|
||||
child.node.removeAttribute(action.RemoveAttribute);
|
||||
} else if (action.SetAttribute) {
|
||||
const [name, value] = action.SetAttribute;
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > SetAttribute", child.node, action.SetAttribute);
|
||||
child.node.setAttribute(name, value);
|
||||
});
|
||||
console.log(
|
||||
"[HOT RELOAD] > SetAttribute",
|
||||
child.node,
|
||||
action.SetAttribute,
|
||||
);
|
||||
child.node.setAttribute(name, value);
|
||||
} else if (action.SetText) {
|
||||
const node = child.node;
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > SetText", child.node, action.SetText);
|
||||
node.textContent = action.SetText;
|
||||
});
|
||||
console.log("[HOT RELOAD] > SetText", child.node, action.SetText);
|
||||
node.textContent = action.SetText;
|
||||
} else if (action.AppendChildren) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > AppendChildren", child.node, action.AppendChildren);
|
||||
const newChildren = fromReplacementNode(action.AppendChildren, actualChildren);
|
||||
child.node.append(newChildren);
|
||||
});
|
||||
console.log(
|
||||
"[HOT RELOAD] > AppendChildren",
|
||||
child.node,
|
||||
action.AppendChildren,
|
||||
);
|
||||
const newChildren = action.AppendChildren.map((x) =>
|
||||
fromReplacementNode(x, actualChildren),
|
||||
);
|
||||
child.node.append(...newChildren);
|
||||
} else if (action.RemoveChild) {
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > RemoveChild", child.node, child.children, action.RemoveChild);
|
||||
const toRemove = child.children[action.RemoveChild.at];
|
||||
let toRemoveNode = toRemove.node;
|
||||
if (!toRemoveNode) {
|
||||
const range = new Range();
|
||||
range.setStartBefore(toRemove.start);
|
||||
range.setEndAfter(toRemove.end);
|
||||
toRemoveNode = range.deleteContents();
|
||||
} else {
|
||||
toRemoveNode.parentNode.removeChild(toRemoveNode);
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
"[HOT RELOAD] > RemoveChild",
|
||||
child.node,
|
||||
child.children,
|
||||
action.RemoveChild,
|
||||
);
|
||||
const toRemove = child.children[action.RemoveChild.at];
|
||||
let toRemoveNode = toRemove.node;
|
||||
if (!toRemoveNode) {
|
||||
const range = new Range();
|
||||
range.setStartBefore(toRemove.start);
|
||||
range.setEndAfter(toRemove.end);
|
||||
toRemoveNode = range.deleteContents();
|
||||
} else {
|
||||
toRemoveNode.parentNode.removeChild(toRemoveNode);
|
||||
}
|
||||
} else if (action.InsertChild) {
|
||||
const newChild = fromReplacementNode(action.InsertChild.child, actualChildren);
|
||||
const newChild = fromReplacementNode(
|
||||
action.InsertChild.child,
|
||||
actualChildren,
|
||||
);
|
||||
let children = [];
|
||||
if (child.children) {
|
||||
children = child.children;
|
||||
} else if (child.start && child.end) {
|
||||
children = childrenFromRange(child.node || child.start.parentElement, start, end);
|
||||
children = childrenFromRange(
|
||||
child.node || child.start.parentElement,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
} else {
|
||||
console.warn("InsertChildAfter could not build children.");
|
||||
}
|
||||
const before = children[action.InsertChild.before];
|
||||
actions.push(() => {
|
||||
console.log("[HOT RELOAD] > InsertChild", child, child.node, action.InsertChild, " before ", before);
|
||||
if (!before && child.node) {
|
||||
child.node.appendChild(newChild);
|
||||
} else {
|
||||
let node = child.node || child.end.parentElement;
|
||||
const reference = before ? before.node || before.start : child.end;
|
||||
node.insertBefore(newChild, reference);
|
||||
}
|
||||
});
|
||||
const beforeNode = children[action.InsertChild.before];
|
||||
console.log(
|
||||
"[HOT RELOAD] > InsertChild",
|
||||
child,
|
||||
child.node,
|
||||
action.InsertChild,
|
||||
" before ",
|
||||
beforeNode,
|
||||
);
|
||||
if (beforeNode) {
|
||||
let node = beforeNode.node || beforeNode.start.previousSibling;
|
||||
node.parentElement.insertBefore(newChild, node);
|
||||
} else if (child.node) {
|
||||
child.node.appendChild(newChild);
|
||||
} else if (children) {
|
||||
let lastNode = children[children.length - 1];
|
||||
let afterNode = lastNode.node || lastNode.end.nextSibling;
|
||||
afterNode.after(newChild);
|
||||
}
|
||||
} else if (action.InsertChildAfter) {
|
||||
const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren);
|
||||
const newChild = fromReplacementNode(
|
||||
action.InsertChildAfter.child,
|
||||
actualChildren,
|
||||
);
|
||||
let children = [];
|
||||
if (child.children) {
|
||||
children = child.children;
|
||||
} else if (child.start && child.end) {
|
||||
children = childrenFromRange(child.node || child.start.parentElement, start, end);
|
||||
children = childrenFromRange(
|
||||
child.node || child.start.parentElement,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
} else {
|
||||
console.warn("InsertChildAfter could not build children.");
|
||||
}
|
||||
const after = children[action.InsertChildAfter.after];
|
||||
actions.push(() => {
|
||||
console.log(
|
||||
"[HOT RELOAD] > InsertChildAfter",
|
||||
child,
|
||||
child.node,
|
||||
action.InsertChildAfter,
|
||||
" after ",
|
||||
after
|
||||
);
|
||||
if (child.node && (!after || !(after.node || after.start).nextSibling)) {
|
||||
child.node.appendChild(newChild);
|
||||
console.log(
|
||||
"[HOT RELOAD] > InsertChildAfter",
|
||||
child,
|
||||
child.node,
|
||||
action.InsertChildAfter,
|
||||
" after ",
|
||||
after,
|
||||
);
|
||||
if (
|
||||
child.node &&
|
||||
(!after || !(after.node || after.start).nextSibling)
|
||||
) {
|
||||
child.node.appendChild(newChild);
|
||||
} else {
|
||||
const node = child.node || child.end;
|
||||
const parent =
|
||||
node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
|
||||
if (!after) {
|
||||
parent.appendChild(newChild);
|
||||
} else {
|
||||
const node = child.node || child.end;
|
||||
const parent = node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
|
||||
if (!after) {
|
||||
parent.appendChild(newChild);
|
||||
} else {
|
||||
parent.insertBefore(newChild, (after.node || after.start).nextSibling);
|
||||
}
|
||||
parent.insertBefore(
|
||||
newChild,
|
||||
(after.node || after.start).nextSibling,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Unmatched action", action);
|
||||
}
|
||||
}
|
||||
|
||||
// actually run the actions
|
||||
// the reason we delay them is so that children aren't moved before other children are found, etc.
|
||||
for (const action of actions) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -191,8 +243,10 @@ function patch(json) {
|
||||
return element;
|
||||
} else {
|
||||
const child = childAtPath(
|
||||
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
|
||||
node.Path
|
||||
actualChildren.length > 1
|
||||
? { children: actualChildren }
|
||||
: actualChildren[0],
|
||||
node.Path,
|
||||
);
|
||||
if (child) {
|
||||
let childNode = child.node;
|
||||
@@ -215,7 +269,10 @@ function patch(json) {
|
||||
}
|
||||
return childNode;
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Could not find replacement node at ", node.Path);
|
||||
console.warn(
|
||||
"[HOT RELOADING] Could not find replacement node at ",
|
||||
node.Path,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -227,13 +284,16 @@ function patch(json) {
|
||||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT,
|
||||
{
|
||||
acceptNode(node) {
|
||||
if (node.parentNode == element && (!range || range.isPointInRange(node, 0))) {
|
||||
if (
|
||||
node.parentNode == element &&
|
||||
(!range || range.isPointInRange(node, 0))
|
||||
) {
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
} else {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
const actualChildren = [],
|
||||
elementCount = {};
|
||||
@@ -259,18 +319,22 @@ function patch(json) {
|
||||
node: walker.currentNode,
|
||||
});
|
||||
} else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
|
||||
if (walker.currentNode.textContent.trim().startsWith("hot-reload")) {
|
||||
if (walker.currentNode.textContent.trim().endsWith("-children|open")) {
|
||||
if (walker.currentNode.textContent.trim().startsWith("hot-reload|")) {
|
||||
if (walker.currentNode.textContent.trim().endsWith("|open")) {
|
||||
const startingName = walker.currentNode.textContent.trim();
|
||||
const componentName = startingName.replace("-children|open").replace("hot-reload|");
|
||||
const endingName = `hot-reload|${componentName}-children|close`;
|
||||
const componentName = startingName
|
||||
.replace("|open", "")
|
||||
.replace("hot-reload|", "");
|
||||
const endingName = `hot-reload|${componentName}|close`;
|
||||
let start = walker.currentNode;
|
||||
let depth = 1;
|
||||
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.trim() == endingName) {
|
||||
depth--;
|
||||
} else if (walker.currentNode.textContent.trim() == startingName) {
|
||||
} else if (
|
||||
walker.currentNode.textContent.trim() == startingName
|
||||
) {
|
||||
depth++;
|
||||
}
|
||||
|
||||
@@ -283,7 +347,11 @@ function patch(json) {
|
||||
type: "fragment",
|
||||
start: start.nextSibling,
|
||||
end: end.previousSibling,
|
||||
children: childrenFromRange(start.parentElement, start.nextSibling, end.previousSibling),
|
||||
children: childrenFromRange(
|
||||
start.parentElement,
|
||||
start.nextSibling,
|
||||
end.previousSibling,
|
||||
),
|
||||
});
|
||||
}
|
||||
} else if (walker.currentNode.textContent.trim() == "<() />") {
|
||||
@@ -358,7 +426,10 @@ function patch(json) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Building children, encountered", walker.currentNode);
|
||||
console.warn(
|
||||
"[HOT RELOADING] Building children, encountered",
|
||||
walker.currentNode,
|
||||
);
|
||||
}
|
||||
}
|
||||
return actualChildren;
|
||||
@@ -374,7 +445,11 @@ function patch(json) {
|
||||
} else if (path == [0]) {
|
||||
return element;
|
||||
} else if (element.start && element.end) {
|
||||
const actualChildren = childrenFromRange(element.node || element.start.parentElement, element.start, element.end);
|
||||
const actualChildren = childrenFromRange(
|
||||
element.node || element.start.parentElement,
|
||||
element.start,
|
||||
element.end,
|
||||
);
|
||||
return childAtPath({ children: actualChildren }, path);
|
||||
} else {
|
||||
console.warn("[HOT RELOADING] Child at ", path, "not found in ", element);
|
||||
|
||||
@@ -358,16 +358,14 @@ fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {
|
||||
}
|
||||
|
||||
fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))] {
|
||||
Some(leptos_hot_reload::span_to_stable_id(
|
||||
site.file(),
|
||||
site.start().line()
|
||||
))
|
||||
} else {
|
||||
_ = site;
|
||||
None
|
||||
}
|
||||
if cfg!(debug_assertions) {
|
||||
Some(leptos_hot_reload::span_to_stable_id(
|
||||
site.file(),
|
||||
site.start().line(),
|
||||
))
|
||||
} else {
|
||||
_ = site;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ pub fn render_view(
|
||||
view_marker: Option<String>,
|
||||
disable_inert_html: bool,
|
||||
) -> Option<TokenStream> {
|
||||
let disable_inert_html = disable_inert_html || global_class.is_some();
|
||||
|
||||
let (base, should_add_view) = match nodes.len() {
|
||||
0 => {
|
||||
let span = Span::call_site();
|
||||
@@ -401,6 +403,9 @@ fn inert_element_to_tokens(
|
||||
}
|
||||
}
|
||||
|
||||
/// # Note
|
||||
/// Should not be used on top level `<svg>` elements.
|
||||
/// Use [`inert_element_to_tokens`] instead.
|
||||
fn inert_svg_element_to_tokens(
|
||||
node: &Node<impl CustomNode>,
|
||||
escape_text: bool,
|
||||
@@ -704,7 +709,7 @@ fn node_to_tokens(
|
||||
&& el_name != "textarea";
|
||||
|
||||
let el_name = el_node.name().to_string();
|
||||
if is_svg_element(&el_name) {
|
||||
if is_svg_element(&el_name) && el_name != "svg" {
|
||||
Some(inert_svg_element_to_tokens(
|
||||
node,
|
||||
escape,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -13,8 +13,8 @@ leptos = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
indexmap = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true , default-features = true }
|
||||
tracing = { optional = true, workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@@ -413,6 +413,7 @@ where
|
||||
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
const EXISTS: bool = false;
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
self.el.dry_resolve()
|
||||
|
||||
@@ -322,6 +322,7 @@ impl RenderHtml for TitleView {
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
const EXISTS: bool = false;
|
||||
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.2.2"
|
||||
version = "0.2.4"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -16,19 +16,26 @@ futures = { workspace = true, default-features = true }
|
||||
hydration_context = { workspace = true, optional = true }
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
rustc-hash = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"], optional = true , workspace = true, default-features = true }
|
||||
serde = { features = [
|
||||
"derive",
|
||||
], optional = true, workspace = true, default-features = true }
|
||||
slotmap = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
thiserror = { workspace = true, default-features = true }
|
||||
tracing = { optional = true, workspace = true, default-features = true }
|
||||
guardian = { workspace = true, default-features = true }
|
||||
async-lock = { workspace = true, default-features = true }
|
||||
send_wrapper = { features = ["futures"] , workspace = true, default-features = true }
|
||||
send_wrapper = { features = [
|
||||
"futures",
|
||||
], workspace = true, default-features = true }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
web-sys = { version = "0.3.77", features = ["console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
|
||||
tokio = { features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
], workspace = true, default-features = true }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.2.2"
|
||||
version = "0.2.4"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -11,7 +11,7 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
guardian = { workspace = true, default-features = true }
|
||||
itertools = { workspace = true , default-features = true }
|
||||
itertools = { workspace = true, default-features = true }
|
||||
or_poisoned = { workspace = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
reactive_graph = { workspace = true }
|
||||
@@ -21,7 +21,10 @@ dashmap = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
|
||||
tokio = { features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
], workspace = true, default-features = true }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
reactive_graph = { workspace = true, features = ["effects"] }
|
||||
|
||||
@@ -1105,11 +1105,6 @@ mod tests {
|
||||
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
|
||||
}
|
||||
|
||||
#[derive(Debug, Store)]
|
||||
pub struct StructWithOption {
|
||||
opt_field: Option<Todo>,
|
||||
}
|
||||
|
||||
// regression test for https://github.com/leptos-rs/leptos/issues/3523
|
||||
#[tokio::test]
|
||||
async fn notifying_all_descendants() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores_macro"
|
||||
version = "0.2.2"
|
||||
version = "0.2.4"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -13,8 +13,8 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
convert_case = { workspace = true , default-features = true }
|
||||
convert_case = { workspace = true, default-features = true }
|
||||
proc-macro-error2 = { workspace = true, default-features = true }
|
||||
proc-macro2 = { workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
syn = { features = ["full"] , workspace = true, default-features = true }
|
||||
syn = { features = ["full"], workspace = true, default-features = true }
|
||||
|
||||
@@ -111,10 +111,8 @@ impl ToTokens for Model {
|
||||
} = &self;
|
||||
let any_store_field = Ident::new("AnyStoreField", Span::call_site());
|
||||
let trait_name = Ident::new(&format!("{name}StoreFields"), name.span());
|
||||
let generics_with_orig = {
|
||||
let params = &generics.params;
|
||||
quote! { <#any_store_field, #params> }
|
||||
};
|
||||
let params = &generics.params;
|
||||
let generics_with_orig = quote! { <#any_store_field, #params> };
|
||||
let where_with_orig = {
|
||||
generics
|
||||
.where_clause
|
||||
@@ -140,13 +138,13 @@ impl ToTokens for Model {
|
||||
|
||||
// read access
|
||||
tokens.extend(quote! {
|
||||
#vis trait #trait_name <AnyStoreField>
|
||||
#vis trait #trait_name <AnyStoreField, #params>
|
||||
#where_with_orig
|
||||
{
|
||||
#(#trait_fields)*
|
||||
}
|
||||
|
||||
impl #generics_with_orig #trait_name <AnyStoreField> for AnyStoreField
|
||||
impl #generics_with_orig #trait_name <AnyStoreField, #params> for AnyStoreField
|
||||
#where_with_orig
|
||||
{
|
||||
#(#read_fields)*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -20,11 +20,11 @@ tachys = { workspace = true, features = ["reactive_graph"] }
|
||||
futures = { workspace = true, default-features = true }
|
||||
url = { workspace = true, default-features = true }
|
||||
js-sys = { workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true , default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, default-features = true }
|
||||
tracing = { optional = true, workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
percent-encoding = { optional = true , workspace = true, default-features = true }
|
||||
thiserror = { workspace = true, default-features = true }
|
||||
percent-encoding = { optional = true, workspace = true, default-features = true }
|
||||
gloo-net = { workspace = true, default-features = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@@ -67,10 +67,32 @@ impl Url {
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> &str {
|
||||
#[cfg(all(feature = "ssr", any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!(
|
||||
"Reading hash on the server can lead to hydration errors."
|
||||
);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!(
|
||||
"Reading hash on the server can lead to hydration errors."
|
||||
);
|
||||
}
|
||||
&self.hash
|
||||
}
|
||||
|
||||
pub fn hash_mut(&mut self) -> &mut String {
|
||||
#[cfg(all(feature = "ssr", any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!(
|
||||
"Reading hash on the server can lead to hydration errors."
|
||||
);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!(
|
||||
"Reading hash on the server can lead to hydration errors."
|
||||
);
|
||||
}
|
||||
&mut self.hash
|
||||
}
|
||||
|
||||
@@ -173,7 +195,7 @@ impl Location {
|
||||
let state = state.into();
|
||||
let pathname = Memo::new(move |_| url.with(|url| url.path.clone()));
|
||||
let search = Memo::new(move |_| url.with(|url| url.search.clone()));
|
||||
let hash = Memo::new(move |_| url.with(|url| url.hash.clone()));
|
||||
let hash = Memo::new(move |_| url.with(|url| url.hash().to_string()));
|
||||
let query =
|
||||
Memo::new(move |_| url.with(|url| url.search_params.clone()));
|
||||
Location {
|
||||
|
||||
@@ -109,7 +109,6 @@ where
|
||||
base,
|
||||
&mut loaders,
|
||||
&mut outlets,
|
||||
&outer_owner,
|
||||
);
|
||||
drop(url);
|
||||
|
||||
@@ -180,7 +179,6 @@ where
|
||||
&mut preloaders,
|
||||
&mut full_loaders,
|
||||
&mut state.outlets,
|
||||
&self.outer_owner,
|
||||
self.set_is_routing.is_some(),
|
||||
0,
|
||||
);
|
||||
@@ -340,7 +338,6 @@ where
|
||||
base,
|
||||
&mut loaders,
|
||||
&mut outlets,
|
||||
&outer_owner,
|
||||
);
|
||||
|
||||
// outlets will not send their views if the loaders are never polled
|
||||
@@ -394,7 +391,6 @@ where
|
||||
base,
|
||||
&mut loaders,
|
||||
&mut outlets,
|
||||
&outer_owner,
|
||||
);
|
||||
|
||||
// outlets will not send their views if the loaders are never polled
|
||||
@@ -448,7 +444,6 @@ where
|
||||
base,
|
||||
&mut loaders,
|
||||
&mut outlets,
|
||||
&outer_owner,
|
||||
);
|
||||
drop(url);
|
||||
|
||||
@@ -483,10 +478,10 @@ pub(crate) struct RouteContext {
|
||||
trigger: ArcTrigger,
|
||||
url: ArcRwSignal<Url>,
|
||||
params: ArcRwSignal<ParamsMap>,
|
||||
owner: Owner,
|
||||
pub matched: ArcRwSignal<String>,
|
||||
base: Option<Oco<'static, str>>,
|
||||
view_fn: Arc<Mutex<OutletViewFn>>,
|
||||
owner: Arc<Mutex<Option<Owner>>>,
|
||||
child: ChildRoute,
|
||||
}
|
||||
|
||||
@@ -500,7 +495,6 @@ impl Debug for RouteContext {
|
||||
.field("trigger", &self.trigger)
|
||||
.field("url", &self.url)
|
||||
.field("params", &self.params)
|
||||
.field("owner", &self.owner.debug_id())
|
||||
.field("matched", &self.matched)
|
||||
.field("base", &self.base)
|
||||
.finish_non_exhaustive()
|
||||
@@ -514,10 +508,10 @@ impl Clone for RouteContext {
|
||||
id: self.id,
|
||||
trigger: self.trigger.clone(),
|
||||
params: self.params.clone(),
|
||||
owner: self.owner.clone(),
|
||||
matched: self.matched.clone(),
|
||||
base: self.base.clone(),
|
||||
view_fn: Arc::clone(&self.view_fn),
|
||||
owner: Arc::clone(&self.owner),
|
||||
child: self.child.clone(),
|
||||
}
|
||||
}
|
||||
@@ -530,7 +524,6 @@ trait AddNestedRoute {
|
||||
base: Option<Oco<'static, str>>,
|
||||
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
parent: &Owner,
|
||||
);
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -540,9 +533,8 @@ trait AddNestedRoute {
|
||||
base: Option<Oco<'static, str>>,
|
||||
items: &mut usize,
|
||||
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
full_loaders: &mut Vec<oneshot::Receiver<()>>,
|
||||
full_loaders: &mut Vec<oneshot::Receiver<Option<Owner>>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
parent: &Owner,
|
||||
set_is_routing: bool,
|
||||
level: u8,
|
||||
) -> u8;
|
||||
@@ -558,15 +550,9 @@ where
|
||||
base: Option<Oco<'static, str>>,
|
||||
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
parent: &Owner,
|
||||
) {
|
||||
let orig_url = url;
|
||||
|
||||
// each Outlet gets its own owner, so it can inherit context from its parent route,
|
||||
// a new owner will be constructed if a different route replaces this one in the outlet,
|
||||
// so that any signals it creates or context it provides will be cleaned up
|
||||
let owner = parent.child();
|
||||
|
||||
// the params signal can be updated to allow the same outlet to update to changes in the
|
||||
// params, even if there's not a route match change
|
||||
let params = ArcRwSignal::new(self.to_params().into_iter().collect());
|
||||
@@ -624,13 +610,13 @@ where
|
||||
url,
|
||||
trigger: trigger.clone(),
|
||||
params,
|
||||
owner: owner.clone(),
|
||||
matched,
|
||||
view_fn: Arc::new(Mutex::new(Box::new(|_owner| {
|
||||
Suspend::new(Box::pin(async { ().into_any() }))
|
||||
}))),
|
||||
base: base.clone(),
|
||||
child: ChildRoute(Arc::new(Mutex::new(None))),
|
||||
owner: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
if !outlets.is_empty() {
|
||||
let prev_index = outlets.len().saturating_sub(1);
|
||||
@@ -646,6 +632,7 @@ where
|
||||
let url = outlet.url.clone();
|
||||
let matched = Matched(matched_including_parents);
|
||||
let view_fn = Arc::clone(&outlet.view_fn);
|
||||
let route_owner = Arc::clone(&outlet.owner);
|
||||
let outlet = outlet.clone();
|
||||
let params = params_including_parents.clone();
|
||||
let url = url.clone();
|
||||
@@ -655,6 +642,8 @@ where
|
||||
let child = outlet.child.clone();
|
||||
*view_fn.lock().or_poisoned() =
|
||||
Box::new(move |owner_where_used| {
|
||||
*route_owner.lock().or_poisoned() =
|
||||
Some(owner_where_used.clone());
|
||||
let view = view.clone();
|
||||
let child = child.clone();
|
||||
let params = params.clone();
|
||||
@@ -696,7 +685,7 @@ where
|
||||
// this is important because to build the view, we need access to the outlet
|
||||
// and the outlet will be returned from building this child
|
||||
if let Some(child) = child {
|
||||
child.build_nested_route(orig_url, base, loaders, outlets, &owner);
|
||||
child.build_nested_route(orig_url, base, loaders, outlets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,9 +696,8 @@ where
|
||||
base: Option<Oco<'static, str>>,
|
||||
items: &mut usize,
|
||||
preloaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
full_loaders: &mut Vec<oneshot::Receiver<()>>,
|
||||
full_loaders: &mut Vec<oneshot::Receiver<Option<Owner>>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
parent: &Owner,
|
||||
set_is_routing: bool,
|
||||
level: u8,
|
||||
) -> u8 {
|
||||
@@ -718,11 +706,17 @@ where
|
||||
.take(*items)
|
||||
.map(|route| (route.params.clone(), route.matched.clone()))
|
||||
.unzip();
|
||||
|
||||
if outlets.get(*items).is_some() && *items > 0 {
|
||||
*outlets[*items - 1].child.0.lock().or_poisoned() =
|
||||
Some(outlets[*items].clone());
|
||||
}
|
||||
|
||||
let current = outlets.get_mut(*items);
|
||||
match current {
|
||||
// if there's nothing currently in the routes at this point, build from here
|
||||
None => {
|
||||
self.build_nested_route(url, base, preloaders, outlets, parent);
|
||||
self.build_nested_route(url, base, preloaders, outlets);
|
||||
level
|
||||
}
|
||||
Some(current) => {
|
||||
@@ -789,9 +783,6 @@ where
|
||||
|
||||
// assign a new owner, so that contexts and signals owned by the previous route
|
||||
// in this outlet can be dropped
|
||||
let mut old_owner =
|
||||
Some(mem::replace(&mut current.owner, parent.child()));
|
||||
let owner = current.owner.clone();
|
||||
let (full_tx, full_rx) = oneshot::channel();
|
||||
let full_tx = Mutex::new(Some(full_tx));
|
||||
full_loaders.push(full_rx);
|
||||
@@ -801,22 +792,24 @@ where
|
||||
// and notify the trigger so that the reactive view inside the Outlet tracking
|
||||
// the trigger runs again
|
||||
preloaders.push(Box::pin(ScopedFuture::new({
|
||||
let owner = owner.clone();
|
||||
let trigger = current.trigger.clone();
|
||||
let url = current.url.clone();
|
||||
let matched = Matched(matched_including_parents);
|
||||
let view_fn = Arc::clone(¤t.view_fn);
|
||||
let route_owner = Arc::clone(¤t.owner);
|
||||
let child = outlet.child.clone();
|
||||
async move {
|
||||
view.preload().await;
|
||||
let child = child.clone();
|
||||
*view_fn.lock().or_poisoned() =
|
||||
Box::new(move |owner_where_used| {
|
||||
let owner = owner.clone();
|
||||
let prev_owner = route_owner
|
||||
.lock()
|
||||
.or_poisoned()
|
||||
.replace(owner_where_used.clone());
|
||||
let view = view.clone();
|
||||
let full_tx =
|
||||
full_tx.lock().or_poisoned().take();
|
||||
let old_owner = old_owner.take();
|
||||
let child = child.clone();
|
||||
let params =
|
||||
params_including_parents.clone();
|
||||
@@ -841,15 +834,13 @@ where
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
let view = view.await;
|
||||
if let Some(old_owner) = old_owner {
|
||||
old_owner.cleanup();
|
||||
}
|
||||
|
||||
if let Some(tx) = full_tx {
|
||||
_ = tx.send(());
|
||||
_ = tx.send(prev_owner);
|
||||
}
|
||||
owner.with(|| {
|
||||
owner_where_used.with(|| {
|
||||
OwnedView::new(view).into_any()
|
||||
})
|
||||
}))
|
||||
@@ -868,9 +859,10 @@ where
|
||||
|
||||
// if this children has matches, then rebuild the lower section of the tree
|
||||
if let Some(child) = child {
|
||||
child.build_nested_route(
|
||||
url, base, preloaders, outlets, &owner,
|
||||
);
|
||||
child
|
||||
.build_nested_route(url, base, preloaders, outlets);
|
||||
} else {
|
||||
*outlets[*items].child.0.lock().or_poisoned() = None;
|
||||
}
|
||||
|
||||
return level;
|
||||
@@ -882,7 +874,6 @@ where
|
||||
current.params.set(new_params);
|
||||
current.url.set(url.to_owned());
|
||||
if let Some(child) = child {
|
||||
let owner = current.owner.clone();
|
||||
*items += 1;
|
||||
child.rebuild_nested_route(
|
||||
url,
|
||||
@@ -891,11 +882,11 @@ where
|
||||
preloaders,
|
||||
full_loaders,
|
||||
outlets,
|
||||
&owner,
|
||||
set_is_routing,
|
||||
level + 1,
|
||||
)
|
||||
} else {
|
||||
*current.child.0.lock().or_poisoned() = None;
|
||||
level
|
||||
}
|
||||
}
|
||||
@@ -933,13 +924,13 @@ fn top_level_outlet(outlets: &[RouteContext], outer_owner: &Owner) -> AnyView {
|
||||
let child = outlet.child.clone();
|
||||
let view_fn = outlet.view_fn.clone();
|
||||
let trigger = outlet.trigger.clone();
|
||||
let owner = outer_owner.child();
|
||||
outer_owner.clone().with(|| {
|
||||
provide_context(child.clone());
|
||||
let outer_owner = outer_owner.clone();
|
||||
(move || {
|
||||
trigger.track();
|
||||
let mut view_fn = view_fn.lock().or_poisoned();
|
||||
view_fn(owner.clone())
|
||||
view_fn(outer_owner.child())
|
||||
})
|
||||
.into_any()
|
||||
})
|
||||
@@ -953,13 +944,13 @@ where
|
||||
{
|
||||
let ChildRoute(child) = use_context()
|
||||
.expect("<Outlet/> used without RouteContext being provided.");
|
||||
let owner = Owner::new();
|
||||
let child = child.lock().or_poisoned().clone();
|
||||
let outer_owner = Owner::current().unwrap();
|
||||
child.map(|child| {
|
||||
move || {
|
||||
child.trigger.track();
|
||||
let mut view_fn = child.view_fn.lock().or_poisoned();
|
||||
view_fn(owner.clone())
|
||||
view_fn(outer_owner.child())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router_macro"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -13,10 +13,10 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro-error2 = { default-features = false , workspace = true }
|
||||
proc-macro-error2 = { default-features = false, workspace = true }
|
||||
proc-macro2 = { workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
syn = { features = ["full"] , workspace = true, default-features = true }
|
||||
syn = { features = ["full"], workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos_router = { path = "../router" }
|
||||
|
||||
@@ -32,7 +32,7 @@ dashmap = { workspace = true, default-features = true }
|
||||
|
||||
## servers
|
||||
# actix
|
||||
actix-web = { optional = true, workspace = true, default-features = true }
|
||||
actix-web = { optional = true, workspace = true, default-features = false }
|
||||
actix-ws = { optional = true, workspace = true, default-features = true }
|
||||
|
||||
# axum
|
||||
@@ -108,7 +108,8 @@ axum-no-default = [
|
||||
"dep:tower-layer",
|
||||
]
|
||||
form-redirects = []
|
||||
actix = ["ssr", "dep:actix-web", "dep:actix-ws", "dep:send_wrapper"]
|
||||
actix-no-default = ["ssr", "dep:actix-web", "dep:actix-ws", "dep:send_wrapper"]
|
||||
actix = ["actix-web/default", "actix-no-default"]
|
||||
axum = ["axum/default", "axum-no-default", "axum/ws", "dep:tokio"]
|
||||
browser = [
|
||||
"dep:gloo-net",
|
||||
|
||||
@@ -120,7 +120,7 @@ pub mod request;
|
||||
/// Types and traits for HTTP responses.
|
||||
pub mod response;
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
#[cfg(feature = "actix-no-default")]
|
||||
#[doc(hidden)]
|
||||
pub use ::actix_web as actix_export;
|
||||
#[cfg(feature = "axum-no-default")]
|
||||
@@ -1118,7 +1118,7 @@ pub mod axum {
|
||||
}
|
||||
|
||||
/// Actix integration.
|
||||
#[cfg(feature = "actix")]
|
||||
#[cfg(feature = "actix-no-default")]
|
||||
pub mod actix {
|
||||
use crate::{
|
||||
error::FromServerFnError, middleware::BoxedService,
|
||||
|
||||
@@ -123,7 +123,7 @@ mod axum {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
#[cfg(feature = "actix-no-default")]
|
||||
mod actix {
|
||||
use crate::{
|
||||
error::ServerFnErrorErr,
|
||||
|
||||
@@ -4,7 +4,7 @@ use http::Method;
|
||||
use std::{borrow::Cow, future::Future};
|
||||
|
||||
/// Request types for Actix.
|
||||
#[cfg(feature = "actix")]
|
||||
#[cfg(feature = "actix-no-default")]
|
||||
pub mod actix;
|
||||
/// Request types for Axum.
|
||||
#[cfg(feature = "axum-no-default")]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// Response types for Actix.
|
||||
#[cfg(feature = "actix")]
|
||||
#[cfg(feature = "actix-no-default")]
|
||||
pub mod actix;
|
||||
/// Response types for the browser.
|
||||
#[cfg(feature = "browser")]
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/empty_return.rs:3:1
|
||||
|
|
||||
3 | #[server]
|
||||
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
||||
|
|
||||
= help: the trait `ServerFnMustReturnResult` is not implemented for `()`
|
||||
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
|
||||
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
|
||||
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/empty_return.rs:3:1
|
||||
|
|
||||
@@ -16,22 +27,11 @@ error[E0271]: expected `impl Future<Output = ()>` to be a future that resolves t
|
||||
|
|
||||
= note: expected enum `Result<_, _>`
|
||||
found unit type `()`
|
||||
note: required by a bound in `ServerFn::{anon_assoc#0}`
|
||||
note: required by a bound in `ServerFn::run_body::{anon_assoc#0}`
|
||||
--> src/lib.rs
|
||||
|
|
||||
| ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{anon_assoc#0}`
|
||||
|
||||
error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/empty_return.rs:3:1
|
||||
|
|
||||
3 | #[server]
|
||||
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
||||
|
|
||||
= help: the trait `ServerFnMustReturnResult` is not implemented for `()`
|
||||
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
|
||||
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
|
||||
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::run_body::{anon_assoc#0}`
|
||||
|
||||
error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/empty_return.rs:3:1
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/not_result.rs:25:1
|
||||
|
|
||||
25 | #[server]
|
||||
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
||||
|
|
||||
= help: the trait `ServerFnMustReturnResult` is not implemented for `CustomError`
|
||||
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
|
||||
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
|
||||
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/not_result.rs:25:1
|
||||
|
|
||||
@@ -16,22 +27,11 @@ error[E0271]: expected `impl Future<Output = CustomError>` to be a future that r
|
||||
|
|
||||
= note: expected enum `Result<_, _>`
|
||||
found enum `CustomError`
|
||||
note: required by a bound in `ServerFn::{anon_assoc#0}`
|
||||
note: required by a bound in `ServerFn::run_body::{anon_assoc#0}`
|
||||
--> src/lib.rs
|
||||
|
|
||||
| ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{anon_assoc#0}`
|
||||
|
||||
error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/not_result.rs:25:1
|
||||
|
|
||||
25 | #[server]
|
||||
| ^^^^^^^^^ Must return a `Result` or aliased `Result`.
|
||||
|
|
||||
= help: the trait `ServerFnMustReturnResult` is not implemented for `CustomError`
|
||||
= note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type.
|
||||
= help: the trait `ServerFnMustReturnResult` is implemented for `Result<T, E>`
|
||||
= note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::run_body::{anon_assoc#0}`
|
||||
|
||||
error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/not_result.rs:25:1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tachys"
|
||||
version = "0.2.3"
|
||||
version = "0.2.5"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
Attribute,
|
||||
},
|
||||
hydration::Cursor,
|
||||
renderer::Rndr,
|
||||
ssr::StreamBuilder,
|
||||
};
|
||||
use futures::future::{join, join_all};
|
||||
@@ -90,6 +91,7 @@ pub struct AnyViewState {
|
||||
),
|
||||
insert_before_this: fn(&ErasedLocal, child: &mut dyn Mountable) -> bool,
|
||||
elements: fn(&ErasedLocal) -> Vec<crate::renderer::types::Element>,
|
||||
placeholder: Option<crate::renderer::types::Placeholder>,
|
||||
}
|
||||
|
||||
impl Debug for AnyViewState {
|
||||
@@ -214,6 +216,9 @@ where
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
if !T::EXISTS {
|
||||
buf.push_str("<!--<() />-->");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -232,6 +237,9 @@ where
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
if !T::EXISTS {
|
||||
buf.push_sync("<!--<() />-->");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -250,10 +258,14 @@ where
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
if !T::EXISTS {
|
||||
buf.push_sync("<!--<() />-->");
|
||||
}
|
||||
}
|
||||
|
||||
fn build<T: RenderHtml + 'static>(value: Erased) -> AnyViewState {
|
||||
let state = ErasedLocal::new(value.into_inner::<T>().build());
|
||||
let placeholder = (!T::EXISTS).then(Rndr::create_placeholder);
|
||||
AnyViewState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
@@ -261,6 +273,7 @@ where
|
||||
unmount: unmount_any::<T>,
|
||||
insert_before_this: insert_before_this::<T>,
|
||||
elements: elements::<T>,
|
||||
placeholder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +286,8 @@ where
|
||||
let state = ErasedLocal::new(
|
||||
value.into_inner::<T>().hydrate::<true>(cursor, position),
|
||||
);
|
||||
let placeholder =
|
||||
(!T::EXISTS).then(|| cursor.next_placeholder(position));
|
||||
AnyViewState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
@@ -280,6 +295,7 @@ where
|
||||
unmount: unmount_any::<T>,
|
||||
insert_before_this: insert_before_this::<T>,
|
||||
elements: elements::<T>,
|
||||
placeholder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +343,12 @@ impl Render for AnyView {
|
||||
(self.rebuild)(self.value, state)
|
||||
} else {
|
||||
let mut new = self.build();
|
||||
state.insert_before_this(&mut new);
|
||||
if let Some(placeholder) = &mut state.placeholder {
|
||||
placeholder.insert_before_this(&mut new);
|
||||
placeholder.unmount();
|
||||
} else {
|
||||
state.insert_before_this(&mut new);
|
||||
}
|
||||
state.unmount();
|
||||
*state = new;
|
||||
}
|
||||
@@ -554,7 +575,10 @@ impl RenderHtml for AnyView {
|
||||
|
||||
impl Mountable for AnyViewState {
|
||||
fn unmount(&mut self) {
|
||||
(self.unmount)(&mut self.state)
|
||||
(self.unmount)(&mut self.state);
|
||||
if let Some(placeholder) = &mut self.placeholder {
|
||||
placeholder.unmount();
|
||||
}
|
||||
}
|
||||
|
||||
fn mount(
|
||||
@@ -562,11 +586,23 @@ impl Mountable for AnyViewState {
|
||||
parent: &crate::renderer::types::Element,
|
||||
marker: Option<&crate::renderer::types::Node>,
|
||||
) {
|
||||
(self.mount)(&mut self.state, parent, marker)
|
||||
(self.mount)(&mut self.state, parent, marker);
|
||||
if let Some(placeholder) = &mut self.placeholder {
|
||||
placeholder.mount(parent, marker);
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
|
||||
(self.insert_before_this)(&self.state, child)
|
||||
let before_view = (self.insert_before_this)(&self.state, child);
|
||||
if before_view {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(placeholder) = &self.placeholder {
|
||||
placeholder.insert_before_this(child)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<crate::renderer::types::Element> {
|
||||
|
||||
@@ -134,6 +134,7 @@ where
|
||||
type Owned = (A::Owned,);
|
||||
|
||||
const MIN_LENGTH: usize = A::MIN_LENGTH;
|
||||
const EXISTS: bool = A::EXISTS;
|
||||
|
||||
fn html_len(&self) -> usize {
|
||||
self.0.html_len()
|
||||
@@ -239,7 +240,6 @@ macro_rules! impl_view_for_tuples {
|
||||
{
|
||||
type State = ($first::State, $($ty::State,)*);
|
||||
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
#[allow(non_snake_case)]
|
||||
let ($first, $($ty,)*) = self;
|
||||
@@ -267,7 +267,7 @@ macro_rules! impl_view_for_tuples {
|
||||
{
|
||||
type AsyncOutput = ($first::AsyncOutput, $($ty::AsyncOutput,)*);
|
||||
type Owned = ($first::Owned, $($ty::Owned,)*);
|
||||
|
||||
const EXISTS: bool = $first::EXISTS || $($ty::EXISTS || )* false;
|
||||
const MIN_LENGTH: usize = $first::MIN_LENGTH $(+ $ty::MIN_LENGTH)*;
|
||||
|
||||
#[inline(always)]
|
||||
|
||||
Reference in New Issue
Block a user