mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f67a47a6f | ||
|
|
356d69c689 | ||
|
|
8a28a30bef | ||
|
|
d7d2573b3e | ||
|
|
32d580b69e | ||
|
|
38f5368596 | ||
|
|
5136d0216f | ||
|
|
646cfc12ed | ||
|
|
62977a68b0 | ||
|
|
e9ee90c78f | ||
|
|
73e728f145 | ||
|
|
6f047a2271 | ||
|
|
7c942b8b47 | ||
|
|
d4bf6d9cb6 | ||
|
|
9deb96ea01 | ||
|
|
d1899cde1c | ||
|
|
ee731d7a3a | ||
|
|
59cbcfa0fb | ||
|
|
0939cf63ad | ||
|
|
d37512bebd | ||
|
|
7dd44919cf | ||
|
|
3eaabf85ea | ||
|
|
d60c632c90 | ||
|
|
f5ad4f4b88 | ||
|
|
f3a053f99b | ||
|
|
06573cbca1 | ||
|
|
c0ca97e42f | ||
|
|
9a4e93ab07 | ||
|
|
bee2b5ea1c | ||
|
|
3b058e77f1 | ||
|
|
7adb11ec49 | ||
|
|
1af5f66ee6 | ||
|
|
956f1836ec | ||
|
|
9779ca558d | ||
|
|
4ed2e1505f | ||
|
|
992f1a3365 |
840
Cargo.lock
generated
840
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
51
Cargo.toml
@@ -50,39 +50,39 @@ any_spawner = { path = "./any_spawner/", version = "0.3.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.6" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
leptos = { path = "./leptos", version = "0.8.8" }
|
||||
leptos = { path = "./leptos", version = "0.8.9" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.7" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.6" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.5" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.8" }
|
||||
leptos_router = { path = "./router", version = "0.8.6" }
|
||||
leptos_router = { path = "./router", version = "0.8.7" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.5" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.5" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.5" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.1" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.6" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.7" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.5" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.6" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.7" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.7" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" }
|
||||
tachys = { path = "./tachys", version = "0.2.7" }
|
||||
tachys = { path = "./tachys", version = "0.2.8" }
|
||||
wasm_split_helpers = { path = "./wasm_split", version = "0.1.2" }
|
||||
wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.2" }
|
||||
wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.3" }
|
||||
|
||||
# members deps
|
||||
async-once-cell = { default-features = false, version = "0.5.3" }
|
||||
itertools = { default-features = false, version = "0.14.0" }
|
||||
convert_case = { default-features = false, version = "0.8.0" }
|
||||
serde_json = { default-features = false, version = "1.0.142" }
|
||||
serde_json = { default-features = false, version = "1.0.143" }
|
||||
trybuild = { default-features = false, version = "1.0.110" }
|
||||
typed-builder = { default-features = false, version = "0.21.0" }
|
||||
thiserror = { default-features = false, version = "2.0.12" }
|
||||
typed-builder = { default-features = false, version = "0.21.2" }
|
||||
thiserror = { default-features = false, version = "2.0.16" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.100" }
|
||||
indexmap = { default-features = false, version = "2.9.0" }
|
||||
indexmap = { default-features = false, version = "2.11.0" }
|
||||
rstml = { default-features = false, version = "0.12.1" }
|
||||
rustc_version = { default-features = false, version = "0.4.1" }
|
||||
guardian = { default-features = false, version = "1.3.0" }
|
||||
@@ -102,15 +102,15 @@ gloo-net = { default-features = false, version = "0.6.0" }
|
||||
url = { default-features = false, version = "2.5.4" }
|
||||
tokio = { default-features = false, version = "1.47.1" }
|
||||
base64 = { default-features = false, version = "0.22.1" }
|
||||
cfg-if = { default-features = false, version = "1.0.0" }
|
||||
cfg-if = { default-features = false, version = "1.0.3" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
|
||||
tower = { default-features = false, version = "0.5.2" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.96" }
|
||||
proc-macro2 = { default-features = false, version = "1.0.101" }
|
||||
serde = { default-features = false, version = "1.0.219" }
|
||||
parking_lot = { default-features = false, version = "0.12.4" }
|
||||
axum = { default-features = false, version = "0.8.4" }
|
||||
serde_qs = { default-features = false, version = "0.15.0" }
|
||||
syn = { default-features = false, version = "2.0.104" }
|
||||
syn = { default-features = false, version = "2.0.106" }
|
||||
xxhash-rust = { default-features = false, version = "0.8.15" }
|
||||
paste = { default-features = false, version = "1.0.15" }
|
||||
quote = { default-features = false, version = "1.0.40" }
|
||||
@@ -122,16 +122,16 @@ 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.12" }
|
||||
async-trait = { default-features = false, version = "0.1.88" }
|
||||
async-trait = { default-features = false, version = "0.1.89" }
|
||||
typed-builder-macro = { default-features = false, version = "0.21.0" }
|
||||
linear-map = { default-features = false, version = "1.2.0" }
|
||||
anyhow = { default-features = false, version = "1.0.98" }
|
||||
anyhow = { default-features = false, version = "1.0.99" }
|
||||
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.36" }
|
||||
inventory = { default-features = false, version = "0.3.20" }
|
||||
config = { default-features = false, version = "0.15.13" }
|
||||
prettyplease = { default-features = false, version = "0.2.37" }
|
||||
inventory = { default-features = false, version = "0.3.21" }
|
||||
config = { default-features = false, version = "0.15.14" }
|
||||
camino = { default-features = false, version = "1.1.11" }
|
||||
ciborium = { default-features = false, version = "0.2.2" }
|
||||
multer = { default-features = false, version = "3.1.0" }
|
||||
@@ -144,24 +144,24 @@ temp-env = { default-features = false, version = "0.3.6" }
|
||||
uuid = { default-features = false, version = "1.18.0" }
|
||||
bytes = { default-features = false, version = "1.10.1" }
|
||||
http = { default-features = false, version = "1.3.1" }
|
||||
regex = { default-features = false, version = "1.11.1" }
|
||||
regex = { default-features = false, version = "1.11.2" }
|
||||
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
|
||||
tempfile = { default-features = false, version = "3.20.0" }
|
||||
tempfile = { default-features = false, version = "3.21.0" }
|
||||
futures-lite = { default-features = false, version = "2.6.1" }
|
||||
log = { default-features = false, version = "0.4.27" }
|
||||
percent-encoding = { default-features = false, version = "2.3.1" }
|
||||
percent-encoding = { default-features = false, version = "2.3.2" }
|
||||
async-executor = { default-features = false, version = "1.13.2" }
|
||||
const-str = { default-features = false, version = "0.6.4" }
|
||||
http-body-util = { default-features = false, version = "0.1.3" }
|
||||
hyper = { default-features = false, version = "1.6.0" }
|
||||
hyper = { default-features = false, version = "1.7.0" }
|
||||
postcard = { default-features = false, version = "1.1.3" }
|
||||
rmp-serde = { default-features = false, version = "1.3.0" }
|
||||
reqwest = { default-features = false, version = "0.12.22" }
|
||||
reqwest = { default-features = false, version = "0.12.23" }
|
||||
tower-layer = { default-features = false, version = "0.3.3" }
|
||||
attribute-derive = { default-features = false, version = "0.10.3" }
|
||||
insta = { default-features = false, version = "1.43.1" }
|
||||
codee = { default-features = false, version = "0.3.0" }
|
||||
actix-http = { default-features = false, version = "3.11.0" }
|
||||
actix-http = { default-features = false, version = "3.11.1" }
|
||||
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
|
||||
rustversion = { default-features = false, version = "1.0.22" }
|
||||
getrandom = { default-features = false, version = "0.3.3" }
|
||||
@@ -170,6 +170,9 @@ async-lock = { default-features = false, version = "3.4.1" }
|
||||
base16 = { default-features = false, version = "0.2.1" }
|
||||
digest = { default-features = false, version = "0.10.7" }
|
||||
sha2 = { default-features = false, version = "0.10.8" }
|
||||
subsecond = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" }
|
||||
dioxus-cli-config = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" }
|
||||
dioxus-devtools = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
9
examples/regression/e2e/features/issue_4285.feature
Normal file
9
examples/regression/e2e/features/issue_4285.feature
Normal file
@@ -0,0 +1,9 @@
|
||||
@check_issue_4285
|
||||
Feature: Check that issue 4285 does not reappear
|
||||
|
||||
Scenario: Navigating several times to same lazy route does not cause issues.
|
||||
Given I see the app
|
||||
And I can access regression test 4285
|
||||
And I can access regression test 4285
|
||||
And I can access regression test 4285
|
||||
Then I see the result is the string 42
|
||||
18
examples/regression/e2e/features/issue_4296.feature
Normal file
18
examples/regression/e2e/features/issue_4296.feature
Normal file
@@ -0,0 +1,18 @@
|
||||
@check_issue_4296
|
||||
Feature: Check that issue 4296 does not reappear
|
||||
|
||||
Scenario: Query param signals created in LazyRoute::data() are reactive in ::view().
|
||||
Given I see the app
|
||||
And I can access regression test 4296
|
||||
Then I see the result is the string None
|
||||
When I select the link abc
|
||||
Then I see the result is the string Some("abc")
|
||||
When I select the link def
|
||||
Then I see the result is the string Some("def")
|
||||
|
||||
Scenario: Loading page with query signal works as well.
|
||||
Given I see the app
|
||||
And I can access regression test 4296
|
||||
When I select the link abc
|
||||
When I reload the page
|
||||
Then I see the result is the string Some("abc")
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217,
|
||||
pr_4015::Routes4015, pr_4091::Routes4091,
|
||||
issue_4285::Routes4285, issue_4296::Routes4296, pr_4015::Routes4015,
|
||||
pr_4091::Routes4091,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{MetaTags, *};
|
||||
@@ -31,9 +32,11 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
let fallback = || view! { "Page not found." }.into_view();
|
||||
let (_, set_is_routing) = signal(false);
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/regression.css"/>
|
||||
<Router>
|
||||
<Router set_is_routing>
|
||||
<main>
|
||||
<Routes fallback>
|
||||
<Route path=path!("") view=HomePage/>
|
||||
@@ -42,6 +45,8 @@ pub fn App() -> impl IntoView {
|
||||
<Routes4088/>
|
||||
<Routes4217/>
|
||||
<Routes4005/>
|
||||
<Routes4285/>
|
||||
<Routes4296/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -66,6 +71,8 @@ fn HomePage() -> impl IntoView {
|
||||
<li><a href="/4088/">"4088"</a></li>
|
||||
<li><a href="/4217/">"4217"</a></li>
|
||||
<li><a href="/4005/">"4005"</a></li>
|
||||
<li><a href="/4285/">"4285"</a></li>
|
||||
<li><a href="/4296/">"4296"</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
49
examples/regression/src/issue_4285.rs
Normal file
49
examples/regression/src/issue_4285.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::LazyRoute;
|
||||
#[allow(unused_imports)]
|
||||
use leptos_router::{
|
||||
components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn Routes4285() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<Route path=path!("4285") view={Lazy::<Issue4285>::new()}/>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
struct Issue4285 {
|
||||
data: Resource<Result<i32, ServerFnError>>,
|
||||
}
|
||||
|
||||
impl LazyRoute for Issue4285 {
|
||||
fn data() -> Self {
|
||||
Self {
|
||||
data: Resource::new(|| (), |_| slow_call()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn view(this: Self) -> AnyView {
|
||||
let Issue4285 { data } = this;
|
||||
view! {
|
||||
<Suspense>
|
||||
{move || {
|
||||
Suspend::new(async move {
|
||||
let data = data.await;
|
||||
view! {
|
||||
<p id="result">{data}</p>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn slow_call() -> Result<i32, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
Ok(42)
|
||||
}
|
||||
36
examples/regression/src/issue_4296.rs
Normal file
36
examples/regression/src/issue_4296.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use leptos::prelude::*;
|
||||
#[allow(unused_imports)]
|
||||
use leptos_router::{
|
||||
components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions,
|
||||
};
|
||||
use leptos_router::{hooks::use_query_map, LazyRoute};
|
||||
|
||||
#[component]
|
||||
pub fn Routes4296() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<Route path=path!("4296") view={Lazy::<Issue4296>::new()}/>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
struct Issue4296 {
|
||||
query: Signal<Option<String>>,
|
||||
}
|
||||
|
||||
impl LazyRoute for Issue4296 {
|
||||
fn data() -> Self {
|
||||
let query = use_query_map();
|
||||
let query = Signal::derive(move || query.read().get("q"));
|
||||
Self { query }
|
||||
}
|
||||
|
||||
async fn view(this: Self) -> AnyView {
|
||||
let Issue4296 { query } = this;
|
||||
view! {
|
||||
<a href="?q=abc">"abc"</a>
|
||||
<a href="?q=def">"def"</a>
|
||||
<p id="result">{move || format!("{:?}", query.get())}</p>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ pub mod app;
|
||||
mod issue_4005;
|
||||
mod issue_4088;
|
||||
mod issue_4217;
|
||||
mod issue_4285;
|
||||
mod issue_4296;
|
||||
mod pr_4015;
|
||||
mod pr_4091;
|
||||
|
||||
|
||||
7
examples/subsecond_hot_patch/.gitignore
vendored
Normal file
7
examples/subsecond_hot_patch/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target
|
||||
.DS_Store
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
13
examples/subsecond_hot_patch/Cargo.toml
Normal file
13
examples/subsecond_hot_patch/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "subsecond_hot_patch"
|
||||
version = "0.1.0"
|
||||
authors = ["Greg Johnston <greg.johnston@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr", "subsecond"] }
|
||||
leptos_router = { path = "../../router" }
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
web = []
|
||||
21
examples/subsecond_hot_patch/Dioxus.toml
Normal file
21
examples/subsecond_hot_patch/Dioxus.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[application]
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "ltest"
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# Additional CSS style files
|
||||
style = []
|
||||
|
||||
# Additional JavaScript files
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
||||
1
examples/subsecond_hot_patch/Makefile.toml
Normal file
1
examples/subsecond_hot_patch/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
31
examples/subsecond_hot_patch/README.md
Normal file
31
examples/subsecond_hot_patch/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Hot Patching with `dx`
|
||||
|
||||
This is an experimental example exploring how to combine Leptos with the binary hot-patching provided by Dioxus's `subsecond` library and `dx` cli.
|
||||
|
||||
### Serving Your App
|
||||
|
||||
This requires installing the Dioxus CLI version 0.7.0. At the time I'm writing this README, that does not yet have a stable release. Once `dioxus-cli` 0.7.0 has been released, you should use the latest stable release. Until then, I'd suggest installing from git:
|
||||
|
||||
```sh
|
||||
cargo install dioxus-cli --git https://github.com/DioxusLabs/dioxus
|
||||
```
|
||||
|
||||
Then you can run the example with `dx serve --hot-patch --platform web`.
|
||||
|
||||
### Hot Patching
|
||||
|
||||
Changes to the your application should be reflected in your app without a full rebuild and reload.
|
||||
|
||||
### Limitatations
|
||||
|
||||
Currently we only support hot-patching for reactive view functions. You probably want to use `AnyView` (via `.into_any()`) on any views that will be hot-patched, so they can be rebuilt correctly despite their types changing when the structure of the view tree changes.
|
||||
|
||||
If you are using `leptos_router` this actually works quite well, as every route’s view is erased to `AnyView` and the router itself is a reactive view function: in other words, changes inside any route should be hot-patched in any case.
|
||||
|
||||
Note that any hot-patch will cause all render effects to run again. This means that some client-side state (like the values of signals) will be wiped out.
|
||||
|
||||
### Build Tooling
|
||||
|
||||
The preference of the Dioxus team is that all hot-patching work that uses their `subsecond` also use `dioxus-cli`. As this demo shows, it's completely possible to use `dioxus-cli` to build and run a Leptos project. We do not plan to build `subsecond` into our own build tooling at this time.
|
||||
|
||||
**This is an experiment/POC. It is being published because members of the community have found it useful and have asked for the support to be merged in its current state. Further development and bugfixes are a relatively low priority at this time.**
|
||||
BIN
examples/subsecond_hot_patch/assets/favicon.ico
Normal file
BIN
examples/subsecond_hot_patch/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
20
examples/subsecond_hot_patch/assets/header.svg
Normal file
20
examples/subsecond_hot_patch/assets/header.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 23 KiB |
46
examples/subsecond_hot_patch/assets/main.css
Normal file
46
examples/subsecond_hot_patch/assets/main.css
Normal file
@@ -0,0 +1,46 @@
|
||||
/* App-wide styling */
|
||||
body {
|
||||
background-color: #0f1116;
|
||||
color: #ffffff;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
#hero {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#links {
|
||||
width: 400px;
|
||||
text-align: left;
|
||||
font-size: x-large;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#links a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin-top: 20px;
|
||||
margin: 10px 0px;
|
||||
border: white 1px solid;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#links a:hover {
|
||||
background-color: #1f1f1f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#header {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
44
examples/subsecond_hot_patch/src/main.rs
Normal file
44
examples/subsecond_hot_patch/src/main.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use leptos::{prelude::*, subsecond::connect_to_hot_patch_messages};
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
path,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
// connect to DX CLI and patch the WASM binary whenever we receive a message
|
||||
connect_to_hot_patch_messages();
|
||||
|
||||
// wrapping App here in a closure so we can hot-reload it, because we only do that
|
||||
// for reactive views right now. changing anything will re-run App and update the view
|
||||
mount_to_body(|| App);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<nav>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/about">"About"</a>
|
||||
</nav>
|
||||
<Router>
|
||||
<Routes fallback=|| "Not found">
|
||||
<Route path=path!("/") view=HomePage/>
|
||||
<Route path=path!("/about") view=About/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Home Page"</h1>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn About() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"About"</h1>
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos"
|
||||
version = "0.8.8"
|
||||
version = "0.8.9"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -58,6 +58,9 @@ slotmap = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
wasm_split_helpers.workspace = true
|
||||
subsecond = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-cli-config = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-devtools = { workspace = true, default-features = true, optional = true }
|
||||
|
||||
[features]
|
||||
hydration = [
|
||||
@@ -102,6 +105,16 @@ trace-component-props = [
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
islands-router = ["tachys/mark_branches"]
|
||||
subsecond = [
|
||||
"reactive_graph/subsecond",
|
||||
"dep:subsecond",
|
||||
"dep:dioxus-cli-config",
|
||||
"dep:dioxus-devtools",
|
||||
"web-sys/Location",
|
||||
"web-sys/MessageEvent",
|
||||
"web-sys/WebSocket",
|
||||
"web-sys/Window",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { features = [
|
||||
|
||||
@@ -262,6 +262,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> From<View<C>> for ViewFn
|
||||
where
|
||||
C: Clone + Send + Sync + 'static,
|
||||
View<C>: IntoAny,
|
||||
{
|
||||
fn from(value: View<C>) -> Self {
|
||||
Self(Arc::new(move || value.clone().into_any()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewFn {
|
||||
/// Execute the wrapped function
|
||||
pub fn run(&self) -> AnyView {
|
||||
@@ -289,6 +299,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> From<View<C>> for ViewFnOnce
|
||||
where
|
||||
C: Send + Sync + 'static,
|
||||
View<C>: IntoAny,
|
||||
{
|
||||
fn from(value: View<C>) -> Self {
|
||||
Self(Box::new(move || value.into_any()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewFnOnce {
|
||||
/// Execute the wrapped function
|
||||
pub fn run(self) -> AnyView {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
if (window.location.protocol === 'https:') {
|
||||
protocol = 'wss://';
|
||||
}
|
||||
|
||||
let host = window.location.hostname;
|
||||
let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`);
|
||||
ws.onmessage = (ev) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#![deny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! # About Leptos
|
||||
//!
|
||||
@@ -85,12 +84,22 @@
|
||||
//! # Feature Flags
|
||||
//!
|
||||
//! - **`nightly`**: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
|
||||
//! Also enables some experimental optimizations that improve the handling of static strings and
|
||||
//! the performance of the `template! {}` macro.
|
||||
//! - **`csr`** Client-side rendering: Generate DOM nodes in the browser.
|
||||
//! - **`ssr`** Server-side rendering: Generate an HTML string (typically on the server).
|
||||
//! - **`islands`** Activates “islands mode,” in which components are not made interactive on the
|
||||
//! client unless they use the `#[island]` macro.
|
||||
//! - **`hydrate`** Hydration: use this to add interactivity to an SSRed Leptos app.
|
||||
//! - **`rkyv`** In SSR/hydrate mode, uses [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - **`nonce`** Adds support for nonces to be added as part of a Content Security Policy.
|
||||
//! - **`rkyv`** In SSR/hydrate mode, enables using [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources.
|
||||
//! - **`tracing`** Adds support for [`tracing`](https://docs.rs/tracing/latest/tracing/).
|
||||
//! - **`trace-component-props`** Adds `tracing` support for component props.
|
||||
//! - **`delegation`** Uses event delegation rather than the browser’s native event handling
|
||||
//! system. (This improves the performance of creating large numbers of elements simultaneously,
|
||||
//! in exchange for occasional edge cases in which events behave differently from native browser
|
||||
//! events.)
|
||||
//! - **`rustls`** Use `rustls` for server functions.
|
||||
//!
|
||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||
//! which mode your app is operating in. You should only enable one of these per build target,
|
||||
@@ -296,6 +305,10 @@ pub use tachys::mathml as math;
|
||||
#[doc(inline)]
|
||||
pub use tachys::svg;
|
||||
|
||||
#[cfg(feature = "subsecond")]
|
||||
/// Utilities for using binary hot-patching with [`subsecond`].
|
||||
pub mod subsecond;
|
||||
|
||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||
pub mod logging {
|
||||
pub use leptos_dom::{debug_warn, error, log, warn};
|
||||
|
||||
62
leptos/src/subsecond.rs
Normal file
62
leptos/src/subsecond.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use dioxus_devtools::DevserverMsg;
|
||||
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||
use web_sys::{js_sys::JsString, MessageEvent, WebSocket};
|
||||
|
||||
/// Sets up a websocket connect to the `dx` CLI, waiting for incoming hot-patching messages
|
||||
/// and patching the WASM binary appropriately.
|
||||
//
|
||||
// Note: This is a stripped-down version of Dioxus's `make_ws` from `dioxus_web`
|
||||
// It's essentially copy-pasted here because it's not pub there.
|
||||
// Would love to just take a dependency on that to be able to use it and deduplicate.
|
||||
//
|
||||
// https://github.com/DioxusLabs/dioxus/blob/main/packages/web/src/devtools.rs#L36
|
||||
pub fn connect_to_hot_patch_messages() {
|
||||
// Get the location of the devserver, using the current location plus the /_dioxus path
|
||||
// The idea here being that the devserver is always located on the /_dioxus behind a proxy
|
||||
let location = web_sys::window().unwrap().location();
|
||||
let url = format!(
|
||||
"{protocol}//{host}/_dioxus?build_id={build_id}",
|
||||
protocol = match location.protocol().unwrap() {
|
||||
prot if prot == "https:" => "wss:",
|
||||
_ => "ws:",
|
||||
},
|
||||
host = location.host().unwrap(),
|
||||
build_id = dioxus_cli_config::build_id(),
|
||||
);
|
||||
|
||||
let ws = WebSocket::new(&url).unwrap();
|
||||
|
||||
ws.set_onmessage(Some(
|
||||
Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
|
||||
let Ok(text) = e.data().dyn_into::<JsString>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// The devserver messages have some &'static strs in them, so we need to leak the source string
|
||||
let string: String = text.into();
|
||||
let string = Box::leak(string.into_boxed_str());
|
||||
|
||||
if let Ok(DevserverMsg::HotReload(msg)) =
|
||||
serde_json::from_str::<DevserverMsg>(string)
|
||||
{
|
||||
if let Some(jump_table) = msg.jump_table.as_ref().cloned() {
|
||||
if msg.for_build_id == Some(dioxus_cli_config::build_id()) {
|
||||
let our_pid = if cfg!(target_family = "wasm") {
|
||||
None
|
||||
} else {
|
||||
Some(std::process::id())
|
||||
};
|
||||
|
||||
if msg.for_pid == our_pid {
|
||||
unsafe { subsecond::apply_patch(jump_table) }
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_js_value()
|
||||
.as_ref()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
}
|
||||
@@ -32,12 +32,12 @@ use tachys::{
|
||||
};
|
||||
use throw_error::ErrorHookFuture;
|
||||
|
||||
/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
|
||||
/// If any [`Resource`](crate::prelude::Resource) is read in the `children` of this
|
||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
||||
/// it will render the `children`.
|
||||
///
|
||||
/// Each time one of the resources is loading again, it will fall back. To keep the current
|
||||
/// children instead, use [Transition](crate::Transition).
|
||||
/// children instead, use [Transition](crate::prelude::Transition).
|
||||
///
|
||||
/// Note that the `children` will be rendered initially (in order to capture the fact that
|
||||
/// those resources are read under the suspense), so you cannot assume that resources read
|
||||
|
||||
@@ -16,11 +16,11 @@ use reactive_graph::{
|
||||
use slotmap::{DefaultKey, SlotMap};
|
||||
use tachys::reactive_graph::OwnedView;
|
||||
|
||||
/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
|
||||
/// If any [`Resource`](crate::prelude::Resource) is read in the `children` of this
|
||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
||||
/// it will render the `children`.
|
||||
///
|
||||
/// Unlike [`Suspense`](crate::Suspense), this will not fall
|
||||
/// Unlike [`Suspense`](crate::prelude::Suspense), this will not fall
|
||||
/// back to the `fallback` state if there are further changes after the initial load.
|
||||
///
|
||||
/// Note that the `children` will be rendered initially (in order to capture the fact that
|
||||
|
||||
@@ -463,7 +463,7 @@ pub fn set_interval_with_handle(
|
||||
|
||||
#[inline(never)]
|
||||
fn si(
|
||||
cb: Box<dyn Fn()>,
|
||||
cb: Box<dyn FnMut()>,
|
||||
duration: Duration,
|
||||
) -> Result<IntervalHandle, JsValue> {
|
||||
let cb = Closure::wrap(cb).into_js_value();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -27,6 +27,7 @@ async-lock = { workspace = true, default-features = true }
|
||||
send_wrapper = { features = [
|
||||
"futures",
|
||||
], workspace = true, default-features = true }
|
||||
subsecond = { workspace = true, default-features = true, optional = true }
|
||||
indexmap = { workspace = true, default-features = true }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
@@ -51,6 +52,7 @@ hydration = ["dep:hydration_context"]
|
||||
effects = [
|
||||
] # whether to run effects: should be disabled for something like server rendering
|
||||
sandboxed-arenas = []
|
||||
subsecond = ["dep:subsecond"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{
|
||||
computed::{ArcMemo, Memo},
|
||||
computed::{ArcMemo, Memo, ScopedFuture},
|
||||
diagnostics::is_suppressing_resource_load,
|
||||
owner::{ArcStoredValue, ArenaItem},
|
||||
graph::untrack,
|
||||
owner::{ArcStoredValue, ArenaItem, Owner},
|
||||
send_wrapper_ext::SendOption,
|
||||
signal::{ArcMappedSignal, ArcRwSignal, MappedSignal, RwSignal},
|
||||
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update, Write},
|
||||
@@ -199,13 +200,18 @@ where
|
||||
I: Send + Sync,
|
||||
O: Send + Sync,
|
||||
{
|
||||
let owner = Owner::current().unwrap_or_default();
|
||||
ArcAction {
|
||||
in_flight: ArcRwSignal::new(0),
|
||||
input: ArcRwSignal::new(SendOption::new(None)),
|
||||
value: ArcRwSignal::new(SendOption::new(value)),
|
||||
version: Default::default(),
|
||||
dispatched: Default::default(),
|
||||
action_fn: Arc::new(move |input| Box::pin(action_fn(input))),
|
||||
action_fn: Arc::new(move |input| {
|
||||
Box::pin(owner.with(|| {
|
||||
ScopedFuture::new_untracked(untrack(|| action_fn(input)))
|
||||
}))
|
||||
}),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
@@ -370,6 +376,7 @@ where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
let owner = Owner::current().unwrap_or_default();
|
||||
let action_fn = SendWrapper::new(action_fn);
|
||||
ArcAction {
|
||||
in_flight: ArcRwSignal::new(0),
|
||||
@@ -378,7 +385,9 @@ where
|
||||
version: Default::default(),
|
||||
dispatched: Default::default(),
|
||||
action_fn: Arc::new(move |input| {
|
||||
Box::pin(SendWrapper::new(action_fn(input)))
|
||||
Box::pin(SendWrapper::new(owner.with(|| {
|
||||
ScopedFuture::new_untracked(untrack(|| action_fn(input)))
|
||||
})))
|
||||
}),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
|
||||
@@ -521,9 +521,10 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
{
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
let fut = ScopedFuture::new_untracked(async move {
|
||||
SendOption::new(Some(fut.await))
|
||||
});
|
||||
let fut =
|
||||
ScopedFuture::new_untracked_with_diagnostics(async move {
|
||||
SendOption::new(Some(fut.await))
|
||||
});
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
let fut = Sandboxed::new(fut);
|
||||
fut
|
||||
|
||||
@@ -54,11 +54,55 @@ impl<Fut> ScopedFuture<Fut> {
|
||||
fut,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn new_untracked_with_diagnostics(
|
||||
fut: Fut,
|
||||
) -> ScopedFutureUntrackedWithDiagnostics<Fut> {
|
||||
let owner = Owner::current().unwrap_or_default();
|
||||
ScopedFutureUntrackedWithDiagnostics {
|
||||
owner,
|
||||
observer: None,
|
||||
fut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut: Future> Future for ScopedFuture<Fut> {
|
||||
type Output = Fut::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
this.owner.with(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
let _maybe_guard = if this.observer.is_none() {
|
||||
Some(crate::diagnostics::SpecialNonReactiveZone::enter())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
this.observer.with_observer(|| this.fut.poll(cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
/// A [`Future`] wrapper that sets the [`Owner`] and [`Observer`] before polling the inner
|
||||
/// `Future`, output of [`ScopedFuture::new_untracked_with_diagnostics`].
|
||||
///
|
||||
/// In leptos 0.9 this will be replaced with `ScopedFuture` itself.
|
||||
#[derive(Clone)]
|
||||
pub struct ScopedFutureUntrackedWithDiagnostics<Fut> {
|
||||
owner: Owner,
|
||||
observer: Option<AnySubscriber>,
|
||||
#[pin]
|
||||
fut: Fut,
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut: Future> Future for ScopedFutureUntrackedWithDiagnostics<Fut> {
|
||||
type Output = Fut::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
this.owner
|
||||
|
||||
@@ -9,6 +9,8 @@ use crate::{
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use or_poisoned::OrPoisoned;
|
||||
#[cfg(feature = "subsecond")]
|
||||
use std::sync::Mutex;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
future::{Future, IntoFuture},
|
||||
@@ -49,13 +51,39 @@ impl<T> Debug for RenderEffect<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "subsecond")]
|
||||
type CurrentHotPtr = Box<dyn Fn() -> Option<subsecond::HotFnPtr> + Send + Sync>;
|
||||
|
||||
impl<T> RenderEffect<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
/// Creates a new render effect, which immediately runs `fun`.
|
||||
pub fn new(fun: impl FnMut(Option<T>) -> T + 'static) -> Self {
|
||||
Self::new_with_value_erased(Box::new(fun), None)
|
||||
#[cfg(feature = "subsecond")]
|
||||
let (hot_fn_ptr, fun) = {
|
||||
let fun = Arc::new(Mutex::new(subsecond::HotFn::current(fun)));
|
||||
(
|
||||
{
|
||||
let fun = Arc::downgrade(&fun);
|
||||
let wrapped = send_wrapper::SendWrapper::new(move || {
|
||||
fun.upgrade()
|
||||
.map(|n| n.lock().or_poisoned().ptr_address())
|
||||
});
|
||||
// it's not redundant, it's due to the SendWrapper deref
|
||||
#[allow(clippy::redundant_closure)]
|
||||
Box::new(move || wrapped())
|
||||
},
|
||||
move |prev| fun.lock().or_poisoned().call((prev,)),
|
||||
)
|
||||
};
|
||||
|
||||
Self::new_with_value_erased(
|
||||
Box::new(fun),
|
||||
None,
|
||||
#[cfg(feature = "subsecond")]
|
||||
hot_fn_ptr,
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new render effect with an initial value.
|
||||
@@ -63,7 +91,30 @@ where
|
||||
fun: impl FnMut(Option<T>) -> T + 'static,
|
||||
initial_value: Option<T>,
|
||||
) -> Self {
|
||||
Self::new_with_value_erased(Box::new(fun), initial_value)
|
||||
#[cfg(feature = "subsecond")]
|
||||
let (hot_fn_ptr, fun) = {
|
||||
let fun = Arc::new(Mutex::new(subsecond::HotFn::current(fun)));
|
||||
(
|
||||
{
|
||||
let fun = Arc::downgrade(&fun);
|
||||
let wrapped = send_wrapper::SendWrapper::new(move || {
|
||||
fun.upgrade()
|
||||
.map(|n| n.lock().or_poisoned().ptr_address())
|
||||
});
|
||||
// it's not redundant, it's due to the SendWrapper deref
|
||||
#[allow(clippy::redundant_closure)]
|
||||
Box::new(move || wrapped())
|
||||
},
|
||||
move |prev| fun.lock().or_poisoned().call((prev,)),
|
||||
)
|
||||
};
|
||||
|
||||
Self::new_with_value_erased(
|
||||
Box::new(fun),
|
||||
initial_value,
|
||||
#[cfg(feature = "subsecond")]
|
||||
hot_fn_ptr,
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new render effect, which immediately runs `fun`.
|
||||
@@ -71,6 +122,11 @@ where
|
||||
fun: impl FnMut(Option<T>) -> T + 'static,
|
||||
value: impl IntoFuture<Output = T> + 'static,
|
||||
) -> Self {
|
||||
#[cfg(feature = "subsecond")]
|
||||
let mut fun = subsecond::HotFn::current(fun);
|
||||
#[cfg(feature = "subsecond")]
|
||||
let fun = move |prev| fun.call((prev,));
|
||||
|
||||
Self::new_with_async_value_erased(
|
||||
Box::new(fun),
|
||||
Box::pin(value.into_future()),
|
||||
@@ -79,8 +135,13 @@ where
|
||||
}
|
||||
|
||||
fn new_with_value_erased(
|
||||
mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>,
|
||||
#[allow(unused_mut)] mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>,
|
||||
initial_value: Option<T>,
|
||||
// this argument can be used to invalidate individual effects in the future
|
||||
// in present experiments, I have found that it is not actually granular enough to make a difference
|
||||
#[allow(unused)]
|
||||
#[cfg(feature = "subsecond")]
|
||||
hot_fn_ptr: CurrentHotPtr,
|
||||
) -> Self {
|
||||
// codegen optimisation:
|
||||
fn prep() -> (Owner, Arc<RwLock<EffectInner>>, crate::channel::Receiver)
|
||||
@@ -104,12 +165,56 @@ where
|
||||
let _ = initial_value;
|
||||
let _ = owner;
|
||||
let _ = &mut rx;
|
||||
let _ = &mut fun;
|
||||
let _ = fun;
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
{
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
|
||||
#[cfg(all(feature = "subsecond", debug_assertions))]
|
||||
let mut fun = {
|
||||
use crate::graph::ReactiveNode;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use subsecond::HotFnPtr;
|
||||
|
||||
static HOT_RELOAD_SUBSCRIBERS: LazyLock<
|
||||
Mutex<FxHashMap<AnySubscriber, (HotFnPtr, CurrentHotPtr)>>,
|
||||
> = LazyLock::new(|| {
|
||||
subsecond::register_handler(Arc::new(|| {
|
||||
HOT_RELOAD_SUBSCRIBERS.lock().or_poisoned().retain(
|
||||
|subscriber, (prev_ptr, hot_fn_ptr)| {
|
||||
match hot_fn_ptr() {
|
||||
None => false,
|
||||
Some(curr_hot_ptr) => {
|
||||
if curr_hot_ptr != *prev_ptr {
|
||||
crate::log_warning(format_args!(
|
||||
"{prev_ptr:?} <> \
|
||||
{curr_hot_ptr:?}",
|
||||
));
|
||||
*prev_ptr = curr_hot_ptr;
|
||||
|
||||
subscriber.mark_dirty();
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}));
|
||||
Default::default()
|
||||
});
|
||||
|
||||
let mut fun = subsecond::HotFn::current(fun);
|
||||
let initial_ptr = hot_fn_ptr().unwrap();
|
||||
HOT_RELOAD_SUBSCRIBERS
|
||||
.lock()
|
||||
.or_poisoned()
|
||||
.insert(subscriber.clone(), (initial_ptr, hot_fn_ptr));
|
||||
move |prev| fun.call((prev,))
|
||||
};
|
||||
|
||||
*value.write().or_poisoned() = Some(
|
||||
owner.with(|| subscriber.with_observer(|| fun(initial_value))),
|
||||
);
|
||||
@@ -230,6 +335,11 @@ where
|
||||
pub fn new_isomorphic(
|
||||
fun: impl FnMut(Option<T>) -> T + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
#[cfg(feature = "subsecond")]
|
||||
let mut fun = subsecond::HotFn::current(fun);
|
||||
#[cfg(feature = "subsecond")]
|
||||
let fun = move |prev| fun.call((prev,));
|
||||
|
||||
fn erased<T: Send + Sync + 'static>(
|
||||
mut fun: Box<dyn FnMut(Option<T>) -> T + Send + Sync + 'static>,
|
||||
) -> RenderEffect<T> {
|
||||
|
||||
@@ -257,6 +257,20 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<ReadSignal<T, S>> for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<ArcReadSignal<T>> + Storage<T>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ReadSignal<T, S>) -> Self {
|
||||
Self {
|
||||
inner: SignalTypes::ReadSignal(value.into()),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync> From<ArcRwSignal<T>> for ArcSignal<T, SyncStorage> {
|
||||
#[track_caller]
|
||||
fn from(value: ArcRwSignal<T>) -> Self {
|
||||
@@ -268,6 +282,20 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<RwSignal<T, S>> for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<ArcRwSignal<T>> + Storage<ArcReadSignal<T>> + Storage<T>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: RwSignal<T, S>) -> Self {
|
||||
Self {
|
||||
inner: SignalTypes::ReadSignal(value.read_only().into()),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<ArcMemo<T, S>> for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
@@ -282,6 +310,20 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<Memo<T, S>> for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<ArcMemo<T, S>> + Storage<T>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: Memo<T, S>) -> Self {
|
||||
Self {
|
||||
inner: SignalTypes::Memo(value.into()),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> DefinedAt for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -221,6 +221,13 @@ impl LocationProvider for BrowserUrl {
|
||||
fn complete_navigation(&self, loc: &LocationChange) {
|
||||
let history = window().history().unwrap();
|
||||
|
||||
let current_path = self
|
||||
.path_stack
|
||||
.read_value()
|
||||
.last()
|
||||
.map(|url| url.to_full_path());
|
||||
let add_to_stack = current_path.as_ref() != Some(&loc.value);
|
||||
|
||||
if loc.replace {
|
||||
history
|
||||
.replace_state_with_url(
|
||||
@@ -229,7 +236,7 @@ impl LocationProvider for BrowserUrl {
|
||||
Some(&loc.value),
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
} else if add_to_stack {
|
||||
// push the "forward direction" marker
|
||||
let state = &loc.state.to_js_value();
|
||||
history
|
||||
@@ -240,7 +247,9 @@ impl LocationProvider for BrowserUrl {
|
||||
// add this URL to the "path stack" for detecting back navigations, and
|
||||
// unset "navigating back" state
|
||||
if let Ok(url) = Self::current() {
|
||||
self.path_stack.write_value().push(url);
|
||||
if add_to_stack {
|
||||
self.path_stack.write_value().push(url);
|
||||
}
|
||||
self.is_back.set(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ use reactive_graph::{
|
||||
computed::{ArcMemo, ScopedFuture},
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::{ArcRwSignal, ArcTrigger},
|
||||
traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track},
|
||||
traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track, Write},
|
||||
transition::AsyncTransition,
|
||||
wrappers::write::SignalSetter,
|
||||
};
|
||||
@@ -119,6 +119,7 @@ where
|
||||
base,
|
||||
&mut loaders,
|
||||
&mut outlets,
|
||||
&outer_owner,
|
||||
);
|
||||
drop(url);
|
||||
|
||||
@@ -159,13 +160,14 @@ where
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// since the path didn't match, we'll update the retained path for future diffing
|
||||
state.path.clear();
|
||||
state.path.push_str(url_snapshot.path());
|
||||
|
||||
let new_match = self.routes.match_route(url_snapshot.path());
|
||||
|
||||
state.current_url.set(url_snapshot);
|
||||
*state.current_url.write_untracked() = url_snapshot;
|
||||
|
||||
match new_match {
|
||||
None => {
|
||||
@@ -192,6 +194,7 @@ where
|
||||
&mut state.outlets,
|
||||
self.set_is_routing.is_some(),
|
||||
0,
|
||||
&self.outer_owner,
|
||||
);
|
||||
|
||||
let (abort_handle, abort_registration) =
|
||||
@@ -369,6 +372,7 @@ where
|
||||
base,
|
||||
&mut loaders,
|
||||
&mut outlets,
|
||||
&outer_owner,
|
||||
);
|
||||
|
||||
// outlets will not send their views if the loaders are never polled
|
||||
@@ -422,8 +426,16 @@ where
|
||||
base,
|
||||
&mut loaders,
|
||||
&mut outlets,
|
||||
&outer_owner,
|
||||
);
|
||||
|
||||
let preload_owners = outlets
|
||||
.iter()
|
||||
.map(|o| o.preload_owner.clone())
|
||||
.collect::<Vec<_>>();
|
||||
outer_owner
|
||||
.with(|| Owner::on_cleanup(move || drop(preload_owners)));
|
||||
|
||||
// outlets will not send their views if the loaders are never polled
|
||||
// the loaders are async so that they can lazy-load routes in the browser,
|
||||
// but they should always be synchronously available on the server
|
||||
@@ -475,6 +487,7 @@ where
|
||||
base,
|
||||
&mut loaders,
|
||||
&mut outlets,
|
||||
&outer_owner,
|
||||
);
|
||||
drop(url);
|
||||
|
||||
@@ -530,6 +543,7 @@ where
|
||||
base,
|
||||
&mut loaders,
|
||||
&mut outlets,
|
||||
&outer_owner,
|
||||
);
|
||||
drop(url);
|
||||
|
||||
@@ -566,6 +580,7 @@ pub(crate) struct RouteContext {
|
||||
base: Option<Oco<'static, str>>,
|
||||
view_fn: Arc<Mutex<OutletViewFn>>,
|
||||
owner: Arc<Mutex<Option<Owner>>>,
|
||||
preload_owner: Owner,
|
||||
child: ChildRoute,
|
||||
}
|
||||
|
||||
@@ -597,6 +612,7 @@ impl Clone for RouteContext {
|
||||
view_fn: Arc::clone(&self.view_fn),
|
||||
owner: Arc::clone(&self.owner),
|
||||
child: self.child.clone(),
|
||||
preload_owner: self.preload_owner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -608,6 +624,7 @@ trait AddNestedRoute {
|
||||
base: Option<Oco<'static, str>>,
|
||||
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
outer_owner: &Owner,
|
||||
);
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -621,6 +638,7 @@ trait AddNestedRoute {
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
set_is_routing: bool,
|
||||
level: u8,
|
||||
outer_owner: &Owner,
|
||||
) -> u8;
|
||||
}
|
||||
|
||||
@@ -634,6 +652,7 @@ where
|
||||
base: Option<Oco<'static, str>>,
|
||||
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
outer_owner: &Owner,
|
||||
) {
|
||||
let orig_url = url;
|
||||
|
||||
@@ -701,6 +720,7 @@ where
|
||||
base: base.clone(),
|
||||
child: ChildRoute(Arc::new(Mutex::new(None))),
|
||||
owner: Arc::new(Mutex::new(None)),
|
||||
preload_owner: outer_owner.child(),
|
||||
};
|
||||
if !outlets.is_empty() {
|
||||
let prev_index = outlets.len().saturating_sub(1);
|
||||
@@ -725,7 +745,15 @@ where
|
||||
provide_context(params.clone());
|
||||
provide_context(url.clone());
|
||||
provide_context(matched.clone());
|
||||
view.preload().await;
|
||||
outlet
|
||||
.preload_owner
|
||||
.with(|| {
|
||||
provide_context(params.clone());
|
||||
provide_context(url.clone());
|
||||
provide_context(matched.clone());
|
||||
ScopedFuture::new(view.preload())
|
||||
})
|
||||
.await;
|
||||
let child = outlet.child.clone();
|
||||
*view_fn.lock().or_poisoned() =
|
||||
Box::new(move |owner_where_used| {
|
||||
@@ -772,7 +800,13 @@ 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);
|
||||
child.build_nested_route(
|
||||
orig_url,
|
||||
base,
|
||||
loaders,
|
||||
outlets,
|
||||
outer_owner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,6 +821,7 @@ where
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
set_is_routing: bool,
|
||||
level: u8,
|
||||
outer_owner: &Owner,
|
||||
) -> u8 {
|
||||
let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets
|
||||
.iter()
|
||||
@@ -803,7 +838,13 @@ where
|
||||
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);
|
||||
self.build_nested_route(
|
||||
url,
|
||||
base,
|
||||
preloaders,
|
||||
outlets,
|
||||
outer_owner,
|
||||
);
|
||||
level
|
||||
}
|
||||
Some(current) => {
|
||||
@@ -843,6 +884,10 @@ where
|
||||
&mut current.matched,
|
||||
ArcRwSignal::new(new_match),
|
||||
);
|
||||
let old_preload_owner = mem::replace(
|
||||
&mut current.preload_owner,
|
||||
outer_owner.child(),
|
||||
);
|
||||
let matched_including_parents = {
|
||||
ArcMemo::new({
|
||||
let matched = current.matched.clone();
|
||||
@@ -885,11 +930,26 @@ where
|
||||
let child = outlet.child.clone();
|
||||
async move {
|
||||
let child = child.clone();
|
||||
if set_is_routing {
|
||||
AsyncTransition::run(|| view.preload()).await;
|
||||
} else {
|
||||
view.preload().await;
|
||||
}
|
||||
outlet
|
||||
.preload_owner
|
||||
.with(|| {
|
||||
provide_context(
|
||||
params_including_parents.clone(),
|
||||
);
|
||||
provide_context(url.clone());
|
||||
provide_context(matched.clone());
|
||||
ScopedFuture::new(async {
|
||||
if set_is_routing {
|
||||
AsyncTransition::run(|| {
|
||||
view.preload()
|
||||
})
|
||||
.await;
|
||||
} else {
|
||||
view.preload().await;
|
||||
}
|
||||
})
|
||||
})
|
||||
.await;
|
||||
*view_fn.lock().or_poisoned() =
|
||||
Box::new(move |owner_where_used| {
|
||||
let prev_owner = route_owner
|
||||
@@ -938,6 +998,7 @@ where
|
||||
drop(old_params);
|
||||
drop(old_url);
|
||||
drop(old_matched);
|
||||
drop(old_preload_owner);
|
||||
trigger
|
||||
}
|
||||
})));
|
||||
@@ -948,8 +1009,13 @@ 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);
|
||||
child.build_nested_route(
|
||||
url,
|
||||
base,
|
||||
preloaders,
|
||||
outlets,
|
||||
outer_owner,
|
||||
);
|
||||
} else {
|
||||
*outlets[*items].child.0.lock().or_poisoned() = None;
|
||||
}
|
||||
@@ -973,6 +1039,7 @@ where
|
||||
outlets,
|
||||
set_is_routing,
|
||||
level + 1,
|
||||
outer_owner,
|
||||
)
|
||||
} else {
|
||||
*current.child.0.lock().or_poisoned() = None;
|
||||
|
||||
@@ -5,7 +5,7 @@ license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for any web framework."
|
||||
readme = "../README.md"
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
|
||||
@@ -568,7 +568,7 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static {
|
||||
/// Converts a [`ServerFnErrorErr`] into the application-specific custom error type.
|
||||
fn from_server_fn_error(value: ServerFnErrorErr) -> Self;
|
||||
|
||||
/// Converts the custom error type to a [`String`].
|
||||
/// Serializes the custom error type to bytes, according to the encoding given by `Self::Encoding`.
|
||||
fn ser(&self) -> Bytes {
|
||||
Self::Encoder::encode(self).unwrap_or_else(|e| {
|
||||
Self::Encoder::encode(&Self::from_server_fn_error(
|
||||
@@ -581,7 +581,7 @@ pub trait FromServerFnError: std::fmt::Debug + Sized + 'static {
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserializes the custom error type from a [`&str`].
|
||||
/// Deserializes the custom error type, according to the encoding given by `Self::Encoding`.
|
||||
fn de(data: Bytes) -> Self {
|
||||
Self::Encoder::decode(data).unwrap_or_else(|e| {
|
||||
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
|
||||
|
||||
@@ -307,16 +307,18 @@ pub trait ServerFn: Send + Sized {
|
||||
.await
|
||||
.map(|res| (res, None))
|
||||
.unwrap_or_else(|e| {
|
||||
(
|
||||
let mut response =
|
||||
<<Self as ServerFn>::Server as crate::Server<
|
||||
Self::Error,
|
||||
Self::InputStreamError,
|
||||
Self::OutputStreamError,
|
||||
>>::Response::error_response(
|
||||
Self::PATH, e.ser()
|
||||
),
|
||||
Some(e),
|
||||
)
|
||||
);
|
||||
let content_type =
|
||||
<Self::Error as FromServerFnError>::Encoder::CONTENT_TYPE;
|
||||
response.content_type(content_type);
|
||||
(response, Some(e))
|
||||
});
|
||||
|
||||
// if it accepts HTML, we'll redirect to the Referer
|
||||
|
||||
@@ -72,6 +72,10 @@ mod axum {
|
||||
let inner = self.call(req);
|
||||
Box::pin(async move {
|
||||
inner.await.unwrap_or_else(|e| {
|
||||
// TODO: This does not set the Content-Type on the response. Doing so will
|
||||
// require a breaking change in order to get the correct encoding from the
|
||||
// error's `FromServerFnError::Encoder::CONTENT_TYPE` impl.
|
||||
// Note: This only applies to middleware errors.
|
||||
let err =
|
||||
ser(ServerFnErrorErr::MiddlewareError(e.to_string()));
|
||||
Response::<Body>::error_response(&path, err)
|
||||
@@ -149,6 +153,10 @@ mod actix {
|
||||
let inner = self.call(req);
|
||||
Box::pin(async move {
|
||||
inner.await.unwrap_or_else(|e| {
|
||||
// TODO: This does not set the Content-Type on the response. Doing so will
|
||||
// require a breaking change in order to get the correct encoding from the
|
||||
// error's `FromServerFnError::Encoder::CONTENT_TYPE` impl.
|
||||
// Note: This only applies to middleware errors.
|
||||
let err =
|
||||
ser(ServerFnErrorErr::MiddlewareError(e.to_string()));
|
||||
ActixResponse::error_response(&path, err).take()
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::error::{
|
||||
use actix_web::{
|
||||
http::{
|
||||
header,
|
||||
header::{HeaderValue, LOCATION},
|
||||
header::{HeaderValue, CONTENT_TYPE, LOCATION},
|
||||
StatusCode,
|
||||
},
|
||||
HttpResponse,
|
||||
@@ -80,6 +80,12 @@ impl Res for ActixResponse {
|
||||
))
|
||||
}
|
||||
|
||||
fn content_type(&mut self, content_type: &str) {
|
||||
if let Ok(content_type) = HeaderValue::from_str(content_type) {
|
||||
self.0.headers_mut().insert(CONTENT_TYPE, content_type);
|
||||
}
|
||||
}
|
||||
|
||||
fn redirect(&mut self, path: &str) {
|
||||
if let Ok(path) = HeaderValue::from_str(path) {
|
||||
*self.0.status_mut() = StatusCode::FOUND;
|
||||
|
||||
@@ -100,6 +100,13 @@ impl Res for Response<Body> {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn content_type(&mut self, content_type: &str) {
|
||||
if let Ok(content_type) = HeaderValue::from_str(content_type) {
|
||||
self.headers_mut()
|
||||
.insert(header::CONTENT_TYPE, content_type);
|
||||
}
|
||||
}
|
||||
|
||||
fn redirect(&mut self, path: &str) {
|
||||
if let Ok(path) = HeaderValue::from_str(path) {
|
||||
self.headers_mut().insert(header::LOCATION, path);
|
||||
|
||||
@@ -60,6 +60,13 @@ impl Res for Response<Body> {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn content_type(&mut self, content_type: &str) {
|
||||
if let Ok(content_type) = HeaderValue::from_str(content_type) {
|
||||
self.headers_mut()
|
||||
.insert(header::CONTENT_TYPE, content_type);
|
||||
}
|
||||
}
|
||||
|
||||
fn redirect(&mut self, path: &str) {
|
||||
if let Ok(path) = HeaderValue::from_str(path) {
|
||||
self.headers_mut().insert(header::LOCATION, path);
|
||||
|
||||
@@ -37,9 +37,14 @@ where
|
||||
|
||||
/// Represents the response as created by the server;
|
||||
pub trait Res {
|
||||
/// Converts an error into a response, with a `500` status code and the error text as its body.
|
||||
/// Converts an error into a response, with a `500` status code and the error as its body.
|
||||
fn error_response(path: &str, err: Bytes) -> Self;
|
||||
|
||||
/// Set the `Content-Type` header for the response.
|
||||
fn content_type(&mut self, #[allow(unused_variables)] content_type: &str) {
|
||||
// TODO 0.9: remove this method and default implementation. It is only included here
|
||||
// to allow setting the `Content-Type` header for error responses without requiring a
|
||||
// semver-incompatible change.
|
||||
}
|
||||
/// Redirect the response by setting a 302 code and Location header.
|
||||
fn redirect(&mut self, path: &str);
|
||||
}
|
||||
@@ -103,6 +108,10 @@ impl Res for BrowserMockRes {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn content_type(&mut self, _content_type: &str) {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn redirect(&mut self, _path: &str) {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tachys"
|
||||
version = "0.2.7"
|
||||
version = "0.2.8"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use super::{Attribute, NextAttribute};
|
||||
use crate::erased::{Erased, ErasedLocal};
|
||||
use std::{any::TypeId, fmt::Debug};
|
||||
use crate::{
|
||||
erased::{Erased, ErasedLocal},
|
||||
html::attribute::NamedAttributeKey,
|
||||
renderer::{dom::Element, Rndr},
|
||||
};
|
||||
use std::{any::TypeId, fmt::Debug, mem};
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
@@ -25,6 +29,7 @@ pub struct AnyAttribute {
|
||||
resolve: fn(Erased) -> Pin<Box<dyn Future<Output = AnyAttribute> + Send>>,
|
||||
#[cfg(feature = "ssr")]
|
||||
dry_resolve: fn(&mut Erased),
|
||||
keys: fn(&Erased) -> Vec<NamedAttributeKey>,
|
||||
}
|
||||
|
||||
impl Clone for AnyAttribute {
|
||||
@@ -44,6 +49,7 @@ pub struct AnyAttributeState {
|
||||
type_id: TypeId,
|
||||
state: ErasedLocal,
|
||||
el: crate::renderer::types::Element,
|
||||
keys: Vec<NamedAttributeKey>,
|
||||
}
|
||||
|
||||
/// Converts an [`Attribute`] into [`AnyAttribute`].
|
||||
@@ -84,6 +90,7 @@ where
|
||||
) -> AnyAttributeState {
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
keys: value.get_ref::<T>().keys(),
|
||||
state: ErasedLocal::new(value.into_inner::<T>().build(&el)),
|
||||
el,
|
||||
}
|
||||
@@ -96,6 +103,7 @@ where
|
||||
) -> AnyAttributeState {
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
keys: value.get_ref::<T>().keys(),
|
||||
state: ErasedLocal::new(
|
||||
value.into_inner::<T>().hydrate::<true>(&el),
|
||||
),
|
||||
@@ -110,6 +118,7 @@ where
|
||||
) -> AnyAttributeState {
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
keys: value.get_ref::<T>().keys(),
|
||||
state: ErasedLocal::new(
|
||||
value.into_inner::<T>().hydrate::<true>(&el),
|
||||
),
|
||||
@@ -140,6 +149,12 @@ where
|
||||
async move {value.into_inner::<T>().resolve().await.into_any_attr()}.boxed()
|
||||
}
|
||||
|
||||
fn keys<T: Attribute + 'static>(
|
||||
value: &Erased,
|
||||
) -> Vec<NamedAttributeKey> {
|
||||
value.get_ref::<T>().keys()
|
||||
}
|
||||
|
||||
let value = self.into_cloneable_owned();
|
||||
AnyAttribute {
|
||||
type_id: TypeId::of::<T::CloneableOwned>(),
|
||||
@@ -158,6 +173,7 @@ where
|
||||
resolve: resolve::<T::CloneableOwned>,
|
||||
#[cfg(feature = "ssr")]
|
||||
dry_resolve: dry_resolve::<T::CloneableOwned>,
|
||||
keys: keys::<T::CloneableOwned>,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,6 +284,10 @@ impl Attribute for AnyAttribute {
|
||||
enabled."
|
||||
);
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
(self.keys)(&self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl NextAttribute for Vec<AnyAttribute> {
|
||||
@@ -286,7 +306,7 @@ impl Attribute for Vec<AnyAttribute> {
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
type AsyncOutput = Vec<AnyAttribute>;
|
||||
type State = Vec<AnyAttributeState>;
|
||||
type State = (Element, Vec<AnyAttributeState>);
|
||||
type Cloneable = Vec<AnyAttribute>;
|
||||
type CloneableOwned = Vec<AnyAttribute>;
|
||||
|
||||
@@ -321,13 +341,19 @@ impl Attribute for Vec<AnyAttribute> {
|
||||
) -> Self::State {
|
||||
#[cfg(feature = "hydrate")]
|
||||
if FROM_SERVER {
|
||||
self.into_iter()
|
||||
.map(|attr| attr.hydrate::<true>(el))
|
||||
.collect()
|
||||
(
|
||||
el.clone(),
|
||||
self.into_iter()
|
||||
.map(|attr| attr.hydrate::<true>(el))
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
self.into_iter()
|
||||
.map(|attr| attr.hydrate::<false>(el))
|
||||
.collect()
|
||||
(
|
||||
el.clone(),
|
||||
self.into_iter()
|
||||
.map(|attr| attr.hydrate::<false>(el))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
{
|
||||
@@ -340,13 +366,34 @@ impl Attribute for Vec<AnyAttribute> {
|
||||
}
|
||||
|
||||
fn build(self, el: &crate::renderer::types::Element) -> Self::State {
|
||||
self.into_iter().map(|attr| attr.build(el)).collect()
|
||||
(
|
||||
el.clone(),
|
||||
self.into_iter().map(|attr| attr.build(el)).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
for (attr, state) in self.into_iter().zip(state.iter_mut()) {
|
||||
attr.rebuild(state)
|
||||
let (el, state) = state;
|
||||
for old in mem::take(state) {
|
||||
for key in old.keys {
|
||||
match key {
|
||||
NamedAttributeKey::InnerHtml => {
|
||||
Rndr::set_inner_html(&old.el, "");
|
||||
}
|
||||
NamedAttributeKey::Property(prop_name) => {
|
||||
Rndr::set_property(
|
||||
&old.el,
|
||||
&prop_name,
|
||||
&wasm_bindgen::JsValue::UNDEFINED,
|
||||
);
|
||||
}
|
||||
NamedAttributeKey::Attribute(key) => {
|
||||
Rndr::remove_attribute(&old.el, &key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*state = self.into_iter().map(|s| s.build(el)).collect();
|
||||
}
|
||||
|
||||
fn into_cloneable(self) -> Self::Cloneable {
|
||||
@@ -385,4 +432,8 @@ impl Attribute for Vec<AnyAttribute> {
|
||||
enabled."
|
||||
);
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
self.iter().flat_map(|s| s.keys()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::{
|
||||
use crate::{
|
||||
html::attribute::{
|
||||
maybe_next_attr_erasure_macros::next_attr_combine, Attribute,
|
||||
AttributeValue,
|
||||
AttributeValue, NamedAttributeKey,
|
||||
},
|
||||
view::{add_attr::AddAnyAttr, Position, ToTemplate},
|
||||
};
|
||||
@@ -112,6 +112,12 @@ where
|
||||
value: self.value.resolve().await,
|
||||
}
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![NamedAttributeKey::Attribute(
|
||||
self.key.as_ref().to_string().into(),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> NextAttribute for CustomAttr<K, V>
|
||||
|
||||
@@ -15,7 +15,7 @@ pub use key::*;
|
||||
use maybe_next_attr_erasure_macros::{
|
||||
next_attr_combine, next_attr_output_type,
|
||||
};
|
||||
use std::{fmt::Debug, future::Future};
|
||||
use std::{borrow::Cow, fmt::Debug, future::Future};
|
||||
pub use value::*;
|
||||
|
||||
/// Defines an attribute: anything that can modify an element.
|
||||
@@ -75,6 +75,25 @@ pub trait Attribute: NextAttribute + Send {
|
||||
|
||||
/// “Resolves” this into a type that is not waiting for any asynchronous data.
|
||||
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send;
|
||||
|
||||
/// Returns a set of attribute keys, associated with this attribute, if any.
|
||||
///
|
||||
/// This is only used to manage the removal of type-erased attributes, when needed.
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
// TODO: remove default implementation in 0.9, or fix this whole approach
|
||||
// by making it easier to remove attributes
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
/// An attribute key can be used to remove an attribute from an element.
|
||||
pub enum NamedAttributeKey {
|
||||
/// An ordinary attribute.
|
||||
Attribute(Cow<'static, str>),
|
||||
/// A DOM property.
|
||||
Property(Cow<'static, str>),
|
||||
/// The `inner_html` pseudo-attribute.
|
||||
InnerHtml,
|
||||
}
|
||||
|
||||
/// Adds another attribute to this one, returning a new attribute.
|
||||
@@ -133,6 +152,10 @@ impl Attribute for () {
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl NextAttribute for () {
|
||||
@@ -249,6 +272,10 @@ where
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
Attr(self.0, self.1.resolve().await)
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![NamedAttributeKey::Attribute(K::KEY.into())]
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> NextAttribute for Attr<K, V>
|
||||
@@ -353,6 +380,14 @@ macro_rules! impl_attr_for_tuples {
|
||||
$($ty.resolve()),*
|
||||
)
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
#[allow(non_snake_case)]
|
||||
let ($first, $($ty,)*) = &self;
|
||||
let mut buf = $first.keys();
|
||||
$(buf.extend($ty.keys());)*
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl<$first, $($ty),*> NextAttribute for ($first, $($ty,)*)
|
||||
@@ -462,6 +497,14 @@ macro_rules! impl_attr_for_tuples_truncate_additional {
|
||||
$($ty.resolve()),*
|
||||
)
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
#[allow(non_snake_case)]
|
||||
let ($first, $($ty,)*) = &self;
|
||||
let mut buf = $first.keys();
|
||||
$(buf.extend($ty.keys());)*
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl<$first, $($ty),*> NextAttribute for ($first, $($ty,)*)
|
||||
@@ -538,6 +581,10 @@ where
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
(self.0.resolve().await,)
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
self.0.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> NextAttribute for (A,)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::attribute::{
|
||||
maybe_next_attr_erasure_macros::next_attr_output_type, Attribute,
|
||||
NextAttribute,
|
||||
NamedAttributeKey, NextAttribute,
|
||||
};
|
||||
use crate::{
|
||||
html::attribute::maybe_next_attr_erasure_macros::next_attr_combine,
|
||||
@@ -97,6 +97,10 @@ where
|
||||
class: self.class.resolve().await,
|
||||
}
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![NamedAttributeKey::Attribute("class".into())]
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> NextAttribute for Class<C>
|
||||
|
||||
@@ -3,7 +3,9 @@ use super::attribute::{
|
||||
NextAttribute,
|
||||
};
|
||||
use crate::{
|
||||
html::attribute::maybe_next_attr_erasure_macros::next_attr_combine,
|
||||
html::attribute::{
|
||||
maybe_next_attr_erasure_macros::next_attr_combine, NamedAttributeKey,
|
||||
},
|
||||
prelude::AddAnyAttr,
|
||||
view::{Position, ToTemplate},
|
||||
};
|
||||
@@ -160,6 +162,10 @@ where
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, D, P> NextAttribute for Directive<T, D, P>
|
||||
|
||||
@@ -227,7 +227,7 @@ html_self_closing_elements! {
|
||||
|
||||
html_elements! {
|
||||
/// The `<a>` HTML element (or anchor element), with its href attribute, creates a hyperlink to web pages, files, email addresses, locations in the same page, or anything else a URL can address.
|
||||
a HtmlAnchorElement [download, href, hreflang, ping, rel, target, r#type ] true,
|
||||
a HtmlAnchorElement [download, href, hreflang, ping, referrerpolicy, rel, target, r#type ] true,
|
||||
/// The `<abbr>` HTML element represents an abbreviation or acronym; the optional title attribute can provide an expansion or description for the abbreviation. If present, title must contain this full description and nothing else.
|
||||
abbr HtmlElement [] true,
|
||||
/// The `<address>` HTML element indicates that the enclosed HTML provides contact information for a person or people, or for an organization.
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
maybe_next_attr_erasure_macros::{
|
||||
next_attr_combine, next_attr_output_type,
|
||||
},
|
||||
Attribute, NextAttribute,
|
||||
Attribute, NamedAttributeKey, NextAttribute,
|
||||
},
|
||||
renderer::Rndr,
|
||||
view::add_attr::AddAnyAttr,
|
||||
@@ -105,6 +105,10 @@ where
|
||||
value: self.value.resolve().await,
|
||||
}
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![NamedAttributeKey::InnerHtml]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> NextAttribute for InnerHtml<T>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
html::attribute::{
|
||||
maybe_next_attr_erasure_macros::next_attr_combine, Attribute,
|
||||
NamedAttributeKey,
|
||||
},
|
||||
renderer::{CastFrom, RemoveEventHandler, Rndr},
|
||||
view::{Position, ToTemplate},
|
||||
@@ -110,6 +111,8 @@ where
|
||||
{
|
||||
On {
|
||||
event,
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
owner: reactive_graph::owner::Owner::current().unwrap_or_default(),
|
||||
cb: Some(SendWrapper::new(cb)),
|
||||
}
|
||||
}
|
||||
@@ -135,6 +138,8 @@ where
|
||||
/// An [`Attribute`] that adds an event listener to an element.
|
||||
pub struct On<E, F> {
|
||||
event: E,
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
owner: reactive_graph::owner::Owner,
|
||||
cb: Option<SendWrapper<F>>,
|
||||
}
|
||||
|
||||
@@ -146,6 +151,8 @@ where
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
event: self.event.clone(),
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
owner: self.owner.clone(),
|
||||
cb: self.cb.clone(),
|
||||
}
|
||||
}
|
||||
@@ -193,6 +200,10 @@ where
|
||||
let _tracing_guard = span.enter();
|
||||
|
||||
let ev = E::EventType::from(ev);
|
||||
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
self.owner.with(|| cb.invoke(ev));
|
||||
#[cfg(not(feature = "reactive_graph"))]
|
||||
cb.invoke(ev);
|
||||
}) as Box<dyn FnMut(crate::renderer::types::Event)>;
|
||||
|
||||
@@ -232,6 +243,10 @@ where
|
||||
let _tracing_guard = span.enter();
|
||||
|
||||
let ev = E::EventType::from(ev);
|
||||
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
self.owner.with(|| cb.invoke(ev));
|
||||
#[cfg(not(feature = "reactive_graph"))]
|
||||
cb.invoke(ev);
|
||||
}) as Box<dyn FnMut(crate::renderer::types::Event)>;
|
||||
|
||||
@@ -320,6 +335,8 @@ where
|
||||
fn into_cloneable(self) -> Self::Cloneable {
|
||||
On {
|
||||
cb: self.cb.map(|cb| SendWrapper::new(cb.take().into_shared())),
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
owner: self.owner,
|
||||
event: self.event,
|
||||
}
|
||||
}
|
||||
@@ -327,6 +344,8 @@ where
|
||||
fn into_cloneable_owned(self) -> Self::CloneableOwned {
|
||||
On {
|
||||
cb: self.cb.map(|cb| SendWrapper::new(cb.take().into_shared())),
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
owner: self.owner,
|
||||
event: self.event,
|
||||
}
|
||||
}
|
||||
@@ -342,6 +361,10 @@ where
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, F> NextAttribute for On<E, F>
|
||||
|
||||
@@ -7,7 +7,10 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
html::{
|
||||
attribute::maybe_next_attr_erasure_macros::next_attr_combine,
|
||||
attribute::{
|
||||
maybe_next_attr_erasure_macros::next_attr_combine,
|
||||
NamedAttributeKey,
|
||||
},
|
||||
element::HtmlElement,
|
||||
},
|
||||
prelude::Render,
|
||||
@@ -112,6 +115,10 @@ where
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, C> NextAttribute for NodeRefAttr<E, C>
|
||||
|
||||
@@ -3,7 +3,9 @@ use super::attribute::{
|
||||
NextAttribute,
|
||||
};
|
||||
use crate::{
|
||||
html::attribute::maybe_next_attr_erasure_macros::next_attr_combine,
|
||||
html::attribute::{
|
||||
maybe_next_attr_erasure_macros::next_attr_combine, NamedAttributeKey,
|
||||
},
|
||||
renderer::Rndr,
|
||||
view::{Position, ToTemplate},
|
||||
};
|
||||
@@ -124,6 +126,12 @@ where
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![NamedAttributeKey::Property(
|
||||
self.key.as_ref().to_string().into(),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, P> NextAttribute for Property<K, P>
|
||||
|
||||
@@ -5,7 +5,9 @@ use super::attribute::{
|
||||
#[cfg(all(feature = "nightly", rustc_nightly))]
|
||||
use crate::view::static_types::Static;
|
||||
use crate::{
|
||||
html::attribute::maybe_next_attr_erasure_macros::next_attr_combine,
|
||||
html::attribute::{
|
||||
maybe_next_attr_erasure_macros::next_attr_combine, NamedAttributeKey,
|
||||
},
|
||||
renderer::{dom::CssStyleDeclaration, Rndr},
|
||||
view::{Position, ToTemplate},
|
||||
};
|
||||
@@ -100,6 +102,10 @@ where
|
||||
style: self.style.resolve().await,
|
||||
}
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![NamedAttributeKey::Attribute("style".into())]
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> NextAttribute for Style<S>
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
all(feature = "nightly", rustc_nightly),
|
||||
feature(unsized_const_params)
|
||||
)]
|
||||
// support for const generic &'static str has now moved back and forth between
|
||||
// these two features a couple times; we'll just enable both
|
||||
#![cfg_attr(all(feature = "nightly", rustc_nightly), feature(adt_const_params))]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
/// Commonly-used traits.
|
||||
|
||||
@@ -5,7 +5,8 @@ use crate::{
|
||||
maybe_next_attr_erasure_macros::{
|
||||
next_attr_combine, next_attr_output_type,
|
||||
},
|
||||
Attribute, AttributeKey, AttributeValue, NextAttribute,
|
||||
Attribute, AttributeKey, AttributeValue, NamedAttributeKey,
|
||||
NextAttribute,
|
||||
},
|
||||
event::{change, input, on},
|
||||
property::{prop, IntoProperty},
|
||||
@@ -275,6 +276,10 @@ where
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl<Key, T, R, W> NextAttribute for Bind<Key, T, R, W>
|
||||
|
||||
@@ -699,7 +699,15 @@ impl Render for AnyViewWithAttrs {
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.view.rebuild(&mut state.view);
|
||||
self.attrs.rebuild(&mut state.attrs);
|
||||
|
||||
let elements = state.elements();
|
||||
// FIXME this seems wrong but I think the previous version was also broken!
|
||||
if let Some(element) = elements.first() {
|
||||
self.attrs.rebuild(&mut (
|
||||
element.clone(),
|
||||
std::mem::take(&mut state.attrs),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,7 +828,7 @@ impl AddAnyAttr for AnyViewWithAttrs {
|
||||
}
|
||||
}
|
||||
|
||||
/// wip
|
||||
/// State for any view with attributes spread onto it.
|
||||
pub struct AnyViewWithAttrsState {
|
||||
view: AnyViewState,
|
||||
attrs: Vec<AnyAttributeState>,
|
||||
|
||||
@@ -3,7 +3,10 @@ use super::{
|
||||
Render, RenderHtml,
|
||||
};
|
||||
use crate::{
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute, NextAttribute},
|
||||
html::attribute::{
|
||||
any_attribute::AnyAttribute, Attribute, NamedAttributeKey,
|
||||
NextAttribute,
|
||||
},
|
||||
hydration::Cursor,
|
||||
ssr::StreamBuilder,
|
||||
};
|
||||
@@ -264,6 +267,13 @@ where
|
||||
Either::Right(right) => Either::Right(right.resolve().await),
|
||||
}
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
match self {
|
||||
Either::Left(left) => left.keys(),
|
||||
Either::Right(right) => right.keys(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> RenderHtml for Either<A, B>
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::{
|
||||
maybe_next_attr_erasure_macros::{
|
||||
next_attr_combine, next_attr_output_type,
|
||||
},
|
||||
Attribute, AttributeKey, AttributeValue, NextAttribute,
|
||||
Attribute, AttributeKey, AttributeValue, NamedAttributeKey,
|
||||
NextAttribute,
|
||||
},
|
||||
hydration::Cursor,
|
||||
renderer::{CastFrom, Rndr},
|
||||
@@ -111,6 +112,10 @@ where
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
fn keys(&self) -> Vec<NamedAttributeKey> {
|
||||
vec![NamedAttributeKey::Attribute(K::KEY.into())]
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, const V: &'static str> NextAttribute for StaticAttr<K, V>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wasm_split_macros"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -85,6 +85,7 @@ pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
import_sig.output = async_output;
|
||||
}
|
||||
|
||||
let wrapper_pub = item_fn.vis;
|
||||
let mut wrapper_sig = item_fn.sig;
|
||||
wrapper_sig.asyncness = Some(Default::default());
|
||||
let mut args = Vec::new();
|
||||
@@ -147,7 +148,8 @@ pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#wrapper_sig {
|
||||
#(#attrs)*
|
||||
#wrapper_pub #wrapper_sig {
|
||||
#(#attrs)*
|
||||
#[allow(improper_ctypes_definitions)]
|
||||
#[allow(non_snake_case)]
|
||||
@@ -162,7 +164,7 @@ pub fn wasm_split(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn #preload_name() {
|
||||
#wrapper_pub async fn #preload_name() {
|
||||
::leptos::wasm_split_helpers::ensure_loaded(&#split_loader_ident).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user