mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
39 Commits
script-ord
...
docs-set-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123b1ef24d | ||
|
|
2d418dae93 | ||
|
|
91e0fcdc1b | ||
|
|
a9ed8461d1 | ||
|
|
5a71ca797a | ||
|
|
70eb07d7d6 | ||
|
|
71ee69af01 | ||
|
|
dd41c0586c | ||
|
|
aaf63dbf5c | ||
|
|
87f6802967 | ||
|
|
2cbf3581c5 | ||
|
|
5a67e208fd | ||
|
|
3391a4a035 | ||
|
|
076aa363a4 | ||
|
|
2cb68c0bd4 | ||
|
|
6eb24b5017 | ||
|
|
b2faa6b86c | ||
|
|
43990b5b67 | ||
|
|
9453164dd2 | ||
|
|
00fcd1c65e | ||
|
|
85ad7b0f38 | ||
|
|
f0a9940364 | ||
|
|
b472aaf6a0 | ||
|
|
059c1bf61c | ||
|
|
add13fd6a4 | ||
|
|
904c2e8a67 | ||
|
|
a5c3be586a | ||
|
|
9f5139d929 | ||
|
|
bae305340e | ||
|
|
40c1556f29 | ||
|
|
0db4f5821f | ||
|
|
12ebc95800 | ||
|
|
d7b919032e | ||
|
|
be8bf8b0d6 | ||
|
|
f84f1422f4 | ||
|
|
b01976e3bb | ||
|
|
50b48fb272 | ||
|
|
1617e31d69 | ||
|
|
51cd082d4c |
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.0-alpha"
|
||||
version = "0.3.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0-alpha" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0-alpha" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0-alpha" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0-alpha" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_router = { path = "./router", version = "0.3.0-alpha" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0-alpha" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.0" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0" }
|
||||
leptos_router = { path = "./router", version = "0.3.0" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -69,7 +69,11 @@ dependencies = [
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all", "test-leptos_macro-example", "doc-leptos_macro-example"]
|
||||
dependencies = [
|
||||
"test-all",
|
||||
"test-leptos_macro-example",
|
||||
"doc-leptos_macro-example",
|
||||
]
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
@@ -102,9 +106,15 @@ cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "verify-flow"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
description = "Clean all example projects"
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "clean-all"]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME="ci" # allows examples to check/build without cargo-leptos
|
||||
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
@@ -41,6 +41,14 @@ build-std = ["std", "panic_abort", "core", "alloc"]
|
||||
build-std-features = ["panic_immediate_abort"]
|
||||
```
|
||||
|
||||
Note that if you're using this with SSR too, the same Cargo profile will be applied. You'll need to explicitly specify your target:
|
||||
```toml
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-gnu" # or whatever
|
||||
```
|
||||
|
||||
And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`. Note that this applies the same `build-std` and panic settings to your server binary, which may not be desirable. Some further exploration is probably needed here.
|
||||
|
||||
5. One of the sources of binary size in WASM binaries can be `serde` serialization/deserialization code. Leptos uses `serde` by default to serialize and deserialize resources created with `create_resource`. You might try experimenting with the `miniserde` and `serde-lite` features, which allow you to use those crates for serialization and deserialization instead; each only implements a subset of `serde`’s functionality, but typically optimizes for size over speed.
|
||||
|
||||
## Things to Avoid
|
||||
|
||||
@@ -90,7 +90,8 @@ view! { cx,
|
||||
<button
|
||||
// define an event listener with on:
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
// on stable, this is set_count.set(3);
|
||||
set_count(3);
|
||||
}
|
||||
>
|
||||
// text nodes are wrapped in quotation marks
|
||||
@@ -142,6 +143,16 @@ in a function, telling the framework to update the view every time `count` chang
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
|
||||
|
||||
Let’s make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Let’s replacing “set this value to 3” with “increment this value by 1”:
|
||||
|
||||
```rust
|
||||
move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
```
|
||||
|
||||
You can see here that while `set_count` just sets the value, `set_count.update()` gives us a mutable reference and mutates the value in place. Either one will trigger a reactive update in our UI.
|
||||
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
> show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
|
||||
@@ -15,7 +15,7 @@ You _could_ do this by just creating two `<progress>` elements:
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! {
|
||||
view! { cx,
|
||||
<progress
|
||||
max="50"
|
||||
value=count
|
||||
|
||||
@@ -31,7 +31,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
|
||||
[tasks.verify-flow]
|
||||
description = "Provides pre and post hooks for verify"
|
||||
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
|
||||
dependencies = ["pre-verify", "verify", "post-verify"]
|
||||
|
||||
[tasks.verify]
|
||||
description = "Run all quality checks and tests"
|
||||
@@ -41,16 +41,17 @@ dependencies = ["check-style", "test-unit-and-web"]
|
||||
description = "Run all unit and web tests"
|
||||
dependencies = ["test-flow", "web-test-flow"]
|
||||
|
||||
[tasks.pre-verify-flow]
|
||||
[tasks.pre-verify]
|
||||
|
||||
[tasks.post-verify-flow]
|
||||
[tasks.post-verify]
|
||||
dependencies = ["clean-all"]
|
||||
|
||||
[tasks.web-test-flow]
|
||||
description = "Provides pre and post hooks for web-test"
|
||||
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
|
||||
dependencies = ["pre-web-test", "web-test", "post-web-test"]
|
||||
|
||||
[tasks.pre-web-test-flow]
|
||||
[tasks.pre-web-test]
|
||||
|
||||
[tasks.web-test]
|
||||
|
||||
[tasks.post-web-test-flow]
|
||||
[tasks.post-web-test]
|
||||
|
||||
5
examples/cargo-make/cargo-leptos-web-test.toml
Normal file
5
examples/cargo-make/cargo-leptos-web-test.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[tasks.web-test]
|
||||
dependencies = ["auto-setup", "cargo-leptos-e2e"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]
|
||||
@@ -1,3 +1,6 @@
|
||||
[env]
|
||||
END2END_DIR = "end2end"
|
||||
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
|
||||
@@ -5,6 +8,9 @@ env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
description = "Check for style violations"
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.check-format]
|
||||
args = ["fmt", "--", "--check", "--config-path", "../../"]
|
||||
|
||||
[tasks.verify-local]
|
||||
description = "Run all quality checks and tests from an example directory"
|
||||
dependencies = ["check-style", "test-local"]
|
||||
@@ -12,3 +18,70 @@ dependencies = ["check-style", "test-local"]
|
||||
[tasks.test-local]
|
||||
description = "Run all tests from an example directory"
|
||||
dependencies = ["test", "web-test"]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
description = "Runs the cargo clean command."
|
||||
category = "Cleanup"
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
description = "Runs the trunk clean command."
|
||||
category = "Cleanup"
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-node_modules]
|
||||
description = "Delete all node_modules directories"
|
||||
category = "Cleanup"
|
||||
script = '''
|
||||
find . -type d -name node_modules | xargs rm -rf
|
||||
'''
|
||||
|
||||
[tasks.clean-playwright]
|
||||
description = "Delete playwright directories"
|
||||
category = "Cleanup"
|
||||
cwd = "${END2END_DIR}"
|
||||
command = "rm"
|
||||
args = ["-rf", "playwright", "playwright/.cache", "test-results"]
|
||||
|
||||
[tasks.clean-all]
|
||||
description = "Delete all temporary directories"
|
||||
category = "Cleanup"
|
||||
dependencies = ["clean-cargo"]
|
||||
|
||||
[tasks.wasm-web-test]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
description = "Runs end to end tests with cargo leptos"
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
[tasks.setup]
|
||||
description = "Setup e2e dependencies"
|
||||
cwd = "${END2END_DIR}"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
if command -v pnpm; then
|
||||
pnpm install
|
||||
elif command -v npm; then
|
||||
npm install
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.auto-setup]
|
||||
script = '''
|
||||
if [ ! -d "${END2END_DIR}/node_modules" ]; then
|
||||
cargo make setup
|
||||
fi
|
||||
'''
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[tasks.web-test]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
dependencies = ["wasm-web-test"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-trunk"]
|
||||
|
||||
@@ -10,12 +10,12 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
|
||||
@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -14,7 +14,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::landing::App;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
@@ -5,7 +5,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use crate::landing::*;
|
||||
use axum::body::Body as AxumBody;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
extract::{State, Path},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
@@ -21,7 +21,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
Extension(options): Extension<Arc<LeptosOptions>>,
|
||||
State(options): State<Arc<LeptosOptions>>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
@@ -44,7 +44,7 @@ async fn main() {
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -58,7 +58,7 @@ async fn main() {
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{component, view, IntoView, Scope};
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
@@ -9,23 +9,25 @@ use routes::{nav::*, stories::*, story::*, users::*};
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
view! {
|
||||
cx,
|
||||
<>
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
|
||||
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
|
||||
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</>
|
||||
let (is_routing, set_is_routing) = create_signal(cx, false);
|
||||
|
||||
view! { cx,
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
// adding `set_is_routing` causes the router to wait for async data to load on new pages
|
||||
<Router set_is_routing>
|
||||
// shows a progress bar while async data are loading
|
||||
<RoutingProgress is_routing max_time=std::time::Duration::from_millis(250)/>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
|
||||
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
|
||||
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -15,13 +15,13 @@ if #[cfg(feature = "ssr")] {
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
|
||||
handler(req).await.into_response()
|
||||
|
||||
@@ -7,7 +7,6 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
extract::Extension,
|
||||
};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
@@ -18,7 +17,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use hackernews_axum::*;
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -29,7 +28,7 @@ if #[cfg(feature = "ssr")] {
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -4,7 +4,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -16,13 +16,13 @@ if #[cfg(feature = "ssr")] {
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::errors::TodoAppError;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
res.into_response()
|
||||
} else{
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
|
||||
@@ -6,7 +6,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{Response, IntoResponse},
|
||||
routing::get,
|
||||
extract::{Path, Extension, RawQuery},
|
||||
extract::{Path, State, Extension, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
@@ -33,7 +33,7 @@ if #[cfg(feature = "ssr")] {
|
||||
}, request).await
|
||||
}
|
||||
|
||||
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, auth_session.clone());
|
||||
@@ -68,7 +68,7 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
|
||||
@@ -80,8 +80,8 @@ if #[cfg(feature = "ssr")] {
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
.with_config(auth_config))
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.layer(Extension(Arc::new(leptos_options)))
|
||||
.layer(Extension(pool));
|
||||
.layer(Extension(pool))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -14,13 +14,13 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::app::App;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{extract::Extension, routing::post, Router};
|
||||
use axum::{routing::post, Router};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
|
||||
@@ -9,7 +9,7 @@ async fn main() {
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -24,7 +24,7 @@ async fn main() {
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
7
examples/tailwind/.gitignore
vendored
7
examples/tailwind/.gitignore
vendored
@@ -8,3 +8,10 @@ Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
|
||||
@@ -96,6 +96,7 @@ site-addr = "127.0.0.1:3000"
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
|
||||
@@ -104,3 +104,8 @@ You'll need to install trunk to client side render this bundle.
|
||||
## Attribution
|
||||
|
||||
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/leptos-rs/leptos/discussions/125).
|
||||
|
||||
## Playwright Testing
|
||||
|
||||
- Run `cargo make setup` to install dependencies
|
||||
- Run `cargo leptos test` or `cargo leptos end-to-end` to run the tests
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
test("should see the welcome message", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
|
||||
await expect(page).toHaveTitle("Cargo Leptos");
|
||||
|
||||
await expect(page.locator("h1")).toHaveText("Hi from your Leptos WASM!");
|
||||
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ This is a template demonstrating how to integrate [TailwindCSS](https://tailwind
|
||||
|
||||
Install Tailwind and build the CSS:
|
||||
|
||||
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
|
||||
`Trunk.toml` is configured to build the CSS automatically.
|
||||
|
||||
Install trunk to client side render this bundle.
|
||||
|
||||
|
||||
4
examples/tailwind_csr_trunk/Trunk.toml
Normal file
4
examples/tailwind_csr_trunk/Trunk.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[[hooks]]
|
||||
stage = "pre_build"
|
||||
command = "sh"
|
||||
command_arguments = ["-c", "npx tailwindcss -i input.css -o style/output.css"]
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
|
||||
! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -31,6 +31,7 @@
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
@@ -47,6 +48,8 @@ html {
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
font-variation-settings: normal;
|
||||
/* 6 */
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -433,6 +436,9 @@ video {
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
@@ -480,6 +486,9 @@ video {
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
|
||||
@@ -4,7 +4,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -16,13 +16,13 @@ if #[cfg(feature = "ssr")] {
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::errors::TodoAppError;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
res.into_response()
|
||||
} else{
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
|
||||
@@ -5,7 +5,7 @@ cfg_if! {
|
||||
use leptos::*;
|
||||
use axum::{
|
||||
routing::{post, get},
|
||||
extract::{Extension, Path},
|
||||
extract::{State, Path},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
Router,
|
||||
@@ -18,7 +18,7 @@ cfg_if! {
|
||||
use std::sync::Arc;
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
async fn custom_handler(Path(id): Path<String>, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, id.clone());
|
||||
@@ -42,7 +42,7 @@ cfg_if! {
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
|
||||
@@ -52,7 +52,7 @@ cfg_if! {
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -146,11 +146,8 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Todos/>
|
||||
}/> //Route
|
||||
<Route path="weird" methods=&[Method::Get, Method::Post]
|
||||
ssr=SsrMode::Async
|
||||
@@ -203,63 +200,65 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ impl Todos {
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: Uuid) {
|
||||
self.0.retain(|todo| todo.id != id);
|
||||
self.retain(|todo| todo.id != id);
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
@@ -76,7 +76,23 @@ impl Todos {
|
||||
}
|
||||
|
||||
fn clear_completed(&mut self) {
|
||||
self.0.retain(|todo| !todo.completed.get());
|
||||
self.retain(|todo| !todo.completed.get());
|
||||
}
|
||||
|
||||
fn retain(&mut self, mut f: impl FnMut(&Todo) -> bool) {
|
||||
self.0.retain(|todo| {
|
||||
let retain = f(todo);
|
||||
// because these signals are created at the top level,
|
||||
// they are owned by the <TodoMVC/> component and not
|
||||
// by the individual <Todo/> components. This means
|
||||
// that if they are not manually disposed when removed, they
|
||||
// will be held onto until the <TodoMVC/> is unmounted.
|
||||
if !retain {
|
||||
todo.title.dispose();
|
||||
todo.completed.dispose();
|
||||
}
|
||||
retain
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +152,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
|
||||
// Handle the three filter modes: All, Active, and Completed
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener_untyped("hashchange", move |_| {
|
||||
window_event_listener(ev::hashchange, move |_| {
|
||||
let new_mode =
|
||||
location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
|
||||
@@ -8,6 +8,7 @@ repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Actix integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3"
|
||||
actix-web = "4"
|
||||
futures = "0.3"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
|
||||
@@ -16,9 +16,9 @@ use actix_web::{
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::StatusCode;
|
||||
use leptos::{
|
||||
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement,
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
@@ -185,7 +185,7 @@ pub fn handle_server_fns_with_context(
|
||||
.and_then(|value| value.to_str().ok());
|
||||
|
||||
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
|
||||
let body: &[u8] = &body;
|
||||
let body_ref: &[u8] = &body;
|
||||
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
@@ -198,10 +198,28 @@ pub fn handle_server_fns_with_context(
|
||||
provide_context(cx, req.clone());
|
||||
provide_context(cx, res_options.clone());
|
||||
|
||||
// we consume the body here (using the web::Bytes extractor), but it is required for things
|
||||
// like MultipartForm
|
||||
if req
|
||||
.headers()
|
||||
.get("Content-Type")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| {
|
||||
Some(
|
||||
value.starts_with(
|
||||
"multipart/form-data; boundary=",
|
||||
),
|
||||
)
|
||||
})
|
||||
== Some(true)
|
||||
{
|
||||
provide_context(cx, body.clone());
|
||||
}
|
||||
|
||||
let query = req.query_string().as_bytes();
|
||||
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => body,
|
||||
Encoding::Url | Encoding::Cbor => body_ref,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
let res = match (server_fn.trait_obj)(cx, data).await {
|
||||
@@ -514,6 +532,43 @@ pub fn render_app_to_stream_with_context<IV>(
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options,
|
||||
additional_context,
|
||||
app_fn,
|
||||
method,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This function allows you to provide additional information to Leptos for your route.
|
||||
/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
|
||||
///
|
||||
/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
|
||||
/// from blocking resources should be retrojected into the HTML that's initially served, rather
|
||||
/// than dynamically inserting them with JavaScript on the client. This means you will have
|
||||
/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
replace_blocks: bool,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
@@ -533,7 +588,14 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
stream_app(&options, app, res_options, additional_context).await
|
||||
stream_app(
|
||||
&options,
|
||||
app,
|
||||
res_options,
|
||||
additional_context,
|
||||
replace_blocks,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
match method {
|
||||
@@ -653,103 +715,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
|
||||
/// includes everything described in the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos_actix::DataResponse;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope, data: &'static str) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_preloaded_data_app(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |req| async move {
|
||||
/// Ok(DataResponse::Data(
|
||||
/// "async func that can preload data",
|
||||
/// ))
|
||||
/// },
|
||||
/// |cx, data| view! { cx, <MyApp data/> },
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[deprecated = "You can now use `render_app_async` with `create_resource` and \
|
||||
`<Suspense/>` to achieve async rendering without manually \
|
||||
preloading data."]
|
||||
pub fn render_preloaded_data_app<Data, Fut, IV>(
|
||||
options: LeptosOptions,
|
||||
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
|
||||
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
|
||||
) -> Route
|
||||
where
|
||||
Data: 'static,
|
||||
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
web::get().to(move |req: HttpRequest| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let data_fn = data_fn.clone();
|
||||
let res_options = ResponseOptions::default();
|
||||
|
||||
async move {
|
||||
let data = match data_fn(req.clone()).await {
|
||||
Err(e) => return HttpResponse::from_error(e),
|
||||
Ok(DataResponse::Response(r)) => return r.into(),
|
||||
Ok(DataResponse::Data(d)) => d,
|
||||
};
|
||||
|
||||
let app = {
|
||||
let app_fn = app_fn.clone();
|
||||
let res_options = res_options.clone();
|
||||
move |cx| {
|
||||
provide_contexts(cx, &req, res_options);
|
||||
(app_fn)(cx, data).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
stream_app(&options, app, res_options, |_cx| {}).await
|
||||
}
|
||||
})
|
||||
}
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn provide_contexts(
|
||||
cx: leptos::Scope,
|
||||
@@ -781,12 +746,14 @@ async fn stream_app(
|
||||
app: impl FnOnce(leptos::Scope) -> View + 'static,
|
||||
res_options: ResponseOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
replace_blocks: bool,
|
||||
) -> HttpResponse<BoxBody> {
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_with_prefix_undisposed_with_context(
|
||||
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
app,
|
||||
move |cx| generate_head_metadata_separated(cx).1.into(),
|
||||
additional_context,
|
||||
replace_blocks
|
||||
);
|
||||
|
||||
build_stream_response(options, res_options, stream, runtime, scope).await
|
||||
@@ -985,22 +952,6 @@ pub trait LeptosRoutes {
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
|
||||
#[deprecated = "You can now use `leptos_routes` and a `<Route \
|
||||
mode=SsrMode::Async/>`
|
||||
to achieve async rendering without manually preloading \
|
||||
data."]
|
||||
fn leptos_preloaded_data_routes<Data, Fut, IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<String>,
|
||||
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
|
||||
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Data: 'static,
|
||||
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
|
||||
IV: IntoView + 'static;
|
||||
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
@@ -1035,34 +986,7 @@ where
|
||||
{
|
||||
self.leptos_routes_with_context(options, paths, |_| {}, app_fn)
|
||||
}
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn leptos_preloaded_data_routes<Data, Fut, IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<String>,
|
||||
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
|
||||
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Data: 'static,
|
||||
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
|
||||
for path in paths.iter() {
|
||||
router = router.route(
|
||||
path,
|
||||
#[allow(deprecated)]
|
||||
render_preloaded_data_app(
|
||||
options.clone(),
|
||||
data_fn.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
router
|
||||
}
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
@@ -1091,6 +1015,15 @@ where
|
||||
method,
|
||||
)
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
true,
|
||||
)
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
@@ -1113,7 +1046,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This takes
|
||||
/// A helper to make it easier to use Actix extractors in server functions. This takes
|
||||
/// a handler function as its argument. The handler follows similar rules to an Actix
|
||||
/// [Handler](actix_web::Handler): it is an async function that receives arguments that
|
||||
/// will be extracted from the request and returns some value.
|
||||
@@ -1157,9 +1090,17 @@ where
|
||||
{
|
||||
let req = use_context::<actix_web::HttpRequest>(cx)
|
||||
.expect("HttpRequest should have been provided via context");
|
||||
let input = E::extract(&req)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
|
||||
let input = if let Some(body) = use_context::<Bytes>(cx) {
|
||||
let (_, mut payload) = actix_http::h1::Payload::create(false);
|
||||
payload.unread_data(body);
|
||||
E::from_request(&req, &mut dev::Payload::from(payload))
|
||||
} else {
|
||||
E::extract(&req)
|
||||
}
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
|
||||
Ok(f.call(input).await)
|
||||
}
|
||||
|
||||
|
||||
@@ -129,23 +129,6 @@ pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
/// and other data without consuming the body.
|
||||
#[deprecated(note = "Replaced with generate_request_and_parts() to allow for \
|
||||
putting LeptosRequest in the Context")]
|
||||
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
// provide request headers as context in server scope
|
||||
let (parts, body) = req.into_parts();
|
||||
let body = body::to_bytes(body).await.unwrap_or_default();
|
||||
RequestParts {
|
||||
method: parts.method,
|
||||
uri: parts.uri,
|
||||
headers: parts.headers,
|
||||
version: parts.version,
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
/// and other data without consuming the body. Creates a new Request from the
|
||||
/// original parts for further processing
|
||||
@@ -622,6 +605,54 @@ pub fn render_app_to_stream_with_context<IV>(
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options,
|
||||
additional_context,
|
||||
app_fn,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure.
|
||||
///
|
||||
/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
|
||||
/// from blocking resources should be retrojected into the HTML that's initially served, rather
|
||||
/// than dynamically inserting them with JavaScript on the client. This means you will have
|
||||
/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
|
||||
///
|
||||
/// Otherwise, this function is identical to [render_app_to_stream_with_context].
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
>,
|
||||
> + Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
move |req: Request<Body>| {
|
||||
Box::pin({
|
||||
@@ -651,10 +682,11 @@ where
|
||||
}
|
||||
};
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
replace_blocks
|
||||
);
|
||||
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
@@ -663,6 +695,7 @@ where
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
async fn generate_response(
|
||||
res_options: ResponseOptions,
|
||||
@@ -1093,12 +1126,39 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait allows one to use your custom struct in Axum's router, provided it can provide the
|
||||
/// `LeptosOptions` to use for the `LeptosRoutes` trait functions.
|
||||
pub trait LeptosOptionProvider {
|
||||
fn options(&self) -> LeptosOptions;
|
||||
}
|
||||
|
||||
/// Implement `LeptosOptionProvider` trait for `LeptosOptions` itself.
|
||||
impl LeptosOptionProvider for LeptosOptions {
|
||||
fn options(&self) -> LeptosOptions {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement `LeptosOptionProvider` trait for any type wrapped in an Arc, if that type implements
|
||||
/// `LeptosOptionProvider` as states in axum are often provided wrapped in an Arc.
|
||||
impl<T> LeptosOptionProvider for Arc<T>
|
||||
where
|
||||
T: LeptosOptionProvider,
|
||||
{
|
||||
fn options(&self) -> LeptosOptions {
|
||||
(**self).options()
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
|
||||
/// having to use wildcards or manually define all routes in multiple places.
|
||||
pub trait LeptosRoutes {
|
||||
pub trait LeptosRoutes<OP>
|
||||
where
|
||||
OP: LeptosOptionProvider,
|
||||
{
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
options: OP,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -1107,7 +1167,7 @@ pub trait LeptosRoutes {
|
||||
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
options: OP,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
@@ -1121,16 +1181,20 @@ pub trait LeptosRoutes {
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
H: axum::handler::Handler<T, (), axum::body::Body>,
|
||||
H: axum::handler::Handler<T, OP, axum::body::Body>,
|
||||
T: 'static;
|
||||
}
|
||||
|
||||
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
|
||||
/// to those paths to Leptos's renderer.
|
||||
impl LeptosRoutes for axum::Router {
|
||||
impl<OP> LeptosRoutes<OP> for axum::Router<OP>
|
||||
where
|
||||
OP: LeptosOptionProvider + Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
options: OP,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -1143,7 +1207,7 @@ impl LeptosRoutes for axum::Router {
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
options: OP,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
@@ -1161,7 +1225,7 @@ impl LeptosRoutes for axum::Router {
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
options.options(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1173,9 +1237,24 @@ impl LeptosRoutes for axum::Router {
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
options.options(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
options.options(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1189,7 +1268,7 @@ impl LeptosRoutes for axum::Router {
|
||||
}
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
options.clone(),
|
||||
options.options(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
@@ -1215,7 +1294,7 @@ impl LeptosRoutes for axum::Router {
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
H: axum::handler::Handler<T, (), axum::body::Body>,
|
||||
H: axum::handler::Handler<T, OP, axum::body::Body>,
|
||||
T: 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
@@ -1238,6 +1317,7 @@ impl LeptosRoutes for axum::Router {
|
||||
router
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn get_leptos_pool() -> LocalPoolHandle {
|
||||
static LOCAL_POOL: OnceCell<LocalPoolHandle> = OnceCell::new();
|
||||
|
||||
@@ -506,6 +506,48 @@ pub fn render_app_to_stream_with_context<IV>(
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options,
|
||||
additional_context,
|
||||
app_fn,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This version allows us to pass Viz State/Extractor or other infro from Viz or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure.
|
||||
///
|
||||
/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
|
||||
/// from blocking resources should be retrojected into the HTML that's initially served, rather
|
||||
/// than dynamically inserting them with JavaScript on the client. This means you will have
|
||||
/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
|
||||
///
|
||||
/// Otherwise, this function is identical to [render_app_to_stream_with_context].
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + Clone + Send + 'static,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> impl Fn(
|
||||
Request,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
move |req: Request| {
|
||||
Box::pin({
|
||||
@@ -548,10 +590,11 @@ where
|
||||
};
|
||||
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
replace_blocks
|
||||
);
|
||||
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
@@ -1091,6 +1134,22 @@ impl LeptosRoutes for Router {
|
||||
leptos_router::Method::Patch => router.patch(path, s),
|
||||
}
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s =
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
true,
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => router.get(path, s),
|
||||
leptos_router::Method::Post => router.post(path, s),
|
||||
leptos_router::Method::Put => router.put(path, s),
|
||||
leptos_router::Method::Delete => router.delete(path, s),
|
||||
leptos_router::Method::Patch => router.patch(path, s),
|
||||
}
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
//!
|
||||
//! And you can do all three of these **using the same Leptos code.**
|
||||
//!
|
||||
//! Take a look at the [Leptos Book](https://leptos-rs.github.io/leptos/) for a walkthrough of the framework.
|
||||
//! Join us on our [Discord Channel](https://discord.gg/v38Eef6sWG) to see what the community is building.
|
||||
//! Explore our [Examples](https://github.com/leptos-rs/leptos/tree/main/examples) to see Leptos in action.
|
||||
//!
|
||||
//! # `nightly` Note
|
||||
//! Most of the examples assume you’re using `nightly` Rust. If you’re on stable, note the following:
|
||||
//! 1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`
|
||||
@@ -152,7 +156,6 @@ pub use leptos_config::{self, get_configuration, LeptosOptions};
|
||||
pub mod ssr {
|
||||
pub use leptos_dom::{ssr::*, ssr_in_order::*};
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
pub use leptos_dom::{
|
||||
self, create_node_ref, debug_warn, document, error, ev,
|
||||
helpers::{
|
||||
@@ -161,7 +164,6 @@ pub use leptos_dom::{
|
||||
request_idle_callback, request_idle_callback_with_handle, set_interval,
|
||||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener, window_event_listener_untyped,
|
||||
window_event_listener_with_precast,
|
||||
},
|
||||
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
|
||||
@@ -56,16 +56,17 @@ use std::rc::Rc;
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn Suspense<F, E>(
|
||||
pub fn Suspense<F, E, V>(
|
||||
cx: Scope,
|
||||
/// Returns a fallback UI that will be shown while `async` [Resources](leptos_reactive::Resource) are still loading.
|
||||
fallback: F,
|
||||
/// Children will be displayed once all `async` [Resources](leptos_reactive::Resource) have resolved.
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
children: Box<dyn Fn(Scope) -> V>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> E + 'static,
|
||||
E: IntoView,
|
||||
V: IntoView + 'static,
|
||||
{
|
||||
let context = SuspenseContext::new(cx);
|
||||
|
||||
@@ -74,7 +75,6 @@ where
|
||||
|
||||
let orig_child = Rc::new(children);
|
||||
|
||||
let before_me = HydrationCtx::peek();
|
||||
let current_id = HydrationCtx::next_component();
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
let prev_disposer = Rc::new(RefCell::new(None::<ScopeDisposer>));
|
||||
@@ -101,13 +101,17 @@ where
|
||||
use leptos_reactive::signal_prelude::*;
|
||||
|
||||
// run the child; we'll probably throw this away, but it will register resource reads
|
||||
let child = orig_child(cx).into_view(cx);
|
||||
let _child = orig_child(cx).into_view(cx);
|
||||
let after_original_child = HydrationCtx::id();
|
||||
|
||||
let initial = {
|
||||
// no resources were read under this, so just return the child
|
||||
if context.pending_resources.get() == 0 {
|
||||
child
|
||||
let orig_child = Rc::clone(&orig_child);
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
Fragment::lazy(Box::new(move || {
|
||||
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
|
||||
})).into_view(cx)
|
||||
}
|
||||
// show the fallback, but also prepare to stream HTML
|
||||
else {
|
||||
@@ -161,7 +165,7 @@ where
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
HydrationCtx::continue_from(before_me);
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
|
||||
leptos_dom::View::Suspense(current_id, core_component)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ where
|
||||
F: Fn() -> E + 'static,
|
||||
E: IntoView,
|
||||
{
|
||||
let prev_children = Rc::new(RefCell::new(None::<Vec<View>>));
|
||||
let prev_children = Rc::new(RefCell::new(None::<View>));
|
||||
|
||||
let first_run = Rc::new(std::cell::Cell::new(true));
|
||||
let child_runs = Cell::new(0);
|
||||
@@ -112,13 +112,13 @@ where
|
||||
}
|
||||
})
|
||||
.children(Box::new(move |cx| {
|
||||
let frag = children(cx);
|
||||
let frag = children(cx).into_view(cx);
|
||||
|
||||
let suspense_context = use_context::<SuspenseContext>(cx)
|
||||
.expect("there to be a SuspenseContext");
|
||||
|
||||
if cfg!(feature = "hydrate") || !first_run.get() {
|
||||
*prev_children.borrow_mut() = Some(frag.nodes.clone());
|
||||
*prev_children.borrow_mut() = Some(frag.clone());
|
||||
}
|
||||
if is_first_run(&first_run, &suspense_context) {
|
||||
let has_local_only = suspense_context.has_local_only()
|
||||
|
||||
@@ -14,16 +14,13 @@ fn simple_ssr_test() {
|
||||
</div>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-8|open--><div \
|
||||
id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
|
||||
assert!(rendered.into_view(cx).render_to_string(cx).contains(
|
||||
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
|
||||
id=\"_0-3\">Value: \
|
||||
<!--hk=_0-4o|leptos-dyn-child-start-->0<!\
|
||||
--hk=_0-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-8|close-->"
|
||||
);
|
||||
id=\"_0-5\">+1</button></div>"
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,28 +51,13 @@ fn ssr_test_with_components() {
|
||||
</div>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-49|open--><div id=\"_0-1\" \
|
||||
class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-38|open--><div \
|
||||
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
assert!(rendered.into_view(cx).render_to_string(cx).contains(
|
||||
"<div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
id=\"_0-1-3\">Value: \
|
||||
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-38|close--><!--hk=_0-1-0c|leptos-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-38|open--><div \
|
||||
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
|
||||
id=\"_0-1-5-3\">Value: \
|
||||
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
|
||||
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5-5\">+1</button></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-38|close--><!\
|
||||
--hk=_0-1-5-0c|leptos-counter-end--></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-49|close-->"
|
||||
);
|
||||
id=\"_0-1-5\">+1</button></div>"
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,29 +88,13 @@ fn ssr_test_with_snake_case_components() {
|
||||
</div>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-101|open--><div id=\"_0-1\" \
|
||||
class=\"counters\"><!\
|
||||
--hk=_0-1-0o|leptos-snake-case-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-90|open--><div \
|
||||
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
assert!(rendered.into_view(cx).render_to_string(cx).contains(
|
||||
"<div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
id=\"_0-1-3\">Value: \
|
||||
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-90|close--><!--hk=_0-1-0c|leptos-snake-case-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-snake-case-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-90|open--><div \
|
||||
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
|
||||
id=\"_0-1-5-3\">Value: \
|
||||
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
|
||||
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5-5\">+1</button></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-90|close--><!\
|
||||
--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-101|close-->"
|
||||
);
|
||||
id=\"_0-1-5\">+1</button></div>"
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,12 +110,10 @@ fn test_classes() {
|
||||
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-142|open--><div id=\"_0-1\" \
|
||||
class=\"my big red \
|
||||
car\"></div><!--leptos-view|leptos-tests-ssr.rs-142|close-->"
|
||||
);
|
||||
assert!(rendered
|
||||
.into_view(cx)
|
||||
.render_to_string(cx)
|
||||
.contains("<div id=\"_0-1\" class=\"my big red car\"></div>"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,13 +132,10 @@ fn ssr_with_styles() {
|
||||
</div>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-164|open--><div id=\"_0-1\" \
|
||||
class=\" myclass\"><button id=\"_0-2\" class=\"btn \
|
||||
myclass\">-1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-164|close-->"
|
||||
);
|
||||
assert!(rendered.into_view(cx).render_to_string(cx).contains(
|
||||
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" \
|
||||
class=\"btn myclass\">-1</button></div>"
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,11 +151,9 @@ fn ssr_option() {
|
||||
<option/>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-188|open--><option \
|
||||
id=\"_0-1\"></option><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-188|close-->"
|
||||
);
|
||||
assert!(rendered
|
||||
.into_view(cx)
|
||||
.render_to_string(cx)
|
||||
.contains("<option id=\"_0-1\"></option>"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ leptos_actix = { path = "../../../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen = "0.2.85"
|
||||
serde = "1.0.159"
|
||||
tokio = { version = "1.27.0", features = ["time"], optional = true }
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
<Route path="none" view=|cx| view! { cx, <None/> }/>
|
||||
</Route>
|
||||
// in-order
|
||||
<Route
|
||||
@@ -71,6 +72,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
<Route path="none" view=|cx| view! { cx, <None/> }/>
|
||||
</Route>
|
||||
// async
|
||||
<Route
|
||||
@@ -86,6 +88,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
<Route path="none" view=|cx| view! { cx, <None/> }/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
@@ -101,6 +104,7 @@ fn SecondaryNav(cx: Scope) -> impl IntoView {
|
||||
<A href="single">"Single"</A>
|
||||
<A href="parallel">"Parallel"</A>
|
||||
<A href="inside-component">"Inside Component"</A>
|
||||
<A href="none">"No Resources"</A>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
@@ -217,3 +221,25 @@ fn InsideComponentChild(cx: Scope) -> impl IntoView {
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn None(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<Suspense fallback=|| "Loading 1...">
|
||||
<div>"Children inside Suspense should hydrate properly."</div>
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
</Suspense>
|
||||
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
|
||||
<div>
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -278,7 +278,33 @@ where
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
|
||||
unmount_child(&start, end);
|
||||
match child {
|
||||
View::CoreComponent(
|
||||
crate::CoreComponent::DynChild(
|
||||
child,
|
||||
),
|
||||
) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
View::Component(child) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
_ => unmount_child(&start, end),
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the new child
|
||||
|
||||
@@ -168,7 +168,7 @@ pub(crate) struct EachItem {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment: Option<web_sys::DocumentFragment>,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: Comment,
|
||||
opening: Option<Comment>,
|
||||
pub(crate) child: View,
|
||||
closing: Option<Comment>,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
@@ -201,7 +201,11 @@ impl EachItem {
|
||||
None
|
||||
},
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Borrowed("<EachItem>"), &id, false),
|
||||
if needs_closing {
|
||||
Some(Comment::new(Cow::Borrowed("<EachItem>"), &id, false))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -215,7 +219,10 @@ impl EachItem {
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &closing.node)
|
||||
.append_with_node_2(
|
||||
&markers.1.as_ref().unwrap().node,
|
||||
&closing.node,
|
||||
)
|
||||
.unwrap();
|
||||
fragment.append_with_node_1(&closing.node).unwrap();
|
||||
}
|
||||
@@ -260,10 +267,6 @@ impl Mountable for EachItem {
|
||||
|
||||
#[inline(always)]
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self.opening.node.clone();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return self.child.get_opening_node();
|
||||
}
|
||||
|
||||
@@ -673,10 +676,20 @@ fn apply_cmds<T, EF, N>(
|
||||
// 4. Add
|
||||
if cmds.clear {
|
||||
cmds.removed.clear();
|
||||
crate::log!("clearing list");
|
||||
web_sys::console::log_2(
|
||||
&wasm_bindgen::JsValue::from_str("open"),
|
||||
opening,
|
||||
);
|
||||
web_sys::console::log_2(
|
||||
&wasm_bindgen::JsValue::from_str("closing"),
|
||||
closing,
|
||||
);
|
||||
|
||||
if opening.previous_sibling().is_none()
|
||||
&& closing.next_sibling().is_none()
|
||||
{
|
||||
crate::log!("no siblings");
|
||||
let parent = closing
|
||||
.parent_node()
|
||||
.expect("could not get closing node")
|
||||
@@ -689,6 +702,7 @@ fn apply_cmds<T, EF, N>(
|
||||
#[cfg(not(debug_assertions))]
|
||||
parent.append_with_node_1(closing).unwrap();
|
||||
} else {
|
||||
crate::log!("yes siblings");
|
||||
range.set_start_before(opening).unwrap();
|
||||
range.set_end_before(closing).unwrap();
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ pub trait EventDescriptor: Clone {
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides the [`EventDescriptor::bubbles`] method to always return
|
||||
/// Overrides the [`EventDescriptor::BUBBLES`] value to always return
|
||||
/// `false`, which forces the event to not be globally delegated.
|
||||
#[derive(Clone)]
|
||||
#[allow(non_camel_case_types)]
|
||||
|
||||
@@ -332,32 +332,8 @@ impl IntervalHandle {
|
||||
any(debug_assertions, features = "ssr"),
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
#[deprecated = "use set_interval_with_handle() instead. In the future, \
|
||||
set_interval() will no longer return a handle, for consistency \
|
||||
with other timer helper functions."]
|
||||
pub fn set_interval(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<IntervalHandle, JsValue> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
let handle = window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)?;
|
||||
Ok(IntervalHandle(handle))
|
||||
pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
|
||||
_ = set_interval_with_handle(cb, duration);
|
||||
}
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls,
|
||||
@@ -402,24 +378,6 @@ pub fn set_interval_with_handle(
|
||||
si(Box::new(cb), duration)
|
||||
}
|
||||
|
||||
/// Adds an event listener to the `Window`.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
instrument(level = "trace", skip_all, fields(event_name = %event_name))
|
||||
)]
|
||||
#[inline(always)]
|
||||
#[deprecated = "In the next release, `window_event_listener` will become \
|
||||
typed. You can switch now to `window_event_listener_untyped` \
|
||||
for the current behavior or use \
|
||||
`window_event_listener_with_precast`, which will become the \
|
||||
new`window_event_listener`."]
|
||||
pub fn window_event_listener(
|
||||
event_name: &str,
|
||||
cb: impl Fn(web_sys::Event) + 'static,
|
||||
) {
|
||||
window_event_listener_untyped(event_name, cb)
|
||||
}
|
||||
|
||||
/// Adds an event listener to the `Window`, typed as a generic `Event`.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
@@ -456,8 +414,21 @@ pub fn window_event_listener_untyped(
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a window event listener where the event in the callback is already appropriately cast.
|
||||
pub fn window_event_listener_with_precast<E: ev::EventDescriptor + 'static>(
|
||||
/// Creates a window event listener from a typed event.
|
||||
/// ```
|
||||
/// use leptos::{leptos_dom::helpers::window_event_listener, *};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn App(cx: Scope) -> impl IntoView {
|
||||
/// window_event_listener(ev::keypress, |ev| {
|
||||
/// // ev is typed as KeyboardEvent automatically,
|
||||
/// // so .code() can be called
|
||||
/// let code = ev.code();
|
||||
/// log!("code = {code:?}");
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn window_event_listener<E: ev::EventDescriptor + 'static>(
|
||||
event: E,
|
||||
cb: impl Fn(E::EventType) + 'static,
|
||||
) where
|
||||
|
||||
@@ -382,26 +382,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
#[deprecated = "Use HtmlElement::from_chunks() instead."]
|
||||
pub fn from_html(
|
||||
cx: Scope,
|
||||
element: El,
|
||||
html: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
attrs: smallvec![],
|
||||
children: ElementChildren::Chunks(vec![StringOrView::String(
|
||||
html.into(),
|
||||
)]),
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub fn from_chunks(
|
||||
@@ -827,7 +807,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// Sets a style on an element.
|
||||
///
|
||||
/// **Note**: In the builder syntax, this will be overwritten by the `style`
|
||||
/// attribute if you use `.attr("class", /* */)`. In the `view` macro, they
|
||||
/// attribute if you use `.attr("style", /* */)`. In the `view` macro, they
|
||||
/// are automatically re-ordered so that this over-writing does not happen.
|
||||
#[track_caller]
|
||||
pub fn style(
|
||||
|
||||
@@ -79,12 +79,6 @@ pub fn create_node_ref<T: ElementDescriptor + 'static>(
|
||||
}
|
||||
|
||||
impl<T: ElementDescriptor + 'static> NodeRef<T> {
|
||||
/// Creates an empty reference.
|
||||
#[deprecated = "Use `create_node_ref` instead of `NodeRef::new()`."]
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(create_rw_signal(cx, None))
|
||||
}
|
||||
|
||||
/// Gets the element that is currently stored in the reference.
|
||||
///
|
||||
/// This tracks reactively, so that node references can be used in effects.
|
||||
|
||||
@@ -146,6 +146,44 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
additional_context: impl FnOnce(Scope) + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
|
||||
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
view,
|
||||
prefix,
|
||||
additional_context,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
|
||||
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
|
||||
/// the same scope. This can be used to generate additional HTML that has access to the same `Scope`.
|
||||
///
|
||||
/// If `replace_blocks` is true, this will wait for any fragments with blocking resources and
|
||||
/// actually replace them in the initial HTML. This is slower to render (as it requires walking
|
||||
/// back over the HTML for string replacement) but has the advantage of never including those fallbacks
|
||||
/// in the HTML.
|
||||
///
|
||||
/// This renders:
|
||||
/// 1) the prefix
|
||||
/// 2) the application shell
|
||||
/// a) HTML for everything that is not under a `<Suspense/>`,
|
||||
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
|
||||
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
|
||||
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
|
||||
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
|
||||
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
additional_context: impl FnOnce(Scope) + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
|
||||
HydrationCtx::reset_id();
|
||||
|
||||
@@ -177,7 +215,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
});
|
||||
let cx = Scope { runtime, id: scope };
|
||||
|
||||
let blocking_fragments = FuturesUnordered::new();
|
||||
let mut blocking_fragments = FuturesUnordered::new();
|
||||
let fragments = FuturesUnordered::new();
|
||||
|
||||
for (fragment_id, data) in pending_fragments {
|
||||
@@ -198,24 +236,46 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments = fragments_to_chunks(blocking_fragments);
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
let resolvers = format!(
|
||||
"<script>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
|
||||
if replace_blocks {
|
||||
let mut blocks = Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close = format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) = shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell = format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments =
|
||||
fragments_to_chunks(blocking_fragments);
|
||||
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!("{prefix}{shell}{resolvers}{blocking}")
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
{blocking}
|
||||
"#
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
@@ -229,6 +289,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
|
||||
@@ -240,13 +240,20 @@ impl View {
|
||||
dont_escape_text: bool,
|
||||
) {
|
||||
match self {
|
||||
View::Suspense(id, _) => {
|
||||
View::Suspense(id, view) => {
|
||||
let id = id.to_string();
|
||||
if let Some(data) = cx.take_pending_fragment(&id) {
|
||||
chunks.push_back(StreamChunk::Async {
|
||||
chunks: data.in_order,
|
||||
should_block: data.should_block,
|
||||
});
|
||||
} else {
|
||||
// if not registered, means it was already resolved
|
||||
View::CoreComponent(view).into_stream_chunks_helper(
|
||||
cx,
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
View::Text(node) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "../README.md"
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
syn = { version = "1", features = [
|
||||
syn = { version = "2", features = [
|
||||
"full",
|
||||
"parsing",
|
||||
"extra-traits",
|
||||
@@ -19,7 +19,7 @@ syn = { version = "1", features = [
|
||||
"printing",
|
||||
] }
|
||||
quote = "1"
|
||||
syn-rsx = "0.9"
|
||||
rstml = "0.10.6"
|
||||
proc-macro2 = { version = "1", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12"
|
||||
walkdir = "2"
|
||||
|
||||
@@ -76,7 +76,7 @@ impl ViewMacros {
|
||||
tokens.next(); // ,
|
||||
// TODO handle class = ...
|
||||
let rsx =
|
||||
syn_rsx::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
|
||||
rstml::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
|
||||
let template = LNode::parse_view(rsx)?;
|
||||
views.push(MacroInvocation { id, template })
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::parsing::{is_component_node, value_to_string};
|
||||
use crate::parsing::is_component_node;
|
||||
use anyhow::Result;
|
||||
use quote::quote;
|
||||
use quote::ToTokens;
|
||||
use rstml::node::{Node, NodeAttribute};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use syn_rsx::Node;
|
||||
|
||||
// A lightweight virtual DOM structure we can use to hold
|
||||
// the state of a Leptos view macro template. This is because
|
||||
@@ -58,36 +58,30 @@ impl LNode {
|
||||
}
|
||||
}
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
views.push(LNode::Text(value));
|
||||
} else {
|
||||
let value = text.value.as_ref();
|
||||
let code = quote! { #value };
|
||||
let code = code.to_string();
|
||||
views.push(LNode::DynChild(code));
|
||||
}
|
||||
views.push(LNode::Text(text.value_string()));
|
||||
}
|
||||
Node::Block(block) => {
|
||||
let value = block.value.as_ref();
|
||||
let code = quote! { #value };
|
||||
let code = block.into_token_stream();
|
||||
let code = code.to_string();
|
||||
views.push(LNode::DynChild(code));
|
||||
}
|
||||
Node::Element(el) => {
|
||||
if is_component_node(&el) {
|
||||
let name = el.name().to_string();
|
||||
let mut children = Vec::new();
|
||||
for child in el.children {
|
||||
LNode::parse_node(child, &mut children)?;
|
||||
}
|
||||
views.push(LNode::Component {
|
||||
name: el.name.to_string(),
|
||||
name: name,
|
||||
props: el
|
||||
.open_tag
|
||||
.attributes
|
||||
.into_iter()
|
||||
.filter_map(|attr| match attr {
|
||||
Node::Attribute(attr) => Some((
|
||||
NodeAttribute::Attribute(attr) => Some((
|
||||
attr.key.to_string(),
|
||||
format!("{:#?}", attr.value),
|
||||
format!("{:#?}", attr.value()),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
@@ -95,15 +89,13 @@ impl LNode {
|
||||
children,
|
||||
});
|
||||
} else {
|
||||
let name = el.name.to_string();
|
||||
let name = el.name().to_string();
|
||||
let mut attrs = Vec::new();
|
||||
|
||||
for attr in el.attributes {
|
||||
if let Node::Attribute(attr) = attr {
|
||||
for attr in el.open_tag.attributes {
|
||||
if let NodeAttribute::Attribute(attr) = attr {
|
||||
let name = attr.key.to_string();
|
||||
if let Some(value) =
|
||||
attr.value.as_ref().and_then(value_to_string)
|
||||
{
|
||||
if let Some(value) = attr.value_literal_string() {
|
||||
attrs.push((
|
||||
name,
|
||||
LAttributeValue::Static(value),
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
use syn_rsx::{NodeElement, NodeValueExpr};
|
||||
use rstml::node::NodeElement;
|
||||
|
||||
pub fn value_to_string(value: &NodeValueExpr) -> Option<String> {
|
||||
match &value.as_ref() {
|
||||
///
|
||||
/// Converts `syn::Block` to simple expression
|
||||
///
|
||||
/// For example:
|
||||
/// ```no_build
|
||||
/// // "string literal" in
|
||||
/// {"string literal"}
|
||||
/// // number literal
|
||||
/// {0x12}
|
||||
/// // boolean literal
|
||||
/// {true}
|
||||
/// // variable
|
||||
/// {path::x}
|
||||
/// ```
|
||||
pub fn block_to_primitive_expression(block: &syn::Block) -> Option<&syn::Expr> {
|
||||
// its empty block, or block with multi lines
|
||||
if block.stmts.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
match &block.stmts[0] {
|
||||
syn::Stmt::Expr(e, None) => return Some(&e),
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Converts simple literals to its string representation.
|
||||
///
|
||||
/// This function doesn't convert literal wrapped inside block
|
||||
/// like: `{"string"}`.
|
||||
pub fn value_to_string(value: &syn::Expr) -> Option<String> {
|
||||
match &value {
|
||||
syn::Expr::Lit(lit) => match &lit.lit {
|
||||
syn::Lit::Str(s) => Some(s.value()),
|
||||
syn::Lit::Char(c) => Some(c.value().to_string()),
|
||||
@@ -14,7 +44,7 @@ pub fn value_to_string(value: &NodeValueExpr) -> Option<String> {
|
||||
}
|
||||
|
||||
pub fn is_component_node(node: &NodeElement) -> bool {
|
||||
node.name
|
||||
node.name()
|
||||
.to_string()
|
||||
.starts_with(|c: char| c.is_ascii_uppercase())
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ readme = "../README.md"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
attribute-derive = { version = "0.5", features = ["syn-full"] }
|
||||
attribute-derive = { version = "0.6", features = ["syn-full"] }
|
||||
cfg-if = "1"
|
||||
html-escape = "0.2"
|
||||
itertools = "0.10"
|
||||
prettyplease = "0.1"
|
||||
prettyplease = "0.2.4"
|
||||
proc-macro-error = "1"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full"] }
|
||||
syn-rsx = "0.9"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
rstml = "0.10.6"
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
|
||||
@@ -4,15 +4,15 @@ use convert_case::{
|
||||
Casing,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use leptos_hot_reload::parsing::value_to_string;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, spanned::Spanned,
|
||||
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
|
||||
ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat, PatIdent, Path,
|
||||
PathArguments, ReturnType, Stmt, Type, TypePath, Visibility,
|
||||
ItemFn, LitStr, Meta, Pat, PatIdent, Path, PathArguments, ReturnType, Stmt,
|
||||
Type, TypePath, Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
is_transparent: bool,
|
||||
docs: Docs,
|
||||
@@ -56,14 +56,17 @@ impl Parse for Model {
|
||||
|
||||
// We need to remove the `#[doc = ""]` and `#[builder(_)]`
|
||||
// attrs from the function signature
|
||||
drain_filter(&mut item.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
|
||||
drain_filter(&mut item.attrs, |attr| match &attr.meta {
|
||||
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
|
||||
Meta::List(attr) => attr.path == parse_quote!(prop),
|
||||
_ => false,
|
||||
});
|
||||
item.sig.inputs.iter_mut().for_each(|arg| {
|
||||
if let FnArg::Typed(ty) = arg {
|
||||
drain_filter(&mut ty.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc)
|
||||
|| attr.path == parse_quote!(prop)
|
||||
drain_filter(&mut ty.attrs, |attr| match &attr.meta {
|
||||
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
|
||||
Meta::List(attr) => attr.path == parse_quote!(prop),
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -400,12 +403,20 @@ impl Docs {
|
||||
|
||||
let mut attrs = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| attr.path.is_ident("doc").then(|| {
|
||||
let Ok(Meta::NameValue(MetaNameValue { lit: Lit::Str(doc), .. })) = attr.parse_meta() else {
|
||||
abort!(attr, "expected doc comment to be string literal");
|
||||
.filter_map(|attr| {
|
||||
let Meta::NameValue(attr ) = &attr.meta else {
|
||||
return None
|
||||
};
|
||||
(doc.value(), doc.span())
|
||||
}))
|
||||
if !attr.path.is_ident("doc") {
|
||||
return None
|
||||
}
|
||||
|
||||
let Some(val) = value_to_string(&attr.value) else {
|
||||
abort!(attr, "expected string literal in value of doc comment");
|
||||
};
|
||||
|
||||
Some((val, attr.path.span()))
|
||||
})
|
||||
.flat_map(map)
|
||||
.collect_vec();
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ extern crate proc_macro_error;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenTree};
|
||||
use quote::ToTokens;
|
||||
use rstml::{node::KeyedAttribute, parse};
|
||||
use server_fn_macro::{server_macro_impl, ServerContext};
|
||||
use syn::parse_macro_input;
|
||||
use syn_rsx::{parse, NodeAttribute};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Mode {
|
||||
@@ -351,16 +351,22 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
.chain(tokens)
|
||||
.collect()
|
||||
};
|
||||
|
||||
match parse(tokens.into()) {
|
||||
Ok(nodes) => render_view(
|
||||
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
|
||||
&nodes,
|
||||
Mode::default(),
|
||||
global_class.as_ref(),
|
||||
normalized_call_site(proc_macro::Span::call_site()),
|
||||
),
|
||||
Err(error) => error.to_compile_error(),
|
||||
let config = rstml::ParserConfig::default().recover_block(true);
|
||||
let parser = rstml::Parser::new(config);
|
||||
let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
|
||||
let errors = errors.into_iter().map(|e| e.emit_as_expr_tokens());
|
||||
let nodes_output = render_view(
|
||||
&cx,
|
||||
&nodes,
|
||||
Mode::default(),
|
||||
global_class.as_ref(),
|
||||
normalized_call_site(proc_macro::Span::call_site()),
|
||||
);
|
||||
quote! {
|
||||
{
|
||||
#(#errors;)*
|
||||
#nodes_output
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -672,7 +678,7 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// Annotates a struct so that it can be used with your Component as a `slot`.
|
||||
///
|
||||
/// The `#[slot]` macro allows you to annotate plain Rust struct as component slots and use them
|
||||
/// within your Leptos [component](crate::component!) properties. The struct can contain any number
|
||||
/// within your Leptos [`component`](macro@crate::component) properties. The struct can contain any number
|
||||
/// of fields. When you use the component somewhere else, the names of the slot fields are the
|
||||
/// names of the properties you use in the [view](crate::view!) macro.
|
||||
///
|
||||
@@ -874,9 +880,9 @@ pub fn params_derive(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attribute_value(attr: &NodeAttribute) -> &syn::Expr {
|
||||
match &attr.value {
|
||||
Some(value) => value.as_ref(),
|
||||
pub(crate) fn attribute_value(attr: &KeyedAttribute) -> &syn::Expr {
|
||||
match &attr.possible_value {
|
||||
Some(value) => &value.value,
|
||||
None => abort!(attr.key, "attribute should have value"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ use attribute_derive::Attribute as AttributeDerive;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, Field, ItemStruct, LitStr, Type, Visibility,
|
||||
parse::Parse, parse_quote, Field, ItemStruct, LitStr, Meta, Type,
|
||||
Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
@@ -31,13 +32,16 @@ impl Parse for Model {
|
||||
|
||||
// We need to remove the `#[doc = ""]` and `#[builder(_)]`
|
||||
// attrs from the function signature
|
||||
drain_filter(&mut item.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
|
||||
drain_filter(&mut item.attrs, |attr| match &attr.meta {
|
||||
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
|
||||
Meta::List(attr) => attr.path == parse_quote!(prop),
|
||||
_ => false,
|
||||
});
|
||||
item.fields.iter_mut().for_each(|arg| {
|
||||
drain_filter(&mut arg.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc)
|
||||
|| attr.path == parse_quote!(prop)
|
||||
drain_filter(&mut arg.attrs, |attr| match &attr.meta {
|
||||
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
|
||||
Meta::List(attr) => attr.path == parse_quote!(prop),
|
||||
_ => false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use crate::attribute_value;
|
||||
use leptos_hot_reload::parsing::is_component_node;
|
||||
use itertools::Either;
|
||||
use leptos_hot_reload::parsing::{
|
||||
block_to_primitive_expression, is_component_node, value_to_string,
|
||||
};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, quote_spanned};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use rstml::node::{
|
||||
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
|
||||
};
|
||||
use syn::spanned::Spanned;
|
||||
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeValueExpr};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) fn render_template(cx: &Ident, nodes: &[Node]) -> TokenStream {
|
||||
@@ -53,7 +58,7 @@ fn root_element_to_tokens(
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
let span = node.name.span();
|
||||
let span = node.name().span();
|
||||
|
||||
let navigations = if navigations.is_empty() {
|
||||
quote! {}
|
||||
@@ -67,7 +72,7 @@ fn root_element_to_tokens(
|
||||
quote! { #(#expressions;);* }
|
||||
};
|
||||
|
||||
let tag_name = node.name.to_string();
|
||||
let tag_name = node.name().to_string();
|
||||
|
||||
quote_spanned! {
|
||||
span => {
|
||||
@@ -104,9 +109,9 @@ enum PrevSibChange {
|
||||
Skip,
|
||||
}
|
||||
|
||||
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
|
||||
node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(attribute) = node {
|
||||
fn attributes(node: &NodeElement) -> impl Iterator<Item = &KeyedAttribute> {
|
||||
node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(attribute) = node {
|
||||
Some(attribute)
|
||||
} else {
|
||||
None
|
||||
@@ -129,11 +134,11 @@ fn element_to_tokens(
|
||||
) -> Ident {
|
||||
// create this element
|
||||
*next_el_id += 1;
|
||||
let this_el_ident = child_ident(*next_el_id, node.name.span());
|
||||
let this_el_ident = child_ident(*next_el_id, node.name().span());
|
||||
|
||||
// Open tag
|
||||
let name_str = node.name.to_string();
|
||||
let span = node.name.span();
|
||||
let name_str = node.name().to_string();
|
||||
let span = node.name().span();
|
||||
|
||||
// CSR/hydrate, push to template
|
||||
template.push('<');
|
||||
@@ -145,7 +150,7 @@ fn element_to_tokens(
|
||||
}
|
||||
|
||||
// navigation for this el
|
||||
let debug_name = node.name.to_string();
|
||||
let debug_name = node.name().to_string();
|
||||
let this_nav = if is_root_el {
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
@@ -247,14 +252,17 @@ fn next_sibling_node(
|
||||
if is_component_node(sibling) {
|
||||
next_sibling_node(children, idx + 1, next_el_id)
|
||||
} else {
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.name.span())))
|
||||
Ok(Some(child_ident(
|
||||
*next_el_id + 1,
|
||||
sibling.name().span(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
Node::Block(sibling) => {
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.value.span())))
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
|
||||
}
|
||||
Node::Text(sibling) => {
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.value.span())))
|
||||
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
|
||||
}
|
||||
_ => Err("expected either an element or a block".to_string()),
|
||||
}
|
||||
@@ -263,7 +271,7 @@ fn next_sibling_node(
|
||||
|
||||
fn attr_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeAttribute,
|
||||
node: &KeyedAttribute,
|
||||
el_id: &Ident,
|
||||
template: &mut String,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
@@ -272,8 +280,8 @@ fn attr_to_tokens(
|
||||
let name = name.strip_prefix('_').unwrap_or(&name);
|
||||
let name = name.strip_prefix("attr:").unwrap_or(name);
|
||||
|
||||
let value = match &node.value {
|
||||
Some(expr) => match expr.as_ref() {
|
||||
let value = match &node.value() {
|
||||
Some(expr) => match expr {
|
||||
syn::Expr::Lit(expr_lit) => {
|
||||
if let syn::Lit::Str(s) = &expr_lit.lit {
|
||||
AttributeValue::Static(s.value())
|
||||
@@ -367,7 +375,7 @@ fn child_to_tokens(
|
||||
Node::Element(node) => {
|
||||
if is_component_node(node) {
|
||||
proc_macro_error::emit_error!(
|
||||
node.name.span(),
|
||||
node.name().span(),
|
||||
"component children not allowed in template!, use view! \
|
||||
instead"
|
||||
);
|
||||
@@ -389,7 +397,7 @@ fn child_to_tokens(
|
||||
}
|
||||
Node::Text(node) => block_to_tokens(
|
||||
cx,
|
||||
&node.value,
|
||||
Either::Left(node.value_string()),
|
||||
node.value.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
@@ -399,10 +407,42 @@ fn child_to_tokens(
|
||||
expressions,
|
||||
navigations,
|
||||
),
|
||||
Node::Block(node) => block_to_tokens(
|
||||
Node::RawText(node) => block_to_tokens(
|
||||
cx,
|
||||
&node.value,
|
||||
node.value.span(),
|
||||
Either::Left(node.to_string_best()),
|
||||
node.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
next_el_id,
|
||||
template,
|
||||
expressions,
|
||||
navigations,
|
||||
),
|
||||
Node::Block(NodeBlock::ValidBlock(b)) => {
|
||||
let value = match block_to_primitive_expression(b)
|
||||
.and_then(value_to_string)
|
||||
{
|
||||
Some(v) => Either::Left(v),
|
||||
None => Either::Right(b.into_token_stream()),
|
||||
};
|
||||
block_to_tokens(
|
||||
cx,
|
||||
value,
|
||||
b.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
next_el_id,
|
||||
template,
|
||||
expressions,
|
||||
navigations,
|
||||
)
|
||||
}
|
||||
Node::Block(b @ NodeBlock::Invalid { .. }) => block_to_tokens(
|
||||
cx,
|
||||
Either::Right(b.into_token_stream()),
|
||||
b.span(),
|
||||
parent,
|
||||
prev_sib,
|
||||
next_sib,
|
||||
@@ -418,7 +458,7 @@ fn child_to_tokens(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn block_to_tokens(
|
||||
_cx: &Ident,
|
||||
value: &NodeValueExpr,
|
||||
value: Either<String, TokenStream>,
|
||||
span: Span,
|
||||
parent: &Ident,
|
||||
prev_sib: Option<Ident>,
|
||||
@@ -428,18 +468,6 @@ fn block_to_tokens(
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
navigations: &mut Vec<TokenStream>,
|
||||
) -> PrevSibChange {
|
||||
let value = value.as_ref();
|
||||
let str_value = match value {
|
||||
syn::Expr::Lit(lit) => match &lit.lit {
|
||||
syn::Lit::Str(s) => Some(s.value()),
|
||||
syn::Lit::Char(c) => Some(c.value().to_string()),
|
||||
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
|
||||
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// code to navigate to this text node
|
||||
|
||||
let (name, location) = /* if is_first_child && mode == Mode::Client {
|
||||
@@ -473,27 +501,30 @@ fn block_to_tokens(
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(v) = str_value {
|
||||
navigations.push(location);
|
||||
template.push_str(&v);
|
||||
match value {
|
||||
Either::Left(v) => {
|
||||
navigations.push(location);
|
||||
template.push_str(&v);
|
||||
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
}
|
||||
}
|
||||
} else {
|
||||
template.push_str("<!>");
|
||||
navigations.push(location);
|
||||
Either::Right(value) => {
|
||||
template.push_str("<!>");
|
||||
navigations.push(location);
|
||||
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
|
||||
});
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
|
||||
});
|
||||
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
if let Some(name) = name {
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
PrevSibChange::Parent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use crate::{attribute_value, Mode};
|
||||
use convert_case::{Case::Snake, Casing};
|
||||
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
|
||||
use leptos_hot_reload::parsing::{
|
||||
block_to_primitive_expression, is_component_node, value_to_string,
|
||||
};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use rstml::node::{
|
||||
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
|
||||
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum TagType {
|
||||
@@ -213,18 +217,22 @@ fn root_node_to_tokens_ssr(
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
|
||||
Node::Comment(_) | Node::Doctype(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
quote! {
|
||||
leptos::leptos_dom::html::text(#value)
|
||||
leptos::leptos_dom::html::text(#node)
|
||||
}
|
||||
}
|
||||
Node::RawText(r) => {
|
||||
let text = r.to_string_best();
|
||||
let text = syn::LitStr::new(&text, r.span());
|
||||
quote! {
|
||||
leptos::leptos_dom::html::text(#text)
|
||||
}
|
||||
}
|
||||
Node::Block(node) => {
|
||||
let value = node.value.as_ref();
|
||||
quote! {
|
||||
#[allow(unused_braces)]
|
||||
#value
|
||||
#node
|
||||
}
|
||||
}
|
||||
Node::Element(node) => {
|
||||
@@ -254,9 +262,9 @@ fn fragment_to_tokens_ssr(
|
||||
});
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::lazy(|| vec![
|
||||
leptos::Fragment::lazy(|| [
|
||||
#(#nodes),*
|
||||
])
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
@@ -301,10 +309,12 @@ fn root_element_to_tokens_ssr(
|
||||
let chunks = chunks.into_iter().map(|chunk| match chunk {
|
||||
SsrElementChunks::String { template, holes } => {
|
||||
if holes.is_empty() {
|
||||
let template = template.replace("\\{", "{").replace("\\}", "}");
|
||||
quote! {
|
||||
leptos::leptos_dom::html::StringOrView::String(#template.into())
|
||||
}
|
||||
} else {
|
||||
let template = template.replace("\\{", "{{").replace("\\}", "}}");
|
||||
quote! {
|
||||
leptos::leptos_dom::html::StringOrView::String(
|
||||
format!(
|
||||
@@ -327,15 +337,15 @@ fn root_element_to_tokens_ssr(
|
||||
},
|
||||
});
|
||||
|
||||
let tag_name = node.name.to_string();
|
||||
let tag_name = node.name().to_string();
|
||||
let is_custom_element = is_custom_element(&tag_name);
|
||||
let typed_element_name = if is_custom_element {
|
||||
Ident::new("Custom", node.name.span())
|
||||
Ident::new("Custom", node.name().span())
|
||||
} else {
|
||||
let camel_cased = camel_case_tag_name(
|
||||
&tag_name.replace("svg::", "").replace("math::", ""),
|
||||
);
|
||||
Ident::new(&camel_cased, node.name.span())
|
||||
Ident::new(&camel_cased, node.name().span())
|
||||
};
|
||||
let typed_element_name = if is_svg_element(&tag_name) {
|
||||
quote! { svg::#typed_element_name }
|
||||
@@ -407,7 +417,7 @@ fn element_to_tokens_ssr(
|
||||
}));
|
||||
} else {
|
||||
let tag_name = node
|
||||
.name
|
||||
.name()
|
||||
.to_string()
|
||||
.replace("svg::", "")
|
||||
.replace("math::", "");
|
||||
@@ -417,8 +427,8 @@ fn element_to_tokens_ssr(
|
||||
|
||||
let mut inner_html = None;
|
||||
|
||||
for attr in &node.attributes {
|
||||
if let Node::Attribute(attr) = attr {
|
||||
for attr in node.attributes() {
|
||||
if let NodeAttribute::Attribute(attr) = attr {
|
||||
inner_html = attribute_to_tokens_ssr(
|
||||
cx,
|
||||
attr,
|
||||
@@ -437,9 +447,9 @@ fn element_to_tokens_ssr(
|
||||
quote! { leptos::leptos_dom::HydrationCtx::id() }
|
||||
};
|
||||
match node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.find(|node| matches!(node, Node::Attribute(attr) if attr.key.to_string() == "id"))
|
||||
.find(|node| matches!(node, NodeAttribute::Attribute(attr) if attr.key.to_string() == "id"))
|
||||
{
|
||||
Some(_) => {
|
||||
template.push_str(" leptos-hk=\"_{}\"");
|
||||
@@ -460,7 +470,7 @@ fn element_to_tokens_ssr(
|
||||
|
||||
if let Some(inner_html) = inner_html {
|
||||
template.push_str("{}");
|
||||
let value = inner_html.as_ref();
|
||||
let value = inner_html;
|
||||
|
||||
holes.push(quote! {
|
||||
(#value).into_attribute(#cx).as_nameless_value_string().unwrap_or_default()
|
||||
@@ -482,32 +492,23 @@ fn element_to_tokens_ssr(
|
||||
);
|
||||
}
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
let value = if is_script_or_style {
|
||||
value.into()
|
||||
} else {
|
||||
html_escape::encode_safe(&value)
|
||||
};
|
||||
template.push_str(
|
||||
&value
|
||||
.replace('{', "{{")
|
||||
.replace('}', "}}"),
|
||||
);
|
||||
let value = text.value_string();
|
||||
let value = if is_script_or_style {
|
||||
value.into()
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
let value = text.value.as_ref();
|
||||
|
||||
holes.push(quote! {
|
||||
#value.into_view(#cx).render_to_string(#cx)
|
||||
})
|
||||
}
|
||||
html_escape::encode_safe(&value)
|
||||
};
|
||||
template.push_str(
|
||||
&value.replace('{', "\\{").replace('}', "\\}"),
|
||||
);
|
||||
}
|
||||
Node::Block(block) => {
|
||||
if let Some(value) = value_to_string(&block.value) {
|
||||
Node::Block(NodeBlock::ValidBlock(block)) => {
|
||||
if let Some(value) =
|
||||
block_to_primitive_expression(block)
|
||||
.and_then(value_to_string)
|
||||
{
|
||||
template.push_str(&value);
|
||||
} else {
|
||||
let value = block.value.as_ref();
|
||||
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String {
|
||||
template: std::mem::take(template),
|
||||
@@ -515,10 +516,16 @@ fn element_to_tokens_ssr(
|
||||
})
|
||||
}
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#value}.into_view(#cx)
|
||||
{#block}.into_view(#cx)
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Keep invalid blocks for faster IDE diff (on user type)
|
||||
Node::Block(block @ NodeBlock::Invalid { .. }) => {
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#block}.into_view(#cx)
|
||||
}));
|
||||
}
|
||||
Node::Fragment(_) => abort!(
|
||||
Span::call_site(),
|
||||
"You can't nest a fragment inside an element."
|
||||
@@ -529,7 +536,7 @@ fn element_to_tokens_ssr(
|
||||
}
|
||||
|
||||
template.push_str("</");
|
||||
template.push_str(&node.name.to_string());
|
||||
template.push_str(&node.name().to_string());
|
||||
template.push('>');
|
||||
}
|
||||
}
|
||||
@@ -538,17 +545,17 @@ fn element_to_tokens_ssr(
|
||||
// returns `inner_html`
|
||||
fn attribute_to_tokens_ssr<'a>(
|
||||
cx: &Ident,
|
||||
node: &'a NodeAttribute,
|
||||
attr: &'a KeyedAttribute,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> Option<&'a NodeValueExpr> {
|
||||
let name = node.key.to_string();
|
||||
) -> Option<&'a syn::Expr> {
|
||||
let name = attr.key.to_string();
|
||||
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
|
||||
// ignore refs on SSR
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = attribute_value(node);
|
||||
let handler = attribute_value(attr);
|
||||
let (event_type, _, _) = parse_event_name(name);
|
||||
|
||||
exprs_for_compiler.push(quote! {
|
||||
@@ -561,16 +568,16 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
// ignore props for SSR
|
||||
// ignore classes and sdtyles: we'll handle these separately
|
||||
} else if name == "inner_html" {
|
||||
return node.value.as_ref();
|
||||
return attr.value();
|
||||
} else {
|
||||
let name = name.replacen("attr:", "", 1);
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& node.value.as_ref().and_then(value_to_string).is_none()
|
||||
&& attr.value().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = node.key.span();
|
||||
let span = attr.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
|
||||
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
|
||||
@@ -580,7 +587,7 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
if name != "class" && name != "style" {
|
||||
template.push(' ');
|
||||
|
||||
if let Some(value) = node.value.as_ref() {
|
||||
if let Some(value) = attr.value() {
|
||||
if let Some(value) = value_to_string(value) {
|
||||
template.push_str(&name);
|
||||
template.push_str("=\"");
|
||||
@@ -588,7 +595,6 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
template.push('"');
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
&{#value}.into_attribute(#cx)
|
||||
.as_nameless_value_string()
|
||||
@@ -628,11 +634,13 @@ fn set_class_attribute_ssr(
|
||||
Some(val) => (String::new(), Some(val)),
|
||||
};
|
||||
let static_class_attr = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
Node::Attribute(attr) if attr.key.to_string() == "class" => {
|
||||
attr.value.as_ref().and_then(value_to_string)
|
||||
NodeAttribute::Attribute(attr)
|
||||
if attr.key.to_string() == "class" =>
|
||||
{
|
||||
attr.value().and_then(value_to_string)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
@@ -642,17 +650,17 @@ fn set_class_attribute_ssr(
|
||||
.join(" ");
|
||||
|
||||
let dyn_class_attr = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
if let Node::Attribute(a) = a {
|
||||
if let NodeAttribute::Attribute(a) = a {
|
||||
if a.key.to_string() == "class" {
|
||||
if a.value.as_ref().and_then(value_to_string).is_some()
|
||||
if a.value().and_then(value_to_string).is_some()
|
||||
|| fancy_class_name(&a.key.to_string(), cx, a).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), &a.value))
|
||||
Some((a.key.span(), a.value()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -664,10 +672,10 @@ fn set_class_attribute_ssr(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let class_attrs = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "class" {
|
||||
return if let Some((_, name, value)) =
|
||||
@@ -711,7 +719,6 @@ fn set_class_attribute_ssr(
|
||||
for (_span, value) in dyn_class_attr {
|
||||
if let Some(value) = value {
|
||||
template.push_str(" {}");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
@@ -743,11 +750,13 @@ fn set_style_attribute_ssr(
|
||||
holes: &mut Vec<TokenStream>,
|
||||
) {
|
||||
let static_style_attr = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
Node::Attribute(attr) if attr.key.to_string() == "style" => {
|
||||
attr.value.as_ref().and_then(value_to_string)
|
||||
NodeAttribute::Attribute(attr)
|
||||
if attr.key.to_string() == "style" =>
|
||||
{
|
||||
attr.value().and_then(value_to_string)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
@@ -755,17 +764,17 @@ fn set_style_attribute_ssr(
|
||||
.map(|style| format!("{style};"));
|
||||
|
||||
let dyn_style_attr = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
if let Node::Attribute(a) = a {
|
||||
if let NodeAttribute::Attribute(a) = a {
|
||||
if a.key.to_string() == "style" {
|
||||
if a.value.as_ref().and_then(value_to_string).is_some()
|
||||
if a.value().and_then(value_to_string).is_some()
|
||||
|| fancy_style_name(&a.key.to_string(), cx, a).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), &a.value))
|
||||
Some((a.key.span(), a.value()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -777,10 +786,10 @@ fn set_style_attribute_ssr(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let style_attrs = node
|
||||
.attributes
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "style" {
|
||||
return if let Some((_, name, value)) =
|
||||
@@ -823,7 +832,6 @@ fn set_style_attribute_ssr(
|
||||
for (_span, value) in dyn_style_attr {
|
||||
if let Some(value) = value {
|
||||
template.push_str(" {};");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
@@ -897,18 +905,18 @@ fn fragment_to_tokens(
|
||||
let tokens = if lazy {
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::lazy(|| vec![
|
||||
leptos::Fragment::lazy(|| [
|
||||
#(#nodes),*
|
||||
])
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::new(vec![
|
||||
leptos::Fragment::new([
|
||||
#(#nodes),*
|
||||
])
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
@@ -946,18 +954,14 @@ fn node_to_tokens(
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => Some(quote! {}),
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
Some(quote! {
|
||||
leptos::leptos_dom::html::text(#value)
|
||||
})
|
||||
}
|
||||
Node::Block(node) => {
|
||||
let value = node.value.as_ref();
|
||||
Some(quote! { #value })
|
||||
}
|
||||
Node::Attribute(node) => {
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
Node::Text(node) => Some(quote! {
|
||||
leptos::leptos_dom::html::text(#node)
|
||||
}),
|
||||
Node::Block(node) => Some(quote! { #node }),
|
||||
Node::RawText(r) => {
|
||||
let text = r.to_string_best();
|
||||
let text = syn::LitStr::new(&text, r.span());
|
||||
Some(quote! { #text })
|
||||
}
|
||||
Node::Element(node) => element_to_tokens(
|
||||
cx,
|
||||
@@ -978,6 +982,7 @@ fn element_to_tokens(
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
let name = node.name();
|
||||
if is_component_node(node) {
|
||||
if let Some(slot) = get_slot(node) {
|
||||
slot_to_tokens(cx, node, slot, parent_slots, global_class);
|
||||
@@ -986,20 +991,17 @@ fn element_to_tokens(
|
||||
Some(component_to_tokens(cx, node, global_class))
|
||||
}
|
||||
} else {
|
||||
let tag = node.name.to_string();
|
||||
let tag = name.to_string();
|
||||
let name = if is_custom_element(&tag) {
|
||||
let name = node.name.to_string();
|
||||
let name = node.name().to_string();
|
||||
quote! { leptos::leptos_dom::html::custom(#cx, leptos::leptos_dom::html::Custom::new(#name)) }
|
||||
} else if is_svg_element(&tag) {
|
||||
let name = &node.name;
|
||||
parent_type = TagType::Svg;
|
||||
quote! { leptos::leptos_dom::svg::#name(#cx) }
|
||||
} else if is_math_ml_element(&tag) {
|
||||
let name = &node.name;
|
||||
parent_type = TagType::Math;
|
||||
quote! { leptos::leptos_dom::math::#name(#cx) }
|
||||
} else if is_ambiguous_element(&tag) {
|
||||
let name = &node.name;
|
||||
match parent_type {
|
||||
TagType::Unknown => {
|
||||
// We decided this warning was too aggressive, but I'll leave it here in case we want it later
|
||||
@@ -1018,12 +1020,11 @@ fn element_to_tokens(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let name = &node.name;
|
||||
parent_type = TagType::Html;
|
||||
quote! { leptos::leptos_dom::html::#name(#cx) }
|
||||
};
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
let name = name.trim();
|
||||
if name.starts_with("class:")
|
||||
@@ -1039,8 +1040,8 @@ fn element_to_tokens(
|
||||
None
|
||||
}
|
||||
});
|
||||
let class_attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let class_attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
|
||||
Some(fancy)
|
||||
@@ -1053,8 +1054,8 @@ fn element_to_tokens(
|
||||
None
|
||||
}
|
||||
});
|
||||
let style_attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let style_attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if let Some((fancy, _, _)) = fancy_style_name(&name, cx, node) {
|
||||
Some(fancy)
|
||||
@@ -1099,32 +1100,18 @@ fn element_to_tokens(
|
||||
}),
|
||||
false,
|
||||
),
|
||||
Node::Text(node) => {
|
||||
if let Some(primitive) = value_to_string(&node.value) {
|
||||
(quote! { #primitive }, true)
|
||||
} else {
|
||||
let value = node.value.as_ref();
|
||||
(
|
||||
quote! {
|
||||
#[allow(unused_braces)] #value
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
Node::Block(node) => {
|
||||
if let Some(primitive) = value_to_string(&node.value) {
|
||||
(quote! { #primitive }, true)
|
||||
} else {
|
||||
let value = node.value.as_ref();
|
||||
(
|
||||
quote! {
|
||||
#[allow(unused_braces)] #value
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
Node::Text(node) => (quote! { #node }, true),
|
||||
Node::RawText(node) => {
|
||||
let text = node.to_string_best();
|
||||
let text = syn::LitStr::new(&text, node.span());
|
||||
(quote! { #text }, true)
|
||||
}
|
||||
Node::Block(node) => (
|
||||
quote! {
|
||||
#node
|
||||
},
|
||||
false,
|
||||
),
|
||||
Node::Element(node) => (
|
||||
element_to_tokens(
|
||||
cx,
|
||||
@@ -1137,9 +1124,7 @@ fn element_to_tokens(
|
||||
.unwrap_or_default(),
|
||||
false,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => {
|
||||
(quote! {}, false)
|
||||
}
|
||||
Node::Comment(_) | Node::Doctype(_) => (quote! {}, false),
|
||||
};
|
||||
if is_static {
|
||||
quote! {
|
||||
@@ -1170,7 +1155,7 @@ fn element_to_tokens(
|
||||
|
||||
fn attribute_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeAttribute,
|
||||
node: &KeyedAttribute,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let span = node.key.span();
|
||||
@@ -1301,7 +1286,7 @@ fn attribute_to_tokens(
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& node.value.as_ref().and_then(value_to_string).is_none()
|
||||
&& node.value().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = node.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
@@ -1311,10 +1296,8 @@ fn attribute_to_tokens(
|
||||
};
|
||||
|
||||
// all other attributes
|
||||
let value = match node.value.as_ref() {
|
||||
let value = match node.value() {
|
||||
Some(value) => {
|
||||
let value = value.as_ref();
|
||||
|
||||
quote! { #value }
|
||||
}
|
||||
None => quote_spanned! { span => "" },
|
||||
@@ -1365,7 +1348,7 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
|
||||
pub(crate) fn slot_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
slot: &NodeAttribute,
|
||||
slot: &KeyedAttribute,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
@@ -1374,19 +1357,19 @@ pub(crate) fn slot_to_tokens(
|
||||
let name = convert_to_snake_case(if name.starts_with("slot:") {
|
||||
name.replacen("slot:", "", 1)
|
||||
} else {
|
||||
node.name.to_string()
|
||||
node.name().to_string()
|
||||
});
|
||||
|
||||
let component_name = ident_from_tag_name(&node.name);
|
||||
let span = node.name.span();
|
||||
let component_name = ident_from_tag_name(node.name());
|
||||
let span = node.name().span();
|
||||
|
||||
let Some(parent_slots) = parent_slots else {
|
||||
proc_macro_error::emit_error!(span, "slots cannot be used inside HTML elements");
|
||||
return;
|
||||
};
|
||||
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
if is_slot(node) {
|
||||
None
|
||||
} else {
|
||||
@@ -1404,10 +1387,8 @@ pub(crate) fn slot_to_tokens(
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value
|
||||
.as_ref()
|
||||
.value()
|
||||
.map(|v| {
|
||||
let v = v.as_ref();
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
@@ -1472,9 +1453,9 @@ pub(crate) fn slot_to_tokens(
|
||||
let slot = Ident::new(&slot, span);
|
||||
if values.len() > 1 {
|
||||
quote! {
|
||||
.#slot(vec![
|
||||
.#slot([
|
||||
#(#values)*
|
||||
])
|
||||
].to_vec())
|
||||
}
|
||||
} else {
|
||||
let value = &values[0];
|
||||
@@ -1502,12 +1483,12 @@ pub(crate) fn component_to_tokens(
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let name = &node.name;
|
||||
let component_name = ident_from_tag_name(&node.name);
|
||||
let span = node.name.span();
|
||||
let name = node.name();
|
||||
let component_name = ident_from_tag_name(node.name());
|
||||
let span = node.name().span();
|
||||
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
@@ -1524,10 +1505,8 @@ pub(crate) fn component_to_tokens(
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value
|
||||
.as_ref()
|
||||
.value()
|
||||
.map(|v| {
|
||||
let v = v.as_ref();
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
@@ -1635,7 +1614,7 @@ pub(crate) fn component_to_tokens(
|
||||
}
|
||||
|
||||
pub(crate) fn event_from_attribute_node(
|
||||
attr: &NodeAttribute,
|
||||
attr: &KeyedAttribute,
|
||||
force_undelegated: bool,
|
||||
) -> (TokenStream, &Expr) {
|
||||
let event_name = attr
|
||||
@@ -1695,7 +1674,7 @@ fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
|
||||
fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
|
||||
match expr {
|
||||
syn::Expr::Block(block) => block.block.stmts.last().and_then(|stmt| {
|
||||
if let syn::Stmt::Expr(expr) = stmt {
|
||||
if let syn::Stmt::Expr(expr, ..) = stmt {
|
||||
expr_to_ident(expr)
|
||||
} else {
|
||||
None
|
||||
@@ -1706,15 +1685,15 @@ fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_slot(node: &NodeAttribute) -> bool {
|
||||
fn is_slot(node: &KeyedAttribute) -> bool {
|
||||
let key = node.key.to_string();
|
||||
let key = key.trim();
|
||||
key == "slot" || key.starts_with("slot:")
|
||||
}
|
||||
|
||||
fn get_slot(node: &NodeElement) -> Option<&NodeAttribute> {
|
||||
node.attributes.iter().find_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
fn get_slot(node: &NodeElement) -> Option<&KeyedAttribute> {
|
||||
node.attributes().iter().find_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
if is_slot(node) {
|
||||
Some(node)
|
||||
} else {
|
||||
@@ -1742,7 +1721,7 @@ fn is_self_closing(node: &NodeElement) -> bool {
|
||||
// self-closing tags
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
||||
matches!(
|
||||
node.name.to_string().as_str(),
|
||||
node.name().to_string().as_str(),
|
||||
"area"
|
||||
| "base"
|
||||
| "br"
|
||||
@@ -1897,13 +1876,13 @@ fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
fn fancy_class_name<'a>(
|
||||
name: &str,
|
||||
cx: &Ident,
|
||||
node: &'a NodeAttribute,
|
||||
node: &'a KeyedAttribute,
|
||||
) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex class names:
|
||||
// e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)`
|
||||
if name == "class" {
|
||||
if let Some(expr) = node.value.as_ref() {
|
||||
if let syn::Expr::Tuple(tuple) = expr.as_ref() {
|
||||
if let Some(expr) = node.value() {
|
||||
if let syn::Expr::Tuple(tuple) = expr {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let class = quote_spanned! {
|
||||
@@ -1946,12 +1925,12 @@ fn fancy_class_name<'a>(
|
||||
fn fancy_style_name<'a>(
|
||||
name: &str,
|
||||
cx: &Ident,
|
||||
node: &'a NodeAttribute,
|
||||
node: &'a KeyedAttribute,
|
||||
) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex dynamic style names:
|
||||
if name == "style" {
|
||||
if let Some(expr) = node.value.as_ref() {
|
||||
if let syn::Expr::Tuple(tuple) = expr.as_ref() {
|
||||
if let Some(expr) = node.value() {
|
||||
if let syn::Expr::Tuple(tuple) = expr {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let style = quote_spanned! {
|
||||
|
||||
@@ -44,7 +44,7 @@ error: unexpected end of input, expected assignment `=`
|
||||
47 | #[prop(default)] default: bool,
|
||||
| ^
|
||||
|
||||
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
|
||||
= help: try `#[prop(default=5 * 10)]`
|
||||
--> tests/ui/component.rs:56:22
|
||||
|
||||
@@ -44,7 +44,7 @@ error: unexpected end of input, expected assignment `=`
|
||||
45 | #[prop(default)] default: bool,
|
||||
| ^
|
||||
|
||||
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
|
||||
= help: try `#[prop(default=5 * 10)]`
|
||||
--> tests/ui/component_absolute.rs:54:22
|
||||
|
||||
@@ -110,7 +110,7 @@ pub use slice::*;
|
||||
pub use spawn::*;
|
||||
pub use spawn_microtask::*;
|
||||
pub use stored_value::*;
|
||||
pub use suspense::SuspenseContext;
|
||||
pub use suspense::{GlobalSuspenseContext, SuspenseContext};
|
||||
pub use trigger::*;
|
||||
|
||||
mod macros {
|
||||
|
||||
@@ -5,8 +5,9 @@ use crate::{
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
serialization::Serializable,
|
||||
spawn::spawn_local,
|
||||
use_context, Memo, ReadSignal, Scope, ScopeProperty, SignalGetUntracked,
|
||||
SignalSet, SignalUpdate, SignalWith, SuspenseContext, WriteSignal,
|
||||
use_context, GlobalSuspenseContext, Memo, ReadSignal, Scope, ScopeProperty,
|
||||
SignalGetUntracked, SignalSet, SignalUpdate, SignalWith, SuspenseContext,
|
||||
WriteSignal,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -820,6 +821,7 @@ where
|
||||
f: impl FnOnce(&T) -> U,
|
||||
location: &'static Location<'static>,
|
||||
) -> Option<U> {
|
||||
let global_suspense_cx = use_context::<GlobalSuspenseContext>(cx);
|
||||
let suspense_cx = use_context::<SuspenseContext>(cx);
|
||||
|
||||
let v = self
|
||||
@@ -882,6 +884,22 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(g) = &global_suspense_cx {
|
||||
if let Ok(ref mut contexts) = suspense_contexts.try_borrow_mut()
|
||||
{
|
||||
let s = g.as_inner();
|
||||
if !contexts.contains(s) {
|
||||
contexts.insert(*s);
|
||||
|
||||
if !has_value {
|
||||
s.increment(
|
||||
serializable != ResourceSerialization::Local,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
create_isomorphic_effect(cx, increment);
|
||||
@@ -1005,6 +1023,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum AnyResource {
|
||||
Unserializable(Rc<dyn UnserializableResource>),
|
||||
Serializable(Rc<dyn SerializableResource>),
|
||||
|
||||
@@ -743,7 +743,7 @@ impl Runtime {
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let resources = self.resources.borrow();
|
||||
let resources = { self.resources.borrow().clone() };
|
||||
let res = resources.get(id);
|
||||
if let Some(res) = res {
|
||||
let res_state = match res {
|
||||
@@ -796,7 +796,8 @@ impl Runtime {
|
||||
cx: Scope,
|
||||
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
let f = FuturesUnordered::new();
|
||||
for (id, resource) in self.resources.borrow().iter() {
|
||||
let resources = { self.resources.borrow().clone() };
|
||||
for (id, resource) in resources.iter() {
|
||||
if let AnyResource::Serializable(resource) = resource {
|
||||
f.push(resource.to_serialization_resolver(cx, id));
|
||||
}
|
||||
|
||||
@@ -167,18 +167,6 @@ pub trait SignalUpdate<T> {
|
||||
#[track_caller]
|
||||
fn update(&self, f: impl FnOnce(&mut T));
|
||||
|
||||
/// Applies a function to the current value to mutate it in place
|
||||
/// and notifies subscribers that the signal has changed. Returns
|
||||
/// [`Some(O)`] if the signal is still valid, [`None`] otherwise.
|
||||
///
|
||||
/// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
|
||||
/// even if the value has not actually changed.
|
||||
#[deprecated = "Please use `try_update` instead. This method will be \
|
||||
removed in a future version of this crate"]
|
||||
fn update_returning<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
|
||||
self.try_update(f)
|
||||
}
|
||||
|
||||
/// Applies a function to the current value to mutate it in place
|
||||
/// and notifies subscribers that the signal has changed. Returns
|
||||
/// [`Some(O)`] if the signal is still valid, [`None`] otherwise.
|
||||
@@ -249,19 +237,6 @@ pub trait SignalUpdateUntracked<T> {
|
||||
#[track_caller]
|
||||
fn update_untracked(&self, f: impl FnOnce(&mut T));
|
||||
|
||||
/// Runs the provided closure with a mutable reference to the current
|
||||
/// value without notifying dependents and returns
|
||||
/// the value the closure returned.
|
||||
#[deprecated = "Please use `try_update_untracked` instead. This method \
|
||||
will be removed in a future version of `leptos`"]
|
||||
#[inline(always)]
|
||||
fn update_returning_untracked<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.try_update_untracked(f)
|
||||
}
|
||||
|
||||
/// Runs the provided closure with a mutable reference to the current
|
||||
/// value without notifying dependents and returns
|
||||
/// the value the closure returned.
|
||||
@@ -930,27 +905,6 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
|
||||
self.id.update_with_no_effect(self.runtime, f);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "WriteSignal::update_returning_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update_returning_untracked<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.id.update_with_no_effect(self.runtime, f)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn try_update_untracked<O>(
|
||||
&self,
|
||||
@@ -1345,27 +1299,6 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
|
||||
self.id.update_with_no_effect(self.runtime, f);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features="ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::update_returning_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update_returning_untracked<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.id.update_with_no_effect(self.runtime, f)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
|
||||
@@ -68,16 +68,41 @@ use crate::{
|
||||
/// // setting name only causes name to log, not count
|
||||
/// set_name("Bob".into());
|
||||
/// ```
|
||||
pub fn create_slice<T, O>(
|
||||
pub fn create_slice<T, O, S>(
|
||||
cx: Scope,
|
||||
signal: RwSignal<T>,
|
||||
getter: impl Fn(&T) -> O + Clone + Copy + 'static,
|
||||
setter: impl Fn(&mut T, O) + Clone + Copy + 'static,
|
||||
) -> (Signal<O>, SignalSetter<O>)
|
||||
setter: impl Fn(&mut T, S) + Clone + Copy + 'static,
|
||||
) -> (Signal<O>, SignalSetter<S>)
|
||||
where
|
||||
O: PartialEq,
|
||||
{
|
||||
let getter = create_memo(cx, move |_| signal.with(getter));
|
||||
let setter = move |value| signal.update(|x| setter(x, value));
|
||||
(getter.into(), setter.mapped_signal_setter(cx))
|
||||
(
|
||||
create_read_slice(cx, signal, getter),
|
||||
create_write_slice(cx, signal, setter),
|
||||
)
|
||||
}
|
||||
|
||||
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
|
||||
/// read-only half of [`create_slice`].
|
||||
pub fn create_read_slice<T, O>(
|
||||
cx: Scope,
|
||||
signal: RwSignal<T>,
|
||||
getter: impl Fn(&T) -> O + Clone + Copy + 'static,
|
||||
) -> Signal<O>
|
||||
where
|
||||
O: PartialEq,
|
||||
{
|
||||
create_memo(cx, move |_| signal.with(getter)).into()
|
||||
}
|
||||
|
||||
/// Creates a setter to access one slice of a signal. This is equivalent to the
|
||||
/// write-only half of [`create_slice`].
|
||||
pub fn create_write_slice<T, O>(
|
||||
cx: Scope,
|
||||
signal: RwSignal<T>,
|
||||
setter: impl Fn(&mut T, O) + Clone + Copy + 'static,
|
||||
) -> SignalSetter<O> {
|
||||
let setter = move |value| signal.update(|x| setter(x, value));
|
||||
setter.mapped_signal_setter(cx)
|
||||
}
|
||||
|
||||
@@ -39,8 +39,7 @@ impl<T> Clone for StoredValue<T> {
|
||||
impl<T> Copy for StoredValue<T> {}
|
||||
|
||||
impl<T> StoredValue<T> {
|
||||
/// Returns a clone of the signals current value, subscribing the effect
|
||||
/// to this signal.
|
||||
/// Returns a clone of the current stored value.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
@@ -56,42 +55,8 @@ impl<T> StoredValue<T> {
|
||||
/// }
|
||||
/// let data = store_value(cx, MyCloneableData { value: "a".into() });
|
||||
///
|
||||
/// // calling .get() clones and returns the value
|
||||
/// assert_eq!(data.get().value, "a");
|
||||
/// // there's a short-hand getter form
|
||||
/// assert_eq!(data().value, "a");
|
||||
/// # });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[deprecated = "Please use `get_value` instead, as this method does not \
|
||||
track the stored value. This method will also be removed \
|
||||
in a future version of `leptos`"]
|
||||
pub fn get(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.get_value()
|
||||
}
|
||||
|
||||
/// Returns a clone of the signals current value, subscribing the effect
|
||||
/// to this signal.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
///
|
||||
/// #[derive(Clone)]
|
||||
/// pub struct MyCloneableData {
|
||||
/// pub value: String,
|
||||
/// }
|
||||
/// let data = store_value(cx, MyCloneableData { value: "a".into() });
|
||||
///
|
||||
/// // calling .get() clones and returns the value
|
||||
/// assert_eq!(data.get().value, "a");
|
||||
/// // calling .get_value() clones and returns the value
|
||||
/// assert_eq!(data.get_value().value, "a");
|
||||
/// // there's a short-hand getter form
|
||||
/// assert_eq!(data().value, "a");
|
||||
/// # });
|
||||
@@ -104,19 +69,7 @@ impl<T> StoredValue<T> {
|
||||
self.try_get_value().expect("could not get stored value")
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::get`] but will not panic by default.
|
||||
#[track_caller]
|
||||
#[deprecated = "Please use `try_get_value` instead, as this method does \
|
||||
not track the stored value. This method will also be \
|
||||
removed in a future version of `leptos`"]
|
||||
pub fn try_get(&self) -> Option<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.try_get_value()
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::get`] but will not panic by default.
|
||||
/// Same as [`StoredValue::get_value`] but will not panic by default.
|
||||
#[track_caller]
|
||||
pub fn try_get_value(&self) -> Option<T>
|
||||
where
|
||||
@@ -125,34 +78,7 @@ impl<T> StoredValue<T> {
|
||||
self.try_with_value(T::clone)
|
||||
}
|
||||
|
||||
/// Applies a function to the current stored value.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
///
|
||||
/// pub struct MyUncloneableData {
|
||||
/// pub value: String
|
||||
/// }
|
||||
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
|
||||
///
|
||||
/// // calling .with() to extract the value
|
||||
/// assert_eq!(data.with(|data| data.value.clone()), "a");
|
||||
/// });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[deprecated = "Please use `with_value` instead, as this method does not \
|
||||
track the stored value. This method will also be removed \
|
||||
in a future version of `leptos`"]
|
||||
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
self.with_value(f)
|
||||
}
|
||||
|
||||
/// Applies a function to the current stored value.
|
||||
/// Applies a function to the current stored value and returns the result.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
@@ -167,8 +93,8 @@ impl<T> StoredValue<T> {
|
||||
/// }
|
||||
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
|
||||
///
|
||||
/// // calling .with() to extract the value
|
||||
/// assert_eq!(data.with(|data| data.value.clone()), "a");
|
||||
/// // calling .with_value() to extract the value
|
||||
/// assert_eq!(data.with_value(|data| data.value.clone()), "a");
|
||||
/// # });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
@@ -178,17 +104,8 @@ impl<T> StoredValue<T> {
|
||||
self.try_with_value(f).expect("could not get stored value")
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
|
||||
/// the signal is still valid. [`None`] otherwise.
|
||||
#[deprecated = "Please use `try_with_value` instead, as this method does \
|
||||
not track the stored value. This method will also be \
|
||||
removed in a future version of `leptos`"]
|
||||
pub fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.try_with_value(f)
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
|
||||
/// the signal is still valid. [`None`] otherwise.
|
||||
/// Same as [`StoredValue::with_value`] but returns [`Some(O)]` only if
|
||||
/// the stored value has not yet been disposed. [`None`] otherwise.
|
||||
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let value = {
|
||||
@@ -214,8 +131,8 @@ impl<T> StoredValue<T> {
|
||||
/// pub value: String
|
||||
/// }
|
||||
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
|
||||
/// data.update(|data| data.value = "b".into());
|
||||
/// assert_eq!(data.with(|data| data.value.clone()), "b");
|
||||
/// data.update_value(|data| data.value = "b".into());
|
||||
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
@@ -228,54 +145,12 @@ impl<T> StoredValue<T> {
|
||||
/// }
|
||||
///
|
||||
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
|
||||
/// let updated = data.update_returning(|data| {
|
||||
/// let updated = data.try_update_value(|data| {
|
||||
/// data.value = "b".into();
|
||||
/// data.value.clone()
|
||||
/// });
|
||||
///
|
||||
/// assert_eq!(data.with(|data| data.value.clone()), "b");
|
||||
/// assert_eq!(updated, Some(String::from("b")));
|
||||
/// # });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[deprecated = "Please use `update_value` instead, as this method does not \
|
||||
track the stored value. This method will also be removed \
|
||||
in a future version of `leptos`"]
|
||||
pub fn update(&self, f: impl FnOnce(&mut T)) {
|
||||
self.update_value(f);
|
||||
}
|
||||
|
||||
/// Updates the stored value.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
///
|
||||
/// pub struct MyUncloneableData {
|
||||
/// pub value: String
|
||||
/// }
|
||||
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
|
||||
/// data.update(|data| data.value = "b".into());
|
||||
/// assert_eq!(data.with(|data| data.value.clone()), "b");
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
///
|
||||
/// pub struct MyUncloneableData {
|
||||
/// pub value: String,
|
||||
/// }
|
||||
///
|
||||
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
|
||||
/// let updated = data.update_returning(|data| {
|
||||
/// data.value = "b".into();
|
||||
/// data.value.clone()
|
||||
/// });
|
||||
///
|
||||
/// assert_eq!(data.with(|data| data.value.clone()), "b");
|
||||
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
|
||||
/// assert_eq!(updated, Some(String::from("b")));
|
||||
/// # });
|
||||
/// ```
|
||||
@@ -285,20 +160,8 @@ impl<T> StoredValue<T> {
|
||||
.expect("could not set stored value");
|
||||
}
|
||||
|
||||
/// Updates the stored value.
|
||||
#[track_caller]
|
||||
#[deprecated = "Please use `try_update_value` instead, as this method does \
|
||||
not track the stored value. This method will also be \
|
||||
removed in a future version of `leptos`"]
|
||||
pub fn update_returning<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.try_update_value(f)
|
||||
}
|
||||
|
||||
/// Same as [`Self::update`], but returns [`Some(O)`] if the
|
||||
/// signal is still valid, [`None`] otherwise.
|
||||
/// Same as [`Self::update_value`], but returns [`Some(O)`] if the
|
||||
/// stored value has not yet been disposed, [`None`] otherwise.
|
||||
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
@@ -311,29 +174,6 @@ impl<T> StoredValue<T> {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Sets the stored value.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
///
|
||||
/// pub struct MyUncloneableData {
|
||||
/// pub value: String
|
||||
/// }
|
||||
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
|
||||
/// data.set(MyUncloneableData { value: "b".into() });
|
||||
/// assert_eq!(data.with(|data| data.value.clone()), "b");
|
||||
/// });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[deprecated = "Please use `set_value` instead, as this method does not \
|
||||
track the stored value. This method will also be removed \
|
||||
in a future version of `leptos`"]
|
||||
pub fn set(&self, value: T) {
|
||||
self.set_value(value);
|
||||
}
|
||||
|
||||
/// Sets the stored value.
|
||||
///
|
||||
/// # Examples
|
||||
@@ -345,8 +185,8 @@ impl<T> StoredValue<T> {
|
||||
/// pub value: String,
|
||||
/// }
|
||||
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
|
||||
/// data.set(MyUncloneableData { value: "b".into() });
|
||||
/// assert_eq!(data.with(|data| data.value.clone()), "b");
|
||||
/// data.set_value(MyUncloneableData { value: "b".into() });
|
||||
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
|
||||
/// # });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
@@ -354,8 +194,8 @@ impl<T> StoredValue<T> {
|
||||
self.try_set_value(value);
|
||||
}
|
||||
|
||||
/// Same as [`Self::set`], but returns [`None`] if the signal is
|
||||
/// still valid, [`Some(T)`] otherwise.
|
||||
/// Same as [`Self::set_value`], but returns [`None`] if the
|
||||
/// stored value has not yet been disposed, [`Some(T)`] otherwise.
|
||||
pub fn try_set_value(&self, value: T) -> Option<T> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
@@ -403,10 +243,10 @@ impl<T> StoredValue<T> {
|
||||
/// pub value: String,
|
||||
/// }
|
||||
///
|
||||
/// // ✅ you can move the `StoredValue` and access it with .with()
|
||||
/// // ✅ you can move the `StoredValue` and access it with .with_value()
|
||||
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
|
||||
/// let callback_a = move || data.with(|data| data.value == "a");
|
||||
/// let callback_b = move || data.with(|data| data.value == "b");
|
||||
/// let callback_a = move || data.with_value(|data| data.value == "a");
|
||||
/// let callback_b = move || data.with_value(|data| data.value == "b");
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[track_caller]
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_rw_signal, create_signal, queue_microtask, store_value, ReadSignal,
|
||||
RwSignal, Scope, SignalUpdate, StoredValue, WriteSignal,
|
||||
create_isomorphic_effect, create_rw_signal, create_signal, queue_microtask,
|
||||
signal::SignalGet, store_value, ReadSignal, RwSignal, Scope, SignalSet,
|
||||
SignalUpdate, StoredValue, WriteSignal,
|
||||
};
|
||||
use futures::Future;
|
||||
use std::{borrow::Cow, collections::VecDeque, pin::Pin};
|
||||
use std::{borrow::Cow, cell::RefCell, collections::VecDeque, pin::Pin};
|
||||
|
||||
/// Tracks [`Resource`](crate::Resource)s that are read under a suspense context,
|
||||
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
|
||||
@@ -20,6 +21,24 @@ pub struct SuspenseContext {
|
||||
pub(crate) should_block: StoredValue<bool>,
|
||||
}
|
||||
|
||||
/// A single, global suspense context that will be checked when resources
|
||||
/// are read. This won’t be “blocked” by lower suspense components. This is
|
||||
/// useful for e.g., holding route transitions.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct GlobalSuspenseContext(SuspenseContext);
|
||||
|
||||
impl GlobalSuspenseContext {
|
||||
/// Creates an empty global suspense context.
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(SuspenseContext::new(cx))
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying suspense context.
|
||||
pub fn as_inner(&self) -> &SuspenseContext {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl SuspenseContext {
|
||||
/// Whether the suspense contains local resources at this moment,
|
||||
/// and therefore can't be serialized
|
||||
@@ -32,6 +51,25 @@ impl SuspenseContext {
|
||||
pub fn should_block(&self) -> bool {
|
||||
self.should_block.get_value()
|
||||
}
|
||||
|
||||
/// Returns a `Future` that resolves when this suspense is resolved.
|
||||
pub fn to_future(&self, cx: Scope) -> impl Future<Output = ()> {
|
||||
use futures::StreamExt;
|
||||
|
||||
let pending_resources = self.pending_resources;
|
||||
let (tx, mut rx) = futures::channel::mpsc::channel(1);
|
||||
let tx = RefCell::new(tx);
|
||||
queue_microtask(move || {
|
||||
create_isomorphic_effect(cx, move |_| {
|
||||
if pending_resources.get() == 0 {
|
||||
_ = tx.borrow_mut().try_send(());
|
||||
}
|
||||
})
|
||||
});
|
||||
async move {
|
||||
rx.next().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for SuspenseContext {
|
||||
@@ -98,6 +136,12 @@ impl SuspenseContext {
|
||||
});
|
||||
}
|
||||
|
||||
/// Resets the counter of pending resources.
|
||||
pub fn clear(&self) {
|
||||
self.set_pending_resources.set(0);
|
||||
self.pending_serializable_resources.set(0);
|
||||
}
|
||||
|
||||
/// Tests whether all of the pending resources have resolved.
|
||||
pub fn ready(&self) -> bool {
|
||||
self.pending_resources
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
/// Reactive Trigger, notifies reactive code to rerun.
|
||||
///
|
||||
/// See [`create_trigger`] for more.
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Trigger {
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: NodeId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.3.0-alpha"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -12,6 +12,7 @@ cfg-if = "1"
|
||||
leptos = { workspace = true }
|
||||
tracing = "0.1"
|
||||
wasm-bindgen = "0.2"
|
||||
indexmap = "1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
//! which mode your app is operating in.
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use indexmap::IndexMap;
|
||||
use leptos::{
|
||||
leptos_dom::{debug_warn, html::AnyElement},
|
||||
*,
|
||||
@@ -52,7 +53,6 @@ use leptos::{
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
rc::Rc,
|
||||
};
|
||||
@@ -99,7 +99,7 @@ pub struct MetaTagsContext {
|
||||
#[allow(clippy::type_complexity)]
|
||||
els: Rc<
|
||||
RefCell<
|
||||
HashMap<
|
||||
IndexMap<
|
||||
Cow<'static, str>,
|
||||
(HtmlElement<AnyElement>, Scope, Option<web_sys::Element>),
|
||||
>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.3.0-alpha"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -28,6 +28,7 @@ js-sys = { version = "0.3" }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
wasm-bindgen-futures = { version = "0.4" }
|
||||
lru = { version = "0.10", optional = true }
|
||||
serde_json = "1.0.96"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{use_navigate, use_resolved_path, ToHref, Url};
|
||||
use leptos::{html::form, *};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{error::Error, rc::Rc};
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
@@ -94,8 +95,63 @@ where
|
||||
let action = use_resolved_path(cx, move || action.clone())
|
||||
.get()
|
||||
.unwrap_or_default();
|
||||
// multipart POST (setting Context-Type breaks the request)
|
||||
if method == "post" && enctype == "multipart/form-data" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
spawn_local(async move {
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.redirect(RequestRedirect::Follow)
|
||||
.body(form_data)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(error) = error {
|
||||
error.set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
// Check all the logical 3xx responses that might
|
||||
// get returned from a server function
|
||||
if resp.redirected() {
|
||||
let resp_url = &resp.url();
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
request_animation_frame(move || {
|
||||
if let Err(e) = navigate(
|
||||
&format!(
|
||||
"{}{}",
|
||||
url.pathname, url.search,
|
||||
),
|
||||
Default::default(),
|
||||
) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => warn!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// POST
|
||||
if method == "post" {
|
||||
else if method == "post" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
@@ -240,7 +296,7 @@ pub fn ActionForm<I, O>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
I: Clone + ServerFn + 'static,
|
||||
O: Clone + Serializable + 'static,
|
||||
O: Clone + Serialize + DeserializeOwned + 'static,
|
||||
{
|
||||
let action_url = if let Some(url) = action.url() {
|
||||
url
|
||||
@@ -268,7 +324,6 @@ where
|
||||
|
||||
let on_response = Rc::new(move |resp: &web_sys::Response| {
|
||||
let resp = resp.clone().expect("couldn't get Response");
|
||||
let status = resp.status();
|
||||
spawn_local(async move {
|
||||
let redirected = resp.redirected();
|
||||
|
||||
@@ -277,23 +332,33 @@ where
|
||||
resp.text().expect("couldn't get .text() from Response"),
|
||||
)
|
||||
.await;
|
||||
let status = resp.status();
|
||||
match body {
|
||||
Ok(json) => {
|
||||
// 500 just returns text of error, not JSON
|
||||
if status == 500 {
|
||||
let err = ServerFnError::ServerError(
|
||||
json.as_string().unwrap_or_default(),
|
||||
);
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(err.clone())));
|
||||
let json = json
|
||||
.as_string()
|
||||
.expect("couldn't get String from JsString");
|
||||
if (500..=599).contains(&status) {
|
||||
match serde_json::from_str::<ServerFnError>(&json) {
|
||||
Ok(res) => {
|
||||
value.try_set(Some(Err(res)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
value.try_set(Some(Err(
|
||||
ServerFnError::Deserialization(
|
||||
e.to_string(),
|
||||
),
|
||||
)));
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
value.try_set(Some(Err(err)));
|
||||
} else {
|
||||
match O::de(
|
||||
&json.as_string().expect(
|
||||
"couldn't get String from JsString",
|
||||
),
|
||||
) {
|
||||
match serde_json::from_str::<O>(&json) {
|
||||
Ok(res) => {
|
||||
value.try_set(Some(Ok(res)));
|
||||
if let Some(error) = error {
|
||||
|
||||
@@ -90,10 +90,14 @@ where
|
||||
children: Children,
|
||||
) -> HtmlElement<leptos::html::A> {
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
_ = state;
|
||||
{
|
||||
_ = state;
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
_ = replace;
|
||||
{
|
||||
_ = replace;
|
||||
}
|
||||
|
||||
let location = use_location(cx);
|
||||
let is_active = create_memo(cx, move |_| match href.get() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod form;
|
||||
mod link;
|
||||
mod outlet;
|
||||
mod progress;
|
||||
mod redirect;
|
||||
mod route;
|
||||
mod router;
|
||||
@@ -9,6 +10,7 @@ mod routes;
|
||||
pub use form::*;
|
||||
pub use link::*;
|
||||
pub use outlet::*;
|
||||
pub use progress::*;
|
||||
pub use redirect::*;
|
||||
pub use route::*;
|
||||
pub use router::*;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
animation::{Animation, AnimationState},
|
||||
use_is_back_navigation, use_route,
|
||||
use_is_back_navigation, use_route, SetIsRouting,
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
@@ -45,6 +45,35 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
let outlet: Signal<Option<View>> =
|
||||
if cfg!(any(feature = "csr", feature = "hydrate"))
|
||||
&& use_context::<SetIsRouting>(cx).is_some()
|
||||
{
|
||||
let global_suspense = expect_context::<GlobalSuspenseContext>(cx);
|
||||
let is_fallback =
|
||||
create_memo(cx, move |_| !global_suspense.as_inner().ready());
|
||||
|
||||
let last_two_views = create_memo(
|
||||
cx,
|
||||
move |prev: Option<&(Option<View>, Option<View>)>| match prev {
|
||||
None => (outlet.get(), None),
|
||||
Some((curr, _)) => (outlet.get(), curr.clone()),
|
||||
},
|
||||
);
|
||||
|
||||
create_memo(cx, move |_| {
|
||||
let (curr, prev) = last_two_views.get();
|
||||
if is_fallback.get() && prev.is_some() {
|
||||
prev
|
||||
} else {
|
||||
curr
|
||||
}
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
outlet.into()
|
||||
};
|
||||
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || outlet.get())
|
||||
}
|
||||
|
||||
|
||||
66
router/src/components/progress.rs
Normal file
66
router/src/components/progress.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use leptos::{leptos_dom::helpers::IntervalHandle, *};
|
||||
|
||||
/// A visible indicator that the router is in the process of navigating
|
||||
/// to another route.
|
||||
///
|
||||
/// This is used when `<Router set_is_routing>` has been provided, to
|
||||
/// provide some visual indicator that the page is currently loading
|
||||
/// async data, so that it is does not appear to have frozen. It can be
|
||||
/// styled independently.
|
||||
#[component]
|
||||
pub fn RoutingProgress(
|
||||
cx: Scope,
|
||||
/// Whether the router is currently loading the new page.
|
||||
#[prop(into)]
|
||||
is_routing: Signal<bool>,
|
||||
/// The maximum expected time for loading, which is used to
|
||||
/// calibrate the animation process.
|
||||
#[prop(optional, into)]
|
||||
max_time: std::time::Duration,
|
||||
/// The time to show the full progress bar after page has loaded, before hiding it. (Defaults to 100ms.)
|
||||
#[prop(default = std::time::Duration::from_millis(250))]
|
||||
before_hiding: std::time::Duration,
|
||||
/// CSS classes to be applied to the `<progress>`.
|
||||
#[prop(optional, into)]
|
||||
class: String,
|
||||
) -> impl IntoView {
|
||||
const INCREMENT_EVERY_MS: f32 = 5.0;
|
||||
let expected_increments =
|
||||
max_time.as_secs_f32() / (INCREMENT_EVERY_MS / 1000.0);
|
||||
let percent_per_increment = 100.0 / expected_increments;
|
||||
|
||||
let (is_showing, set_is_showing) = create_signal(cx, false);
|
||||
let (progress, set_progress) = create_signal(cx, 0.0);
|
||||
|
||||
create_effect(cx, move |prev: Option<Option<IntervalHandle>>| {
|
||||
if is_routing.get() {
|
||||
set_is_showing.set(true);
|
||||
set_interval_with_handle(
|
||||
move || {
|
||||
set_progress.update(|n| *n += percent_per_increment);
|
||||
},
|
||||
std::time::Duration::from_millis(INCREMENT_EVERY_MS as u64),
|
||||
)
|
||||
.ok()
|
||||
} else {
|
||||
set_progress.set(100.0);
|
||||
set_timeout(
|
||||
move || {
|
||||
set_progress.set(0.0);
|
||||
set_is_showing.set(false);
|
||||
},
|
||||
before_hiding,
|
||||
);
|
||||
if let Some(Some(interval)) = prev {
|
||||
interval.clear();
|
||||
}
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<Show when=move || is_showing.get() fallback=|_| ()>
|
||||
<progress class=class.clone() min="0" max="100" value=move || progress.get()/>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ pub fn Router(
|
||||
/// A fallback that should be shown if no route is matched.
|
||||
#[prop(optional)]
|
||||
fallback: Option<fn(Scope) -> View>,
|
||||
/// A signal that will be set while the navigation process is underway.
|
||||
#[prop(optional, into)]
|
||||
set_is_routing: Option<SignalSetter<bool>>,
|
||||
/// The `<Router/>` should usually wrap your whole page. It can contain
|
||||
/// any elements, and should include a [Routes](crate::Routes) component somewhere
|
||||
/// to define and display [Route](crate::Route)s.
|
||||
@@ -32,10 +35,17 @@ pub fn Router(
|
||||
// create a new RouterContext and provide it to every component beneath the router
|
||||
let router = RouterContext::new(cx, base, fallback);
|
||||
provide_context(cx, router);
|
||||
provide_context(cx, GlobalSuspenseContext::new(cx));
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
provide_context(cx, SetIsRouting(set_is_routing));
|
||||
}
|
||||
|
||||
children(cx)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct SetIsRouting(pub SignalSetter<bool>);
|
||||
|
||||
/// Context type that contains information about the current router state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouterContext {
|
||||
@@ -228,6 +238,11 @@ impl RouterContextInner {
|
||||
resolve_path("", to, None).map(String::from)
|
||||
};
|
||||
|
||||
// reset count of pending resources at global level
|
||||
expect_context::<GlobalSuspenseContext>(cx)
|
||||
.as_inner()
|
||||
.clear();
|
||||
|
||||
match resolved_to {
|
||||
None => Err(NavigationError::NotRoutable(to.to_string())),
|
||||
Some(resolved_to) => {
|
||||
@@ -262,18 +277,32 @@ impl RouterContextInner {
|
||||
move |state| *state = next_state
|
||||
});
|
||||
|
||||
self.path_stack.update_value(|stack| {
|
||||
let global_suspense =
|
||||
expect_context::<GlobalSuspenseContext>(cx);
|
||||
let path_stack = self.path_stack;
|
||||
path_stack.update_value(|stack| {
|
||||
stack.push(resolved_to.clone())
|
||||
});
|
||||
|
||||
if referrers.borrow().len() == len {
|
||||
this.navigate_end(LocationChange {
|
||||
value: resolved_to,
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state,
|
||||
})
|
||||
let set_is_routing = use_context::<SetIsRouting>(cx);
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
set_is_routing.0.set(true);
|
||||
}
|
||||
spawn_local(async move {
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
global_suspense.as_inner().to_future(cx).await;
|
||||
set_is_routing.0.set(false);
|
||||
}
|
||||
|
||||
if referrers.borrow().len() == len {
|
||||
this.navigate_end(LocationChange {
|
||||
value: resolved_to,
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
expand_optionals, get_route_matches, join_paths, Branch, Matcher,
|
||||
RouteDefinition, RouteMatch,
|
||||
},
|
||||
use_is_back_navigation, RouteContext, RouterContext,
|
||||
use_is_back_navigation, RouteContext, RouterContext, SetIsRouting,
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
@@ -56,6 +56,7 @@ pub fn Routes(
|
||||
|
||||
let id = HydrationCtx::id();
|
||||
let root = root_route(cx, base_route, route_states, root_equal);
|
||||
|
||||
leptos::leptos_dom::DynChild::new_with_id(id, move || root.get())
|
||||
.into_view(cx)
|
||||
}
|
||||
@@ -408,37 +409,67 @@ fn root_route(
|
||||
) -> Memo<Option<View>> {
|
||||
let root_cx = RefCell::new(None);
|
||||
|
||||
create_memo(cx, move |prev| {
|
||||
provide_context(cx, route_states);
|
||||
route_states.with(|state| {
|
||||
if state.routes.borrow().is_empty() {
|
||||
Some(base_route.outlet(cx).into_view(cx))
|
||||
} else {
|
||||
let root = state.routes.borrow();
|
||||
let root = root.get(0);
|
||||
if let Some(route) = root {
|
||||
provide_context(cx, route.clone());
|
||||
}
|
||||
|
||||
if prev.is_none() || !root_equal.get() {
|
||||
let (root_view, _) = cx.run_child_scope(|cx| {
|
||||
let prev_cx = std::mem::replace(
|
||||
&mut *root_cx.borrow_mut(),
|
||||
Some(cx),
|
||||
);
|
||||
if let Some(prev_cx) = prev_cx {
|
||||
prev_cx.dispose();
|
||||
}
|
||||
root.as_ref()
|
||||
.map(|route| route.outlet(cx).into_view(cx))
|
||||
});
|
||||
root_view
|
||||
let root_view = create_memo(cx, {
|
||||
let root_equal = Rc::clone(&root_equal);
|
||||
move |prev| {
|
||||
provide_context(cx, route_states);
|
||||
route_states.with(|state| {
|
||||
if state.routes.borrow().is_empty() {
|
||||
Some(base_route.outlet(cx).into_view(cx))
|
||||
} else {
|
||||
prev.cloned().unwrap()
|
||||
let root = state.routes.borrow();
|
||||
let root = root.get(0);
|
||||
if let Some(route) = root {
|
||||
provide_context(cx, route.clone());
|
||||
}
|
||||
|
||||
if prev.is_none() || !root_equal.get() {
|
||||
let (root_view, _) = cx.run_child_scope(|cx| {
|
||||
let prev_cx = std::mem::replace(
|
||||
&mut *root_cx.borrow_mut(),
|
||||
Some(cx),
|
||||
);
|
||||
if let Some(prev_cx) = prev_cx {
|
||||
prev_cx.dispose();
|
||||
}
|
||||
root.as_ref()
|
||||
.map(|route| route.outlet(cx).into_view(cx))
|
||||
});
|
||||
root_view
|
||||
} else {
|
||||
prev.cloned().unwrap()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if cfg!(any(feature = "csr", feature = "hydrate"))
|
||||
&& use_context::<SetIsRouting>(cx).is_some()
|
||||
{
|
||||
let global_suspense = expect_context::<GlobalSuspenseContext>(cx);
|
||||
let is_fallback =
|
||||
create_memo(cx, move |_| !global_suspense.as_inner().ready());
|
||||
|
||||
let last_two_views = create_memo(
|
||||
cx,
|
||||
move |prev: Option<&(Option<View>, Option<View>)>| match prev {
|
||||
None => (root_view.get(), None),
|
||||
Some((curr, _)) => (root_view.get(), curr.clone()),
|
||||
},
|
||||
);
|
||||
|
||||
create_memo(cx, move |_| {
|
||||
let (curr, prev) = last_two_views.get();
|
||||
if is_fallback.get() && prev.is_some() && !root_equal.get() {
|
||||
prev
|
||||
} else {
|
||||
curr
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
root_view
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -135,14 +135,14 @@ impl History for BrowserIntegration {
|
||||
|
||||
/// The wrapper type that the [Router](crate::Router) uses to interact with a [History].
|
||||
/// This is automatically provided in the browser. For the server, it should be provided
|
||||
/// as a context.
|
||||
/// as a context. Be sure that it can survive conversion to a URL in the browser.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_router::*;
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// let integration = ServerIntegration {
|
||||
/// path: "insert/current/path/here".to_string(),
|
||||
/// path: "http://leptos.rs/".to_string(),
|
||||
/// };
|
||||
/// provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
/// # });
|
||||
@@ -167,7 +167,24 @@ impl History for RouterIntegrationContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic router integration for the server side. All its need is the current path.
|
||||
/// A generic router integration for the server side.
|
||||
///
|
||||
/// This should match what the browser history will show.
|
||||
///
|
||||
/// Generally, this will already be provided if you are using the leptos
|
||||
/// server integrations.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_router::*;
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// let integration = ServerIntegration {
|
||||
/// // Swap out with your URL if integrating manually.
|
||||
/// path: "http://leptos.rs/".to_string(),
|
||||
/// };
|
||||
/// provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServerIntegration {
|
||||
pub path: String,
|
||||
|
||||
@@ -42,7 +42,11 @@ impl TryFrom<&str> for Url {
|
||||
Ok(Self {
|
||||
origin: url.origin(),
|
||||
pathname: url.pathname(),
|
||||
search: url.search(),
|
||||
search: url
|
||||
.search()
|
||||
.strip_prefix('?')
|
||||
.map(String::from)
|
||||
.unwrap_or_default(),
|
||||
search_params: ParamsMap(
|
||||
try_iter(&url.search_params())
|
||||
.map_js_error()?
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
/// 2. **Out-of-order streaming**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `Suspense` nodes.
|
||||
/// - *Pros*: Combines the best of **synchronous** and **`async`**, with a very fast shell and resources that begin loading on the server.
|
||||
/// - *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
|
||||
/// 3. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
|
||||
/// 3. **Partially-blocked out-of-order streaming**: Using `create_blocking_resource` with out-of-order streaming still sends fallbacks and relies on JavaScript to fill them in with the fragments. Partially-blocked streaming does this replacement on the server, making for a slower response but requiring no JavaScript to show blocking resources.
|
||||
/// - *Pros*: Works better if JS is disabled.
|
||||
/// - *Cons*: Slower initial response because of additional string manipulation on server.
|
||||
/// 4. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
|
||||
/// - *Pros*: Does not require JS for HTML to appear in correct order.
|
||||
/// - *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
|
||||
/// of the page will not be interactive until the suspended chunks have loaded.
|
||||
/// 4. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
|
||||
/// 5. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
|
||||
/// - *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
|
||||
/// - *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
|
||||
///
|
||||
@@ -23,6 +26,7 @@
|
||||
pub enum SsrMode {
|
||||
#[default]
|
||||
OutOfOrder,
|
||||
PartiallyBlocked,
|
||||
InOrder,
|
||||
Async,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ serde_qs = "0.12"
|
||||
thiserror = "1"
|
||||
serde_json = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
syn = { version = "2", features = ["full", "parsing", "extra-traits"] }
|
||||
proc-macro2 = "1"
|
||||
ciborium = "0.2"
|
||||
xxhash-rust = { version = "0.8", features = ["const_xxh64"] }
|
||||
|
||||
@@ -11,7 +11,7 @@ description = "The default implementation of the server_fn macro without a conte
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "1", features = ["full"] }
|
||||
syn = { version = "2", features = ["full"] }
|
||||
server_fn_macro = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "../README.md"
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
syn = { version = "2", features = ["full", "parsing", "extra-traits"] }
|
||||
proc-macro2 = "1"
|
||||
proc-macro-error = "1"
|
||||
xxhash-rust = { version = "0.8.6", features = ["const_xxh64"] }
|
||||
|
||||
@@ -75,9 +75,11 @@ pub fn server_macro_impl(
|
||||
struct_name,
|
||||
prefix,
|
||||
encoding,
|
||||
fn_path,
|
||||
..
|
||||
} = syn::parse2::<ServerFnName>(args)?;
|
||||
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
|
||||
let fn_path = fn_path.unwrap_or_else(|| Literal::string(""));
|
||||
let encoding = quote!(#server_fn_path::#encoding);
|
||||
|
||||
let body = syn::parse::<ServerFnBody>(body.into())?;
|
||||
@@ -213,7 +215,11 @@ pub fn server_macro_impl(
|
||||
}
|
||||
|
||||
fn url() -> &'static str {
|
||||
if !#fn_path.is_empty(){
|
||||
#fn_path
|
||||
} else {
|
||||
#server_fn_path::const_format::concatcp!(#fn_name_as_str, #server_fn_path::xxhash_rust::const_xxh64::xxh64(concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(), 0))
|
||||
}
|
||||
}
|
||||
|
||||
fn encoding() -> #server_fn_path::Encoding {
|
||||
@@ -260,6 +266,8 @@ struct ServerFnName {
|
||||
prefix: Option<Literal>,
|
||||
_comma2: Option<Token![,]>,
|
||||
encoding: Path,
|
||||
_comma3: Option<Token![,]>,
|
||||
fn_path: Option<Literal>,
|
||||
}
|
||||
|
||||
impl Parse for ServerFnName {
|
||||
@@ -280,6 +288,8 @@ impl Parse for ServerFnName {
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| syn::parse_quote!(Encoding::Url));
|
||||
let _comma3 = input.parse()?;
|
||||
let fn_path = input.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
struct_name,
|
||||
@@ -287,6 +297,8 @@ impl Parse for ServerFnName {
|
||||
prefix,
|
||||
_comma2,
|
||||
encoding,
|
||||
_comma3,
|
||||
fn_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user