mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
21 Commits
clippy
...
send-sync-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f704f91fe | ||
|
|
4b0e212f1c | ||
|
|
8fa89cffef | ||
|
|
18ceb4c4f8 | ||
|
|
0d3e2baa9b | ||
|
|
1563d237d0 | ||
|
|
b861f84e40 | ||
|
|
62812af5b2 | ||
|
|
f300e7fd41 | ||
|
|
44974fcf69 | ||
|
|
815c2e6dc2 | ||
|
|
2fc20d8312 | ||
|
|
679692e202 | ||
|
|
be1343fa88 | ||
|
|
fc7199f188 | ||
|
|
154e42f3f4 | ||
|
|
4c24795ffd | ||
|
|
f2e7b00d5a | ||
|
|
0b36b68846 | ||
|
|
f24bad4bf2 | ||
|
|
a2ea1d8483 |
20
README.md
20
README.md
@@ -80,7 +80,7 @@ If you’re on `stable`, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0-alpha", features = ["stable"] }`
|
||||
2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
you’ll just call `.get()`, `.set()`, or `.update()` manually. Check out the
|
||||
you’ll just call `.get()`, `.set()`, or `.update()` manually. Check out the
|
||||
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
|
||||
for examples of the correct API.
|
||||
|
||||
@@ -99,6 +99,24 @@ Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
## FAQs
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
People usually mean one of three things by this question.
|
||||
|
||||
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
|
||||
|
||||
With 0.1 the APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
Yes, I’m sure there are. You can see from the state of our issue tracker over time that there aren’t that _many_ bugs and they’re usually resolved pretty quickly. But for sure, there may be moments where you encounter something that requires a fix at the framework level, which may not be immediately resolved.
|
||||
|
||||
3. **Am I a consumer or a contributor?**
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive native any GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
This example creates a simple counter in a client side rendered app with Rust and WASM!
|
||||
|
||||
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
@@ -47,6 +47,7 @@ skip_feature_sets = [["ssr", "hydrate"]]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "counter_isomorphic"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
@@ -56,7 +57,7 @@ site-pkg-dir = "pkg"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-address = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
|
||||
|
||||
## Client Side Rendering
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
|
||||
For this example the server must store the counter state since it can be modified by many users.
|
||||
This means it is not possible to produce a working CSR-only version as a non-static server is required.
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
@@ -28,7 +28,7 @@ cargo leptos build --release
|
||||
## Server Side Rendering without cargo-leptos
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. For examples with CSS you also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
1. Install wasm-pack
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
|
||||
@@ -34,6 +34,7 @@ cfg_if! {
|
||||
crate::counters::register_server_functions();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_address.clone();
|
||||
@@ -56,14 +57,11 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
// client-only main for Trunk
|
||||
else {
|
||||
use counter_isomorphic::counters::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counter/> });
|
||||
// isomorphic counters cannot work in a Client-Side-Rendered only
|
||||
// app as a server is required to maintain state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
examples/counter_without_macros/Cargo.toml
Normal file
13
examples/counter_without_macros/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "counter_without_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
5
examples/counter_without_macros/README.md
Normal file
5
examples/counter_without_macros/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Leptos Counter Example
|
||||
|
||||
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
|
||||
|
||||
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
BIN
examples/counter_without_macros/public/favicon.ico
Normal file
BIN
examples/counter_without_macros/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
44
examples/counter_without_macros/src/lib.rs
Normal file
44
examples/counter_without_macros/src/lib.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use leptos::{ev, *};
|
||||
|
||||
pub struct Props {
|
||||
/// The starting value for the counter
|
||||
pub initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
pub step: i32,
|
||||
}
|
||||
|
||||
/// A simple counter view.
|
||||
pub fn view(cx: Scope, props: Props) -> impl IntoView {
|
||||
let Props {
|
||||
initial_value,
|
||||
step,
|
||||
} = props;
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
div(cx)
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value = 0))
|
||||
.child((cx, "Clear")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value -= step))
|
||||
.child((cx, "-1")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
span(cx)
|
||||
.child((cx, "Value: "))
|
||||
.child((cx, move || value.get()))
|
||||
.child((cx, "!")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value += step))
|
||||
.child((cx, "+1")),
|
||||
))
|
||||
}
|
||||
16
examples/counter_without_macros/src/main.rs
Normal file
16
examples/counter_without_macros/src/main.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use counter_without_macros as counter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
58
examples/counter_without_macros/tests/mod.rs
Normal file
58
examples/counter_without_macros/tests/mod.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counter_without_macros as counter;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let dec = clear
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let text = dec
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let inc = text
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
inc.click();
|
||||
inc.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
|
||||
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
|
||||
|
||||
clear.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
}
|
||||
9
examples/counters/README.md
Normal file
9
examples/counters/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Leptos Counters Example
|
||||
|
||||
This example showcases a basic leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
9
examples/counters_stable/README.md
Normal file
9
examples/counters_stable/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Leptos Counters Example on Rust Stable
|
||||
|
||||
This example showcases a basic Leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events. Unlike the other counters example, it will compile on Rust stable, because it has the `stable` feature enabled.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
9
examples/fetch/README.md
Normal file
9
examples/fetch/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Client Side Fetch
|
||||
|
||||
This example shows how to fetch data from the client in WebAssembly.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -58,7 +58,7 @@ style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-address = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-cargo-features="csr"/>
|
||||
<link data-trunk rel="css" href="./style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
|
||||
@@ -46,7 +46,11 @@ cfg_if! {
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
use hackernews::{App, AppProps};
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <App/> })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-address = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
17
examples/parent_child/README.md
Normal file
17
examples/parent_child/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Parent Child Example
|
||||
|
||||
This example highlights four different ways that child components can communicate with their parent:
|
||||
|
||||
1. <ButtonA/>: passing a WriteSignal as one of the child component props,
|
||||
for the child component to write into and the parent to read
|
||||
2. <ButtonB/>: passing a closure as one of the child component props, for
|
||||
the child component to call
|
||||
3. <ButtonC/>: adding a simple event listener on the child component itself
|
||||
4. <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -1,8 +1,11 @@
|
||||
# Leptos Router Example
|
||||
|
||||
This example demonstrates how Leptos' router works
|
||||
This example demonstrates how Leptos’s router works for client side routing.
|
||||
|
||||
## Build and Run it
|
||||
|
||||
## Run it
|
||||
```bash
|
||||
trunk serve --open
|
||||
```
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
@@ -91,7 +91,7 @@ style-file = "style/output.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-address = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
@@ -60,7 +60,7 @@ style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-address = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
@@ -65,7 +65,7 @@ style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-address = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
|
||||
10
examples/todomvc/README.md
Normal file
10
examples/todomvc/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos TodoMVC
|
||||
|
||||
This is a Leptos implementation of the TodoMVC example common to many frameworks. This is a relatively-simple application but shows off features like interaction between components and state management.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -1,5 +1,11 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Provides functions to easily integrate Leptos with Actix.
|
||||
//!
|
||||
//! For more details on how to use the integrations, see the
|
||||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
//! directory in the Leptos repository.
|
||||
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{ServiceFactory, ServiceRequest},
|
||||
@@ -68,8 +74,8 @@ impl ResponseOptions {
|
||||
}
|
||||
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
|
||||
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
|
||||
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
|
||||
pub async fn redirect(cx: leptos::Scope, path: &str) {
|
||||
let response_options = use_context::<ResponseOptions>(cx).unwrap();
|
||||
response_options.set_status(StatusCode::FOUND).await;
|
||||
@@ -114,6 +120,11 @@ pub async fn redirect(cx: leptos::Scope, path: &str) {
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
pub fn handle_server_fns() -> Route {
|
||||
handle_server_fns_with_context(|_cx| {})
|
||||
}
|
||||
@@ -127,7 +138,12 @@ pub fn handle_server_fns() -> Route {
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// This version allows you to pass in a closure that adds additional route data to the
|
||||
/// context, allowing you to pass in info about the route or user from Actix, or other info
|
||||
/// context, allowing you to pass in info about the route or user from Actix, or other info.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
pub fn handle_server_fns_with_context(
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> Route {
|
||||
@@ -270,6 +286,13 @@ pub fn handle_server_fns_with_context(
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## 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)
|
||||
pub fn render_app_to_stream<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
@@ -285,6 +308,13 @@ where
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// ## 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)
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
@@ -360,6 +390,13 @@ where
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## 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)
|
||||
pub fn render_preloaded_data_app<Data, Fut, IV>(
|
||||
options: LeptosOptions,
|
||||
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Provides functions to easily integrate Leptos with Axum.
|
||||
//!
|
||||
//! For more details on how to use the integrations, see the
|
||||
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
//! directory in the Leptos repository.
|
||||
|
||||
use axum::{
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
extract::Path,
|
||||
@@ -91,6 +97,8 @@ pub async fn redirect(cx: leptos::Scope, path: &str) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
/// and other data without consuming the body.
|
||||
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
// provide request headers as context in server scope
|
||||
let (parts, body) = req.into_parts();
|
||||
@@ -107,8 +115,6 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
///
|
||||
/// This provides an `Arc<[Request<Body>](axum::http::Request)>` [Scope](leptos::Scope).
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// ```
|
||||
@@ -136,145 +142,53 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
/// ```
|
||||
/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired,
|
||||
/// you can specify your own server fn handler based on this one and give it it's own route in the server macro.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
pub async fn handle_server_fns(
|
||||
Path(fn_name): Path<String>,
|
||||
headers: HeaderMap,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
// Axum Path extractor doesn't remove the first slash from the path, while Actix does
|
||||
let fn_name: String = match fn_name.strip_prefix('/') {
|
||||
Some(path) => path.to_string(),
|
||||
None => fn_name,
|
||||
};
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
spawn_blocking({
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
async move {
|
||||
let res = if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) {
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
|
||||
let req_parts = generate_request_parts(req).await;
|
||||
// Add this so we can get details about the Request
|
||||
provide_context(cx, req_parts.clone());
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
match server_fn(cx, &req_parts.body).await {
|
||||
Ok(serialized) => {
|
||||
// If ResponseParts are set, add the headers and extension to the request
|
||||
let res_options = use_context::<ResponseOptions>(cx);
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header =
|
||||
headers.get("Accept").and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
|
||||
// Add headers from ResponseParts if they exist. These should be added as long
|
||||
// as the server function returns an OK response
|
||||
let res_options_outer = res_options.unwrap().0;
|
||||
let res_options_inner = res_options_outer.read().await;
|
||||
let (status, mut res_headers) = (
|
||||
res_options_inner.status,
|
||||
res_options_inner.headers.clone(),
|
||||
);
|
||||
|
||||
if let Some(header_ref) = res.headers_mut() {
|
||||
header_ref.extend(res_headers.drain());
|
||||
};
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
let referer = headers
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("/");
|
||||
|
||||
res = res
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header("Location", referer);
|
||||
}
|
||||
// Override StatusCode if it was set in a Resource or Element
|
||||
res = match status {
|
||||
Some(status) => res.status(status),
|
||||
None => res,
|
||||
};
|
||||
match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header("Content-Type", "application/cbor")
|
||||
.body(Full::from(data)),
|
||||
Payload::Url(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Json(data) => res
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Full::from(data)),
|
||||
}
|
||||
}
|
||||
Err(e) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Full::from(e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Full::from(
|
||||
format!("Could not find a server function at the route {fn_name}. \
|
||||
\n\nIt's likely that you need to call ServerFn::register() on the \
|
||||
server function type, somewhere in your `main` function." )
|
||||
))
|
||||
}
|
||||
.expect("could not build Response");
|
||||
|
||||
_ = tx.send(res);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
rx.await.unwrap()
|
||||
handle_server_fns_inner(fn_name, headers, |_| {}, req).await
|
||||
}
|
||||
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
///
|
||||
/// This provides an `Arc<[Request<Body>](axum::http::Request)>` [Scope](leptos::Scope).
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// This version allows you to pass in a closure to capture additional data from the layers above leptos
|
||||
/// and store it in context. To use it, you'll need to define your own route, and a handler function
|
||||
/// that takes in the data you'd like. See the `render_app_to_stream_with_context()` docs for an example
|
||||
/// of one that should work much like this one
|
||||
/// that takes in the data you'd like. See the [render_app_to_stream_with_context] docs for an example
|
||||
/// of one that should work much like this one.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
pub async fn handle_server_fns_with_context(
|
||||
Path(fn_name): Path<String>,
|
||||
headers: HeaderMap,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
handle_server_fns_inner(fn_name, headers, additional_context, req).await
|
||||
}
|
||||
|
||||
async fn handle_server_fns_inner(
|
||||
fn_name: String,
|
||||
headers: HeaderMap,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
// Axum Path extractor doesn't remove the first slash from the path, while Actix does
|
||||
let fn_name: String = match fn_name.strip_prefix('/') {
|
||||
Some(path) => path.to_string(),
|
||||
None => fn_name,
|
||||
};
|
||||
let fn_name = fn_name
|
||||
.strip_prefix('/')
|
||||
.map(|fn_name| fn_name.to_string())
|
||||
.unwrap_or(fn_name);
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
spawn_blocking({
|
||||
@@ -430,6 +344,12 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## 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<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
@@ -442,178 +362,7 @@ pub fn render_app_to_stream<IV>(
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
move |req: Request<Body>| {
|
||||
Box::pin({
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let default_res_options = ResponseOptions::default();
|
||||
let res_options2 = default_res_options.clone();
|
||||
let res_options3 = default_res_options.clone();
|
||||
|
||||
async move {
|
||||
// Need to get the path and query string of the Request
|
||||
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
||||
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
|
||||
let path = req.uri().path_and_query().unwrap().as_str();
|
||||
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let site_ip = &options.site_address.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
if (msg.css) {{
|
||||
const link = document.querySelector("link#leptos");
|
||||
if (link) {{
|
||||
let href = link.getAttribute('href').split('?')[0];
|
||||
let newHref = href + '?version=' + new Date().getMilliseconds();
|
||||
link.setAttribute('href', newHref);
|
||||
}} else {{
|
||||
console.warn("Could not find link#leptos");
|
||||
}}
|
||||
}};
|
||||
}};
|
||||
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
let tail = "</body></html>";
|
||||
|
||||
let (mut tx, rx) = futures::channel::mpsc::channel(8);
|
||||
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
let app_fn = app_fn.clone();
|
||||
async move {
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let req_parts = generate_request_parts(req).await;
|
||||
move |cx| {
|
||||
let integration = ServerIntegration {
|
||||
path: full_path.clone(),
|
||||
};
|
||||
provide_context(
|
||||
cx,
|
||||
RouterIntegrationContext::new(integration),
|
||||
);
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, req_parts);
|
||||
provide_context(cx, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
let (bundle, runtime, scope) =
|
||||
render_to_stream_with_prefix_undisposed(
|
||||
app,
|
||||
|cx| {
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>").into()
|
||||
},
|
||||
);
|
||||
let mut shell = Box::pin(bundle);
|
||||
while let Some(fragment) = shell.next().await {
|
||||
_ = tx.send(fragment).await;
|
||||
}
|
||||
|
||||
// Extract the value of ResponseOptions from here
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
let new_res_parts = res_options.0.read().await.clone();
|
||||
|
||||
let mut writable = res_options2.0.write().await;
|
||||
*writable = new_res_parts;
|
||||
|
||||
runtime.dispose();
|
||||
|
||||
tx.close_channel();
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
.chain(rx)
|
||||
.chain(futures::stream::once(async { tail.to_string() }))
|
||||
.map(|html| Ok(Bytes::from(html))),
|
||||
);
|
||||
|
||||
// Get the first, second, and third chunks in the stream, which renders the app shell, and thus allows Resources to run
|
||||
let first_chunk = stream.next().await;
|
||||
let second_chunk = stream.next().await;
|
||||
let third_chunk = stream.next().await;
|
||||
|
||||
// Extract the resources now that they've been rendered
|
||||
let res_options = res_options3.0.read().await;
|
||||
|
||||
let complete_stream = futures::stream::iter([
|
||||
first_chunk.unwrap(),
|
||||
second_chunk.unwrap(),
|
||||
third_chunk.unwrap(),
|
||||
])
|
||||
.chain(stream);
|
||||
|
||||
let mut res = Response::new(StreamBody::new(
|
||||
Box::pin(complete_stream) as PinnedHtmlStream
|
||||
));
|
||||
|
||||
if let Some(status) = res_options.status {
|
||||
*res.status_mut() = status
|
||||
}
|
||||
let mut res_headers = res_options.headers.clone();
|
||||
res.headers_mut().extend(res_headers.drain());
|
||||
|
||||
res
|
||||
}
|
||||
})
|
||||
}
|
||||
render_app_to_stream_with_context(options, |_| {}, app_fn)
|
||||
}
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
@@ -633,8 +382,14 @@ where
|
||||
/// handler(req).await.into_response()
|
||||
/// }
|
||||
/// ```
|
||||
/// Otherwise, this function is identical to the `render_app_with_stream() function, which has more info about how this works.`
|
||||
|
||||
/// Otherwise, this function is identical to [render_app_to_stream].
|
||||
///
|
||||
/// ## 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<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
@@ -857,13 +612,8 @@ where
|
||||
let routes = routes.0.read().await.to_owned();
|
||||
// Axum's Router defines Root routes as "/" not ""
|
||||
let routes: Vec<String> = routes
|
||||
.iter()
|
||||
.map(|s| {
|
||||
if s.is_empty() {
|
||||
return "/".to_string();
|
||||
}
|
||||
s.to_string()
|
||||
})
|
||||
.into_iter()
|
||||
.map(|s| if s.is_empty() { "/".to_string() } else { s })
|
||||
.collect();
|
||||
|
||||
if routes.is_empty() {
|
||||
|
||||
@@ -17,36 +17,17 @@ leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.11"
|
||||
once_cell = "1.17.0"
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = ".", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["csr", "serde"]
|
||||
csr = [
|
||||
"leptos_dom/web",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
]
|
||||
hydrate = [
|
||||
"leptos_dom/web",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
]
|
||||
stable = [
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
]
|
||||
csr = ["leptos_dom/web", "leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr"]
|
||||
hydrate = ["leptos_dom/web", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_server/hydrate"]
|
||||
ssr = ["leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "leptos_server/ssr"]
|
||||
stable = ["leptos_dom/stable", "leptos_macro/stable", "leptos_reactive/stable", "leptos_server/stable"]
|
||||
serde = ["leptos_reactive/serde"]
|
||||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
miniserde = ["leptos_reactive/miniserde"]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use leptos_dom::{Errors, Fragment, IntoView, View};
|
||||
use leptos_dom::{Errors, Fragment, IntoView};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
|
||||
|
||||
#[component(transparent)]
|
||||
pub fn ErrorBoundary<F>(
|
||||
pub fn ErrorBoundary<F, IV>(
|
||||
cx: Scope,
|
||||
/// The components inside the tag which will get rendered
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
@@ -11,7 +11,8 @@ pub fn ErrorBoundary<F>(
|
||||
fallback: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope, Option<RwSignal<Errors>>) -> View + 'static,
|
||||
F: Fn(Scope, Option<RwSignal<Errors>>) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
|
||||
|
||||
@@ -21,7 +22,7 @@ where
|
||||
let children = children(cx);
|
||||
|
||||
move || match errors.get().0.is_empty() {
|
||||
true => children.clone(),
|
||||
false => fallback(cx, Some(errors)).into(),
|
||||
true => children.clone().into_view(cx),
|
||||
false => fallback(cx, Some(errors)).into_view(cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
//! or mutating data via async requests to the server
|
||||
//! - multi-page apps (MPAs) rendered on the server, managing navigation, data, and mutations via
|
||||
//! web-standard `<a>` and `<form>` tags
|
||||
//! - progressively-enhanced multi-page apps ([PEMPAs](https://www.epicweb.dev/the-webs-next-transition)?)
|
||||
//! that are rendered on the server and then hydrated on the client, enhancing your `<a>` and `<form>`
|
||||
//! navigations and mutations seamlessly when WASM is available.
|
||||
//! - progressively-enhanced single-page apps that are rendered on the server and then hydrated on the client,
|
||||
//! enhancing your `<a>` and `<form>` navigations and mutations seamlessly when WASM is available.
|
||||
//!
|
||||
//! And you can do all three of these **using the same Leptos code.**
|
||||
//!
|
||||
@@ -24,12 +23,17 @@
|
||||
//!
|
||||
//! # Learning by Example
|
||||
//!
|
||||
//! These docs are a work in progress. If you want to see what Leptos is capable of, check out
|
||||
//! If you want to see what Leptos is capable of, check out
|
||||
//! the [examples](https://github.com/leptos-rs/leptos/tree/main/examples):
|
||||
//! - [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) is the classic
|
||||
//! counter example, showing the basics of client-side rendering and reactive DOM updates
|
||||
//! - [`counter_without_macros`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_without_macros)
|
||||
//! adapts the counter example to use the builder pattern for the UI and avoids other macros, instead showing
|
||||
//! the code that Leptos generates.
|
||||
//! - [`counters`](https://github.com/leptos-rs/leptos/tree/main/examples/counters) introduces parent-child
|
||||
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
|
||||
//! - [`counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable) adapts the `counters` example
|
||||
//! to show how to use Leptos with `stable` Rust.
|
||||
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
|
||||
//! ways a parent component can communicate with a child, including passing a closure, context, and more
|
||||
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) implements the classic to-do
|
||||
@@ -42,15 +46,23 @@
|
||||
//! HTTP request within your reactive code.
|
||||
//! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptos’s nested router
|
||||
//! to enable client-side navigation and route-specific, reactive data loading.
|
||||
//! - [`counter_isomorphic`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic) shows
|
||||
//! different methods of interaction with a stateful server, including server functions, server actions, forms,
|
||||
//! and server-sent events (SSE).
|
||||
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) shows the basics of building an
|
||||
//! isomorphic web app. Both the server and the client import the same app code from the `todomvc` example.
|
||||
//! The server renders the app directly to an HTML string, and the client hydrates that HTML to make it interactive.
|
||||
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews) pulls everything together.
|
||||
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run.
|
||||
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
|
||||
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
|
||||
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
|
||||
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite) and
|
||||
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum)
|
||||
//! show how to build a full-stack app using server functions and database connections.
|
||||
//! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind) shows how to integrate
|
||||
//! TailwindCSS with `cargo-leptos`.
|
||||
//!
|
||||
//! (The SPA examples can be run using `trunk serve`. For information about Trunk,
|
||||
//! [see here]((https://trunkrs.dev/)).)
|
||||
//! Details on how to run each example can be found in its README.
|
||||
//!
|
||||
//! # Quick Links
|
||||
//!
|
||||
@@ -141,7 +153,9 @@ pub use typed_builder;
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
mod for_loop;
|
||||
mod show;
|
||||
pub use for_loop::*;
|
||||
pub use show::*;
|
||||
mod suspense;
|
||||
pub use suspense::*;
|
||||
mod transition;
|
||||
|
||||
32
leptos/src/show.rs
Normal file
32
leptos/src/show.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use leptos::component;
|
||||
use leptos_dom::{Fragment, IntoView};
|
||||
use leptos_reactive::Scope;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// A component that will show it's children when the passed in closure is True, and show the fallback
|
||||
/// when the closure is false
|
||||
#[component]
|
||||
pub fn Show<F, W, IV>(
|
||||
/// The scope the component is running in
|
||||
cx: Scope,
|
||||
/// The components Show wraps
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
/// A closure that returns a bool that determines whether this thing runs
|
||||
when: W,
|
||||
/// A closure that returns what gets rendered if the when statement is false
|
||||
fallback: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
W: Fn() -> bool + 'static,
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
// now you don't render until `when` is actually true
|
||||
let children = Lazy::new(move || children(cx).into_view(cx));
|
||||
let fallback = Lazy::new(move || fallback(cx).into_view(cx));
|
||||
|
||||
move || match when() {
|
||||
true => children.clone(),
|
||||
false => fallback.clone(),
|
||||
}
|
||||
}
|
||||
@@ -72,8 +72,8 @@ fn env_w_default(key: &str, default: &str) -> Result<String, LeptosConfigError>
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum that can be used to define the environment Leptos is running in. Can be passed to [RenderOptions].
|
||||
/// Setting this to the `PROD` variant will not include the websockets code for `cargo-leptos` watch mode.
|
||||
/// An enum that can be used to define the environment Leptos is running in.
|
||||
/// Setting this to the `PROD` variant will not include the WebSocket code for `cargo-leptos` watch mode.
|
||||
/// Defaults to `DEV`.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub enum Env {
|
||||
|
||||
@@ -28,12 +28,12 @@ use wasm_bindgen::JsCast;
|
||||
#[derive(educe::Educe)]
|
||||
#[educe(Default, Clone, PartialEq, Eq)]
|
||||
pub enum CoreComponent {
|
||||
/// The [`Unit`] component.
|
||||
/// The [Unit] component.
|
||||
#[educe(Default)]
|
||||
Unit(UnitRepr),
|
||||
/// The [`DynChild`] component.
|
||||
/// The [DynChild] component.
|
||||
DynChild(DynChildRepr),
|
||||
/// The [`EachKey`] component.
|
||||
/// The [Each] component.
|
||||
Each(EachRepr),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::{HydrationCtx, HydrationKey, IntoView};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::{use_context, RwSignal};
|
||||
use std::{collections::HashMap, error::Error, rc::Rc};
|
||||
use std::{collections::HashMap, error::Error, sync::Arc};
|
||||
|
||||
/// A struct to hold all the possible errors that could be provided by child Views
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Errors(pub HashMap<HydrationKey, Rc<dyn Error>>);
|
||||
pub struct Errors(pub HashMap<HydrationKey, Arc<dyn Error>>);
|
||||
|
||||
impl<T, E> IntoView for Result<T, E>
|
||||
where
|
||||
@@ -56,7 +56,7 @@ impl Errors {
|
||||
where
|
||||
E: Error + 'static,
|
||||
{
|
||||
self.0.insert(key, Rc::new(error));
|
||||
self.0.insert(key, Arc::new(error));
|
||||
}
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove<E>(&mut self, key: &HydrationKey)
|
||||
|
||||
@@ -1043,6 +1043,8 @@ generate_html_tags![
|
||||
footer HtmlElement,
|
||||
/// The `<header>` HTML element represents introductory content, typically a group of introductory or navigational aids. It may contain some heading elements but also a logo, a search form, an author name, and other elements.
|
||||
header HtmlElement,
|
||||
/// The `<hgroup>` HTML element represents a heading and related content. It groups a single `<h1>–<h6>` element with one or more `<p>`.
|
||||
hgroup HtmlElement,
|
||||
/// The `<h1>` to `<h6>` HTML elements represent six levels of section headings. `<h1>` is the highest section level and `<h6>` is the lowest.
|
||||
h1 HtmlHeadingElement,
|
||||
/// The `<h1>` to `<h6>` HTML elements represent six levels of section headings. `<h1>` is the highest section level and `<h6>` is the lowest.
|
||||
|
||||
@@ -31,7 +31,7 @@ pub use hydration::{HydrationCtx, HydrationKey};
|
||||
pub use js_sys;
|
||||
use leptos_reactive::Scope;
|
||||
pub use logging::*;
|
||||
pub use macro_helpers::{IntoAttribute, IntoClass, IntoProperty};
|
||||
pub use macro_helpers::*;
|
||||
pub use node_ref::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
|
||||
@@ -78,7 +78,7 @@ pub fn render_to_stream_with_prefix(
|
||||
stream
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [Runtime] that were created, so
|
||||
/// 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`.
|
||||
///
|
||||
@@ -100,7 +100,7 @@ pub fn render_to_stream_with_prefix_undisposed(
|
||||
render_to_stream_with_prefix_undisposed_with_context(view, prefix, |_cx| {})
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [Runtime] that were created, so
|
||||
/// 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`.
|
||||
///
|
||||
|
||||
@@ -106,7 +106,7 @@ fn convert_from_snake_case(name: &Ident) -> Ident {
|
||||
if !name_str.is_case(Snake) {
|
||||
name.clone()
|
||||
} else {
|
||||
Ident::new(&*name_str.to_case(Pascal), name.span().clone())
|
||||
Ident::new(&name_str.to_case(Pascal), name.span())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ mod server;
|
||||
/// ```
|
||||
///
|
||||
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
|
||||
/// [NodeRef](leptos_reactive::NodeRef) to use later.
|
||||
/// [NodeRef](leptos_dom::NodeRef) to use later.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
@@ -315,10 +315,10 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// Annotates a function so that it can be used with your template as a Leptos `<Component/>`.
|
||||
///
|
||||
/// The `#[component]` macro allows you to annotate plain Rust functions as components
|
||||
/// and use them within your Leptos [view](mod@view) as if they were custom HTML elements. The
|
||||
/// and use them within your Leptos [view](crate::view!) as if they were custom HTML elements. The
|
||||
/// component function takes a [Scope](leptos_reactive::Scope) and any number of other arguments.
|
||||
/// When you use the component somewhere else, the names of its arguments are the names
|
||||
/// of the properties you use in the [view](mod@view) macro.
|
||||
/// of the properties you use in the [view](crate::view!) macro.
|
||||
///
|
||||
/// Every component function should have the return type `-> impl IntoView`.
|
||||
///
|
||||
@@ -373,20 +373,21 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// to do relatively expensive work within the component function, as it will only happen once,
|
||||
/// not on every state change.
|
||||
///
|
||||
/// 2. If a `snake_case` name is used, then the generated component's name will still be in
|
||||
/// `CamelCase`. This is how the renderer recognizes that a particular tag is a component, not
|
||||
/// an HTML element. It's important to be aware of this when using or importing the component.
|
||||
/// 2. Component names are usually in `PascalCase`. If you use a `snake_case` name,
|
||||
/// then the generated component's name will still be in `PascalCase`. This is how the framework
|
||||
/// recognizes that a particular tag is a component, not an HTML element. It's important to be aware
|
||||
/// of this when using or importing the component.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// // PascalCase: Generated component will be called MyComponent
|
||||
/// #[component]
|
||||
/// fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
|
||||
///
|
||||
/// // snake_case: Generated component will be called MySnakeCaseComponent
|
||||
/// #[component]
|
||||
/// fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
|
||||
///
|
||||
/// // CamelCase: Generated component will be called MyComponent
|
||||
/// #[component]
|
||||
/// fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// ```
|
||||
///
|
||||
/// 3. The macro generates a type `ComponentProps` for every `Component` (so, `HomePage` generates `HomePageProps`,
|
||||
@@ -553,11 +554,11 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
|
||||
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
|
||||
/// Defaults to `"Url"`. If you want to use this server function to power an
|
||||
/// [ActionForm](leptos_router::ActionForm) the encoding must be `"Url"`.
|
||||
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
|
||||
/// work without WebAssembly, the encoding must be `"Url"`.
|
||||
///
|
||||
/// The server function itself can take any number of arguments, each of which should be serializable
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos::Scope),
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope),
|
||||
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
|
||||
/// server-side context into the server function.
|
||||
///
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
let block = body.block;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
if #[cfg(all(not(feature = "stable"), debug_assertions))] {
|
||||
use proc_macro::Span;
|
||||
let span = Span::call_site();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -71,6 +71,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
|
||||
if let Pat::Ident(id) = &*arg.pat {
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
let #id = cx;
|
||||
}
|
||||
} else {
|
||||
@@ -90,7 +91,15 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
quote! { #typed_arg }
|
||||
let is_cx = fn_arg_is_cx(f);
|
||||
if is_cx {
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
#typed_arg
|
||||
}
|
||||
} else {
|
||||
quote! { #typed_arg }
|
||||
}
|
||||
});
|
||||
let fn_args_2 = fn_args.clone();
|
||||
|
||||
|
||||
@@ -8,16 +8,16 @@ repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Reactive system for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
slotmap = { version = "1", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde-lite = { version = "0.3", optional = true }
|
||||
futures = { version = "0.3" }
|
||||
js-sys = "0.3"
|
||||
lazy_static = "1"
|
||||
miniserde = { version = "0.1", optional = true }
|
||||
parking_lot = "0.12"
|
||||
serde-wasm-bindgen = "0.4"
|
||||
serde_json = "1"
|
||||
base64 = "0.21"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["rt"], optional = true }
|
||||
tracing = "0.1"
|
||||
|
||||
@@ -57,7 +57,7 @@ where
|
||||
let id = value.type_id();
|
||||
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut contexts = runtime.scope_contexts.borrow_mut();
|
||||
let mut contexts = runtime.scope_contexts.write();
|
||||
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
|
||||
context.insert(id, Box::new(value) as Box<dyn Any>);
|
||||
});
|
||||
@@ -116,7 +116,7 @@ where
|
||||
let id = TypeId::of::<T>();
|
||||
with_runtime(cx.runtime, |runtime| {
|
||||
let local_value = {
|
||||
let contexts = runtime.scope_contexts.borrow();
|
||||
let contexts = runtime.scope_contexts.read();
|
||||
let context = contexts.get(cx.id);
|
||||
context
|
||||
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
|
||||
@@ -124,16 +124,12 @@ where
|
||||
};
|
||||
match local_value {
|
||||
Some(val) => Some(val),
|
||||
None => runtime
|
||||
.scope_parents
|
||||
.borrow()
|
||||
.get(cx.id)
|
||||
.and_then(|parent| {
|
||||
use_context::<T>(Scope {
|
||||
runtime: cx.runtime,
|
||||
id: *parent,
|
||||
})
|
||||
}),
|
||||
None => runtime.scope_parents.read().get(cx.id).and_then(|parent| {
|
||||
use_context::<T>(Scope {
|
||||
runtime: cx.runtime,
|
||||
id: *parent,
|
||||
})
|
||||
}),
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::runtime::{with_runtime, RuntimeId};
|
||||
use crate::runtime::{with_runtime, LocalObserver, RuntimeId};
|
||||
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
|
||||
use cfg_if::cfg_if;
|
||||
use std::cell::RefCell;
|
||||
use parking_lot::RwLock;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Effects run a certain chunk of code whenever the signals they depend on change.
|
||||
@@ -23,7 +23,6 @@ use std::fmt::Debug;
|
||||
/// the server, use [create_isomorphic_effect].
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use log::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (a, set_a) = create_signal(cx, 0);
|
||||
/// let (b, set_b) = create_signal(cx, 0);
|
||||
@@ -31,7 +30,7 @@ use std::fmt::Debug;
|
||||
/// // ✅ use effects to interact between reactive state and the outside world
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // immediately prints "Value: 0" and subscribes to `a`
|
||||
/// log::debug!("Value: {}", a());
|
||||
/// println!("Value: {}", a());
|
||||
/// });
|
||||
///
|
||||
/// set_a(1);
|
||||
@@ -80,7 +79,6 @@ where
|
||||
/// the server as well as the client.
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use log::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (a, set_a) = create_signal(cx, 0);
|
||||
/// let (b, set_b) = create_signal(cx, 0);
|
||||
@@ -88,7 +86,7 @@ where
|
||||
/// // ✅ use effects to interact between reactive state and the outside world
|
||||
/// create_isomorphic_effect(cx, move |_| {
|
||||
/// // immediately prints "Value: 0" and subscribes to `a`
|
||||
/// log::debug!("Value: {}", a());
|
||||
/// println!("Value: {}", a());
|
||||
/// });
|
||||
///
|
||||
/// set_a(1);
|
||||
@@ -152,7 +150,7 @@ where
|
||||
F: Fn(Option<T>) -> T,
|
||||
{
|
||||
pub(crate) f: F,
|
||||
pub(crate) value: RefCell<Option<T>>,
|
||||
pub(crate) value: RwLock<Option<T>>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
@@ -179,22 +177,22 @@ where
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn run(&self, id: EffectId, runtime: RuntimeId) {
|
||||
_ = with_runtime(runtime, |runtime| {
|
||||
fn run(&self, id: EffectId, runtime_id: RuntimeId) {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
// clear previous dependencies
|
||||
id.cleanup(runtime);
|
||||
|
||||
// set this as the current observer
|
||||
let prev_observer = runtime.observer.take();
|
||||
runtime.observer.set(Some(id));
|
||||
let prev_observer = LocalObserver::take(runtime_id);
|
||||
LocalObserver::set(runtime_id, Some(id));
|
||||
|
||||
// run the effect
|
||||
let value = self.value.take();
|
||||
let value = std::mem::take(&mut *self.value.write());
|
||||
let new_value = (self.f)(value);
|
||||
*self.value.borrow_mut() = Some(new_value);
|
||||
*self.value.write() = Some(new_value);
|
||||
|
||||
// restore the previous observer
|
||||
runtime.observer.set(prev_observer);
|
||||
LocalObserver::set(runtime_id, prev_observer);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -203,7 +201,7 @@ impl EffectId {
|
||||
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
let effect = {
|
||||
let effects = runtime.effects.borrow();
|
||||
let effects = runtime.effects.read();
|
||||
effects.get(*self).cloned()
|
||||
};
|
||||
if let Some(effect) = effect {
|
||||
@@ -226,12 +224,12 @@ impl EffectId {
|
||||
)
|
||||
)]
|
||||
pub(crate) fn cleanup(&self, runtime: &Runtime) {
|
||||
let sources = runtime.effect_sources.borrow();
|
||||
let sources = runtime.effect_sources.read();
|
||||
if let Some(sources) = sources.get(*self) {
|
||||
let subs = runtime.signal_subscribers.borrow();
|
||||
for source in sources.borrow().iter() {
|
||||
let subs = runtime.signal_subscribers.read();
|
||||
for source in sources.read().iter() {
|
||||
if let Some(source) = subs.get(*source) {
|
||||
source.borrow_mut().remove(self);
|
||||
source.write().remove(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ macro_rules! debug_warn {
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
log::warn!($($x)*)
|
||||
tracing::warn!($($x)*)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{ }
|
||||
|
||||
@@ -33,7 +33,7 @@ use std::fmt::Debug;
|
||||
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // 🆗 run #1: calls `really_expensive_computation` the first time
|
||||
/// log::debug!("expensive = {}", expensive());
|
||||
/// println!("expensive = {}", expensive());
|
||||
/// });
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
|
||||
@@ -46,7 +46,7 @@ use std::fmt::Debug;
|
||||
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // 🆗 reads the current value of the memo
|
||||
/// log::debug!("memoized = {}", memoized());
|
||||
/// println!("memoized = {}", memoized());
|
||||
/// });
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // ✅ reads the current value **without re-running the calculation**
|
||||
@@ -103,7 +103,7 @@ where
|
||||
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // 🆗 run #1: calls `really_expensive_computation` the first time
|
||||
/// log::debug!("expensive = {}", expensive());
|
||||
/// println!("expensive = {}", expensive());
|
||||
/// });
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
|
||||
@@ -116,7 +116,7 @@ where
|
||||
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // 🆗 reads the current value of the memo
|
||||
/// log::debug!("memoized = {}", memoized());
|
||||
/// println!("memoized = {}", memoized());
|
||||
/// });
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // ✅ reads the current value **without re-running the calculation**
|
||||
|
||||
@@ -6,15 +6,10 @@ use crate::{
|
||||
spawn::spawn_local,
|
||||
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashSet,
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
any::Any, collections::HashSet, fmt::Debug, future::Future, marker::PhantomData, pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Creates [Resource](crate::Resource), which is a signal that reflects the
|
||||
@@ -113,10 +108,10 @@ where
|
||||
|
||||
let (loading, set_loading) = create_signal(cx, false);
|
||||
|
||||
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
|
||||
let fetcher = Arc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
|
||||
let source = create_memo(cx, move |_| source());
|
||||
|
||||
let r = Rc::new(ResourceState {
|
||||
let r = Arc::new(ResourceState {
|
||||
scope: cx,
|
||||
value,
|
||||
set_value,
|
||||
@@ -124,18 +119,18 @@ where
|
||||
set_loading,
|
||||
source,
|
||||
fetcher,
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
resolved: Arc::new(RwLock::new(resolved)),
|
||||
scheduled: Arc::new(RwLock::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
runtime.create_serializable_resource(Rc::clone(&r))
|
||||
runtime.create_serializable_resource(Arc::clone(&r))
|
||||
})
|
||||
.expect("tried to create a Resource in a Runtime that has been disposed.");
|
||||
|
||||
create_isomorphic_effect(cx, {
|
||||
let r = Rc::clone(&r);
|
||||
let r = Arc::clone(&r);
|
||||
move |_| {
|
||||
load_resource(cx, id, r.clone());
|
||||
}
|
||||
@@ -233,10 +228,10 @@ where
|
||||
|
||||
let (loading, set_loading) = create_signal(cx, false);
|
||||
|
||||
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
|
||||
let fetcher = Arc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
|
||||
let source = create_memo(cx, move |_| source());
|
||||
|
||||
let r = Rc::new(ResourceState {
|
||||
let r = Arc::new(ResourceState {
|
||||
scope: cx,
|
||||
value,
|
||||
set_value,
|
||||
@@ -244,18 +239,18 @@ where
|
||||
set_loading,
|
||||
source,
|
||||
fetcher,
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
resolved: Arc::new(RwLock::new(resolved)),
|
||||
scheduled: Arc::new(RwLock::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
runtime.create_unserializable_resource(Rc::clone(&r))
|
||||
runtime.create_unserializable_resource(Arc::clone(&r))
|
||||
})
|
||||
.expect("tried to create a Resource in a runtime that has been disposed.");
|
||||
|
||||
create_effect(cx, {
|
||||
let r = Rc::clone(&r);
|
||||
let r = Arc::clone(&r);
|
||||
// This is a local resource, so we're always going to handle it on the
|
||||
// client
|
||||
move |_| r.load(false)
|
||||
@@ -274,7 +269,7 @@ where
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Rc<ResourceState<S, T>>)
|
||||
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Arc<ResourceState<S, T>>)
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: 'static,
|
||||
@@ -283,7 +278,7 @@ where
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Rc<ResourceState<S, T>>)
|
||||
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Arc<ResourceState<S, T>>)
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
@@ -291,12 +286,12 @@ where
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut context = runtime.shared_context.borrow_mut();
|
||||
let mut context = runtime.shared_context.write();
|
||||
if let Some(data) = context.resolved_resources.remove(&id) {
|
||||
// The server already sent us the serialized resource value, so
|
||||
// deserialize & set it now
|
||||
context.pending_resources.remove(&id); // no longer pending
|
||||
r.resolved.set(true);
|
||||
*r.resolved.write() = true;
|
||||
|
||||
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
|
||||
|
||||
@@ -318,7 +313,7 @@ where
|
||||
move |res: String| {
|
||||
let res =
|
||||
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
|
||||
resolved.set(true);
|
||||
*resolved.write() = true;
|
||||
set_value.update(|n| *n = Some(res));
|
||||
set_loading.update(|n| *n = false);
|
||||
}
|
||||
@@ -551,10 +546,10 @@ where
|
||||
set_loading: WriteSignal<bool>,
|
||||
source: Memo<S>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
fetcher: Rc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
|
||||
resolved: Rc<Cell<bool>>,
|
||||
scheduled: Rc<Cell<bool>>,
|
||||
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
|
||||
fetcher: Arc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
|
||||
resolved: Arc<RwLock<bool>>,
|
||||
scheduled: Arc<RwLock<bool>>,
|
||||
suspense_contexts: Arc<RwLock<HashSet<SuspenseContext>>>,
|
||||
}
|
||||
|
||||
impl<S, T> ResourceState<S, T>
|
||||
@@ -583,7 +578,7 @@ where
|
||||
|
||||
let increment = move |_: Option<()>| {
|
||||
if let Some(s) = &suspense_cx {
|
||||
let mut contexts = suspense_contexts.borrow_mut();
|
||||
let mut contexts = suspense_contexts.write();
|
||||
if !contexts.contains(s) {
|
||||
contexts.insert(*s);
|
||||
|
||||
@@ -607,21 +602,22 @@ where
|
||||
|
||||
fn load(&self, refetching: bool) {
|
||||
// doesn't refetch if already refetching
|
||||
if refetching && self.scheduled.get() {
|
||||
let mut scheduled = self.scheduled.write();
|
||||
if refetching && *scheduled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.scheduled.set(false);
|
||||
*scheduled = false;
|
||||
|
||||
_ = self.source.try_with(|source| {
|
||||
let fut = (self.fetcher)(source.clone());
|
||||
|
||||
// `scheduled` is true for the rest of this code only
|
||||
self.scheduled.set(true);
|
||||
*self.scheduled.write() = true;
|
||||
queue_microtask({
|
||||
let scheduled = Rc::clone(&self.scheduled);
|
||||
let scheduled = Arc::clone(&self.scheduled);
|
||||
move || {
|
||||
scheduled.set(false);
|
||||
*scheduled.write() = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -630,7 +626,7 @@ where
|
||||
// increment counter everywhere it's read
|
||||
let suspense_contexts = self.suspense_contexts.clone();
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
for suspense_context in suspense_contexts.read().iter() {
|
||||
suspense_context.increment();
|
||||
}
|
||||
|
||||
@@ -642,12 +638,12 @@ where
|
||||
async move {
|
||||
let res = fut.await;
|
||||
|
||||
resolved.set(true);
|
||||
*resolved.write() = true;
|
||||
|
||||
set_value.update(|n| *n = Some(res));
|
||||
set_loading.update(|n| *n = false);
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
for suspense_context in suspense_contexts.read().iter() {
|
||||
suspense_context.decrement();
|
||||
}
|
||||
}
|
||||
@@ -686,8 +682,8 @@ where
|
||||
}
|
||||
|
||||
pub(crate) enum AnyResource {
|
||||
Unserializable(Rc<dyn UnserializableResource>),
|
||||
Serializable(Rc<dyn SerializableResource>),
|
||||
Unserializable(Arc<dyn UnserializableResource + Send + Sync>),
|
||||
Serializable(Arc<dyn SerializableResource + Send + Sync>),
|
||||
}
|
||||
|
||||
pub(crate) trait SerializableResource {
|
||||
|
||||
@@ -6,28 +6,30 @@ use crate::{
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
thread_local! {
|
||||
pub(crate) static RUNTIME: Runtime = Runtime::new();
|
||||
lazy_static! {
|
||||
pub(crate) static ref RUNTIME: Runtime = Runtime::new();
|
||||
}
|
||||
} else {
|
||||
thread_local! {
|
||||
pub(crate) static RUNTIMES: RefCell<SlotMap<RuntimeId, Runtime>> = Default::default();
|
||||
lazy_static! {
|
||||
pub(crate) static ref RUNTIMES: RwLock<SlotMap<RuntimeId, Runtime>> = Default::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,15 +41,13 @@ pub(crate) fn with_runtime<T>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> R
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
_ = id;
|
||||
Ok(RUNTIME.with(|runtime| f(runtime)))
|
||||
Ok(f(&RUNTIME))
|
||||
} else {
|
||||
RUNTIMES.with(|runtimes| {
|
||||
let runtimes = runtimes.borrow();
|
||||
match runtimes.get(id) {
|
||||
None => Err(()),
|
||||
Some(runtime) => Ok(f(runtime))
|
||||
}
|
||||
})
|
||||
let runtimes = RUNTIMES.read();
|
||||
match runtimes.get(id) {
|
||||
None => Err(()),
|
||||
Some(runtime) => Ok(f(runtime))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ pub fn create_runtime() -> RuntimeId {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
Default::default()
|
||||
} else {
|
||||
RUNTIMES.with(|runtimes| runtimes.borrow_mut().insert(Runtime::new()))
|
||||
RUNTIMES.with(|runtimes| runtimes.write().insert(Runtime::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ impl RuntimeId {
|
||||
pub fn dispose(self) {
|
||||
cfg_if! {
|
||||
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
|
||||
let runtime = RUNTIMES.with(move |runtimes| runtimes.borrow_mut().remove(self));
|
||||
let runtime = RUNTIMES.with(move |runtimes| runtimes.write().remove(self));
|
||||
drop(runtime);
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ impl RuntimeId {
|
||||
|
||||
pub(crate) fn raw_scope_and_disposer(self) -> (Scope, ScopeDisposer) {
|
||||
with_runtime(self, |runtime| {
|
||||
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
|
||||
let id = { runtime.scopes.write().insert(Default::default()) };
|
||||
let scope = Scope { runtime: self, id };
|
||||
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
|
||||
(scope, disposer)
|
||||
@@ -97,9 +97,9 @@ impl RuntimeId {
|
||||
parent: Option<Scope>,
|
||||
) -> (T, ScopeId, ScopeDisposer) {
|
||||
with_runtime(self, |runtime| {
|
||||
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
|
||||
let id = { runtime.scopes.write().insert(Default::default()) };
|
||||
if let Some(parent) = parent {
|
||||
runtime.scope_parents.borrow_mut().insert(id, parent.id);
|
||||
runtime.scope_parents.write().insert(id, parent.id);
|
||||
}
|
||||
let scope = Scope { runtime: self, id };
|
||||
let val = f(scope);
|
||||
@@ -121,10 +121,7 @@ impl RuntimeId {
|
||||
T: Any + 'static,
|
||||
{
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime
|
||||
.signals
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
runtime.signals.write().insert(Arc::new(RwLock::new(value)))
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed");
|
||||
(
|
||||
@@ -150,10 +147,7 @@ impl RuntimeId {
|
||||
T: Any + 'static,
|
||||
{
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime
|
||||
.signals
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
runtime.signals.write().insert(Arc::new(RwLock::new(value)))
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed");
|
||||
RwSignal {
|
||||
@@ -176,11 +170,11 @@ impl RuntimeId {
|
||||
with_runtime(self, |runtime| {
|
||||
let effect = Effect {
|
||||
f,
|
||||
value: RefCell::new(None),
|
||||
value: RwLock::new(None),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
};
|
||||
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
|
||||
let id = { runtime.effects.write().insert(Arc::new(effect)) };
|
||||
id.run::<T>(self);
|
||||
id
|
||||
})
|
||||
@@ -219,33 +213,63 @@ impl RuntimeId {
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Runtime {
|
||||
pub shared_context: RefCell<SharedContext>,
|
||||
pub observer: Cell<Option<EffectId>>,
|
||||
pub scopes: RefCell<SlotMap<ScopeId, RefCell<Vec<ScopeProperty>>>>,
|
||||
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
|
||||
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
|
||||
pub shared_context: RwLock<SharedContext>,
|
||||
pub scopes: RwLock<SlotMap<ScopeId, RwLock<Vec<ScopeProperty>>>>,
|
||||
pub scope_parents: RwLock<SparseSecondaryMap<ScopeId, ScopeId>>,
|
||||
pub scope_children: RwLock<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_contexts: RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
|
||||
pub scope_contexts: RwLock<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_cleanups: RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
|
||||
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
|
||||
pub signal_subscribers: RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
|
||||
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
|
||||
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
|
||||
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
|
||||
pub scope_cleanups: RwLock<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
|
||||
pub signals: RwLock<SlotMap<SignalId, Arc<RwLock<dyn Any>>>>,
|
||||
pub signal_subscribers: RwLock<SecondaryMap<SignalId, RwLock<HashSet<EffectId>>>>,
|
||||
pub effects: RwLock<SlotMap<EffectId, Arc<dyn AnyEffect>>>,
|
||||
pub effect_sources: RwLock<SecondaryMap<EffectId, RwLock<HashSet<SignalId>>>>,
|
||||
pub resources: RwLock<SlotMap<ResourceId, AnyResource>>,
|
||||
}
|
||||
|
||||
// track current observer thread-locally
|
||||
// because effects run synchronously, the current observer
|
||||
// *in this thread* will not change during the execution of an effect.
|
||||
// but if we track this across threads, it's possible for overlapping
|
||||
// executions to cause the stack to be out of order
|
||||
// so we store at most one current observer per runtime, per thread
|
||||
thread_local! {
|
||||
static OBSERVER: RefCell<HashMap<RuntimeId, EffectId>> = Default::default();
|
||||
}
|
||||
|
||||
pub(crate) struct LocalObserver {}
|
||||
|
||||
impl LocalObserver {
|
||||
pub fn take(runtime: RuntimeId) -> Option<EffectId> {
|
||||
OBSERVER.with(|observer| observer.borrow_mut().remove(&runtime))
|
||||
}
|
||||
|
||||
pub fn get(runtime: RuntimeId) -> Option<EffectId> {
|
||||
OBSERVER.with(|observer| observer.borrow().get(&runtime).copied())
|
||||
}
|
||||
|
||||
pub fn set(runtime: RuntimeId, effect: Option<EffectId>) {
|
||||
OBSERVER.with(|observer| {
|
||||
if let Some(value) = effect {
|
||||
observer.borrow_mut().insert(runtime, value)
|
||||
} else {
|
||||
observer.borrow_mut().remove(&runtime)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Runtime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Runtime")
|
||||
.field("shared_context", &self.shared_context)
|
||||
.field("observer", &self.observer)
|
||||
.field("scopes", &self.scopes)
|
||||
.field("scope_parents", &self.scope_parents)
|
||||
.field("scope_children", &self.scope_children)
|
||||
.field("signals", &self.signals)
|
||||
.field("signal_subscribers", &self.signal_subscribers)
|
||||
.field("effects", &self.effects.borrow().len())
|
||||
.field("effects", &self.effects.read().len())
|
||||
.field("effect_sources", &self.effect_sources)
|
||||
.finish()
|
||||
}
|
||||
@@ -258,27 +282,27 @@ impl Runtime {
|
||||
|
||||
pub(crate) fn create_unserializable_resource<S, T>(
|
||||
&self,
|
||||
state: Rc<ResourceState<S, T>>,
|
||||
state: Arc<ResourceState<S, T>>,
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
.write()
|
||||
.insert(AnyResource::Unserializable(state))
|
||||
}
|
||||
|
||||
pub(crate) fn create_serializable_resource<S, T>(
|
||||
&self,
|
||||
state: Rc<ResourceState<S, T>>,
|
||||
state: Arc<ResourceState<S, T>>,
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
{
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
.write()
|
||||
.insert(AnyResource::Serializable(state))
|
||||
}
|
||||
|
||||
@@ -291,7 +315,7 @@ impl Runtime {
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let resources = self.resources.borrow();
|
||||
let resources = self.resources.read();
|
||||
let res = resources.get(id);
|
||||
if let Some(res) = res {
|
||||
let res_state = match res {
|
||||
@@ -317,7 +341,7 @@ impl Runtime {
|
||||
/// Returns IDs for all [Resource]s found on any scope.
|
||||
pub(crate) fn all_resources(&self) -> Vec<ResourceId> {
|
||||
self.resources
|
||||
.borrow()
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(resource_id, _)| resource_id)
|
||||
.collect()
|
||||
@@ -326,7 +350,7 @@ impl Runtime {
|
||||
/// Returns IDs for all [Resource]s found on any scope, pending from the server.
|
||||
pub(crate) fn pending_resources(&self) -> Vec<ResourceId> {
|
||||
self.resources
|
||||
.borrow()
|
||||
.read()
|
||||
.iter()
|
||||
.filter_map(|(resource_id, res)| {
|
||||
if matches!(res, AnyResource::Serializable(_)) {
|
||||
@@ -342,7 +366,7 @@ impl Runtime {
|
||||
&self,
|
||||
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
let f = FuturesUnordered::new();
|
||||
for (id, resource) in self.resources.borrow().iter() {
|
||||
for (id, resource) in self.resources.read().iter() {
|
||||
if let AnyResource::Serializable(resource) = resource {
|
||||
f.push(resource.to_serialization_resolver(id));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
runtime::{with_runtime, LocalObserver, RuntimeId},
|
||||
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
|
||||
};
|
||||
use futures::stream::FuturesUnordered;
|
||||
@@ -108,7 +108,7 @@ impl Scope {
|
||||
pub fn run_child_scope<T>(self, f: impl FnOnce(Scope) -> T) -> (T, ScopeDisposer) {
|
||||
let (res, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let mut children = runtime.scope_children.borrow_mut();
|
||||
let mut children = runtime.scope_children.write();
|
||||
children
|
||||
.entry(self.id)
|
||||
.expect("trying to add a child to a Scope that has already been disposed")
|
||||
@@ -145,9 +145,9 @@ impl Scope {
|
||||
/// ```
|
||||
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let prev_observer = runtime.observer.take();
|
||||
let prev_observer = LocalObserver::take(self.runtime);
|
||||
let untracked_result = f();
|
||||
runtime.observer.set(prev_observer);
|
||||
LocalObserver::set(self.runtime, prev_observer);
|
||||
untracked_result
|
||||
})
|
||||
.expect("tried to run untracked function in a runtime that has been disposed")
|
||||
@@ -167,7 +167,7 @@ impl Scope {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
// dispose of all child scopes
|
||||
let children = {
|
||||
let mut children = runtime.scope_children.borrow_mut();
|
||||
let mut children = runtime.scope_children.write();
|
||||
children.remove(self.id)
|
||||
};
|
||||
|
||||
@@ -182,7 +182,7 @@ impl Scope {
|
||||
}
|
||||
|
||||
// run cleanups
|
||||
if let Some(cleanups) = runtime.scope_cleanups.borrow_mut().remove(self.id) {
|
||||
if let Some(cleanups) = runtime.scope_cleanups.write().remove(self.id) {
|
||||
for cleanup in cleanups {
|
||||
cleanup();
|
||||
}
|
||||
@@ -190,34 +190,34 @@ impl Scope {
|
||||
|
||||
// remove everything we own and run cleanups
|
||||
let owned = {
|
||||
let owned = runtime.scopes.borrow_mut().remove(self.id);
|
||||
owned.map(|owned| owned.take())
|
||||
let owned = runtime.scopes.write().remove(self.id);
|
||||
owned.map(|owned| std::mem::take(&mut *owned.write()))
|
||||
};
|
||||
if let Some(owned) = owned {
|
||||
for property in owned {
|
||||
match property {
|
||||
ScopeProperty::Signal(id) => {
|
||||
// remove the signal
|
||||
runtime.signals.borrow_mut().remove(id);
|
||||
let subs = runtime.signal_subscribers.borrow_mut().remove(id);
|
||||
runtime.signals.write().remove(id);
|
||||
let subs = runtime.signal_subscribers.write().remove(id);
|
||||
|
||||
// each of the subs needs to remove the signal from its dependencies
|
||||
// so that it doesn't try to read the (now disposed) signal
|
||||
if let Some(subs) = subs {
|
||||
let source_map = runtime.effect_sources.borrow();
|
||||
for effect in subs.borrow().iter() {
|
||||
let source_map = runtime.effect_sources.read();
|
||||
for effect in subs.read().iter() {
|
||||
if let Some(effect_sources) = source_map.get(*effect) {
|
||||
effect_sources.borrow_mut().remove(&id);
|
||||
effect_sources.write().remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ScopeProperty::Effect(id) => {
|
||||
runtime.effects.borrow_mut().remove(id);
|
||||
runtime.effect_sources.borrow_mut().remove(id);
|
||||
runtime.effects.write().remove(id);
|
||||
runtime.effect_sources.write().remove(id);
|
||||
}
|
||||
ScopeProperty::Resource(id) => {
|
||||
runtime.resources.borrow_mut().remove(id);
|
||||
runtime.resources.write().remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,11 +227,24 @@ impl Scope {
|
||||
|
||||
pub(crate) fn with_scope_property(&self, f: impl FnOnce(&mut Vec<ScopeProperty>)) {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let scopes = runtime.scopes.borrow();
|
||||
let scopes = runtime.scopes.read();
|
||||
let scope = scopes
|
||||
.get(self.id)
|
||||
.expect("tried to add property to a scope that has been disposed");
|
||||
f(&mut scope.borrow_mut());
|
||||
f(&mut scope.write());
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the the parent Scope, if any.
|
||||
pub fn parent(&self) -> Option<Scope> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.scope_parents.read().get(self.id).copied()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|id| Scope {
|
||||
runtime: self.runtime,
|
||||
id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -242,7 +255,7 @@ impl Scope {
|
||||
/// are invalidated.
|
||||
pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut cleanups = runtime.scope_cleanups.borrow_mut();
|
||||
let mut cleanups = runtime.scope_cleanups.write();
|
||||
let cleanups = cleanups
|
||||
.entry(cx.id)
|
||||
.expect("trying to clean up a Scope that has already been disposed")
|
||||
@@ -314,7 +327,7 @@ impl Scope {
|
||||
use futures::StreamExt;
|
||||
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
let mut shared_context = runtime.shared_context.write();
|
||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
create_isomorphic_effect(*self, move |_| {
|
||||
@@ -337,12 +350,12 @@ impl Scope {
|
||||
})
|
||||
}
|
||||
|
||||
/// The set of all HTML fragments current pending, by their keys (see [Self::current_fragment_key]).
|
||||
/// The set of all HTML fragments currently pending.
|
||||
/// Returns a tuple of the hydration ID of the previous element, and a pinned `Future` that will yield the
|
||||
/// `<Suspense/>` HTML when all resources are resolved.
|
||||
pub fn pending_fragments(&self) -> HashMap<String, (String, PinnedFuture<String>)> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
let mut shared_context = runtime.shared_context.write();
|
||||
std::mem::take(&mut shared_context.pending_fragments)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
debug_warn,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
runtime::{with_runtime, LocalObserver, RuntimeId},
|
||||
Runtime, Scope, ScopeProperty, UntrackedGettableSignal, UntrackedSettableSignal,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
@@ -246,7 +246,9 @@ where
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) fn subscribe(&self) {
|
||||
_ = with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
self.id.subscribe(self.runtime, runtime)
|
||||
})
|
||||
}
|
||||
|
||||
/// Clones and returns the current value of the signal, and subscribes
|
||||
@@ -286,14 +288,17 @@ where
|
||||
/// Applies the function to the current Signal, if it exists, and subscribes
|
||||
/// the running effect.
|
||||
pub(crate) fn try_with<U>(&self, f: impl FnOnce(&T) -> U) -> Result<U, SignalError> {
|
||||
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f)) {
|
||||
match with_runtime(self.runtime, |runtime| {
|
||||
self.id.try_with(self.runtime, runtime, f)
|
||||
}) {
|
||||
Ok(Ok(v)) => Ok(v),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(SignalError::RuntimeDisposed),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a [Stream] that emits the new value of the signal whenever it changes.
|
||||
/// Generates a [Stream](futures::stream::Stream) that emits the new value of the signal
|
||||
/// whenever it changes.
|
||||
pub fn to_stream(&self) -> impl Stream<Item = T>
|
||||
where
|
||||
T: Clone,
|
||||
@@ -1096,7 +1101,8 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Generates a [Stream] that emits the new value of the signal whenever it changes.
|
||||
/// Generates a [Stream](futures::stream::Stream) that emits the new value of the signal
|
||||
/// whenever it changes.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -1167,20 +1173,20 @@ pub(crate) enum SignalError {
|
||||
}
|
||||
|
||||
impl SignalId {
|
||||
pub(crate) fn subscribe(&self, runtime: &Runtime) {
|
||||
pub(crate) fn subscribe(&self, runtime_id: RuntimeId, runtime: &Runtime) {
|
||||
// add subscriber
|
||||
if let Some(observer) = runtime.observer.get() {
|
||||
if let Some(observer) = LocalObserver::get(runtime_id) {
|
||||
// add this observer to the signal's dependencies (to allow notification)
|
||||
let mut subs = runtime.signal_subscribers.borrow_mut();
|
||||
let mut subs = runtime.signal_subscribers.write();
|
||||
if let Some(subs) = subs.entry(*self) {
|
||||
subs.or_default().borrow_mut().insert(observer);
|
||||
subs.or_default().write().insert(observer);
|
||||
}
|
||||
|
||||
// add this signal to the effect's sources (to allow cleanup)
|
||||
let mut effect_sources = runtime.effect_sources.borrow_mut();
|
||||
let mut effect_sources = runtime.effect_sources.write();
|
||||
if let Some(effect_sources) = effect_sources.entry(observer) {
|
||||
let sources = effect_sources.or_default();
|
||||
sources.borrow_mut().insert(*self);
|
||||
sources.write().insert(*self);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1195,7 +1201,7 @@ impl SignalId {
|
||||
{
|
||||
// get the value
|
||||
let value = {
|
||||
let signals = runtime.signals.borrow();
|
||||
let signals = runtime.signals.read();
|
||||
match signals.get(*self).cloned().ok_or(SignalError::Disposed) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => {
|
||||
@@ -1204,12 +1210,13 @@ impl SignalId {
|
||||
}
|
||||
}
|
||||
}?;
|
||||
let value = value.try_borrow().unwrap_or_else(|e| {
|
||||
debug_warn!(
|
||||
"Signal::try_with_no_subscription failed on Signal<{}>. It seems you're trying to read the value of a signal within an effect caused by updating the signal.",
|
||||
let value = value.try_read().unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Signal::try_with_no_subscription failed on Signal<{}>. \
|
||||
It seems you're trying to read the value of a signal within an effect \
|
||||
caused by updating the signal.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
panic!("{e}");
|
||||
});
|
||||
let value = value
|
||||
.downcast_ref::<T>()
|
||||
@@ -1219,13 +1226,14 @@ impl SignalId {
|
||||
|
||||
pub(crate) fn try_with<T, U>(
|
||||
&self,
|
||||
runtime_id: RuntimeId,
|
||||
runtime: &Runtime,
|
||||
f: impl FnOnce(&T) -> U,
|
||||
) -> Result<U, SignalError>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.subscribe(runtime);
|
||||
self.subscribe(runtime_id, runtime);
|
||||
|
||||
self.try_with_no_subscription(runtime, f)
|
||||
}
|
||||
@@ -1244,12 +1252,14 @@ impl SignalId {
|
||||
.expect("tried to access a signal in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
pub(crate) fn with<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&T) -> U) -> U
|
||||
pub(crate) fn with<T, U>(&self, runtime_id: RuntimeId, f: impl FnOnce(&T) -> U) -> U
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
with_runtime(runtime, |runtime| self.try_with(runtime, f).unwrap())
|
||||
.expect("tried to access a signal in a runtime that has been disposed")
|
||||
with_runtime(runtime_id, |runtime| {
|
||||
self.try_with(runtime_id, runtime, f).unwrap()
|
||||
})
|
||||
.expect("tried to access a signal in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
fn update_value<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T) -> U) -> Option<U>
|
||||
@@ -1258,11 +1268,11 @@ impl SignalId {
|
||||
{
|
||||
with_runtime(runtime, |runtime| {
|
||||
let value = {
|
||||
let signals = runtime.signals.borrow();
|
||||
let signals = runtime.signals.read();
|
||||
signals.get(*self).cloned()
|
||||
};
|
||||
if let Some(value) = value {
|
||||
let mut value = value.borrow_mut();
|
||||
let mut value = value.write();
|
||||
if let Some(value) = value.downcast_mut::<T>() {
|
||||
Some(f(value))
|
||||
} else {
|
||||
@@ -1298,14 +1308,14 @@ impl SignalId {
|
||||
// notify subscribers
|
||||
if updated.is_some() {
|
||||
let subs = {
|
||||
let subs = runtime.signal_subscribers.borrow();
|
||||
let subs = runtime.signal_subscribers.read();
|
||||
let subs = subs.get(*self);
|
||||
subs.map(|subs| subs.borrow().clone())
|
||||
subs.map(|subs| subs.read().clone())
|
||||
};
|
||||
if let Some(subs) = subs {
|
||||
for sub in subs {
|
||||
let effect = {
|
||||
let effects = runtime.effects.borrow();
|
||||
let effects = runtime.effects.read();
|
||||
effects.get(sub).cloned()
|
||||
};
|
||||
if let Some(effect) = effect {
|
||||
|
||||
@@ -574,6 +574,12 @@ impl<T> From<Memo<T>> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Signal<T>> for MaybeSignal<T> {
|
||||
fn from(value: Signal<T>) -> Self {
|
||||
Self::Dynamic(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnOnce<()> for MaybeSignal<T>
|
||||
where
|
||||
|
||||
@@ -19,7 +19,7 @@ fn untracked_set_doesnt_trigger_effect() {
|
||||
create_isomorphic_effect(cx, {
|
||||
let b = b.clone();
|
||||
move |_| {
|
||||
let formatted = format!("Value is {}", a());
|
||||
let formatted = format!("Value is {}", a.get());
|
||||
*b.borrow_mut() = formatted;
|
||||
}
|
||||
});
|
||||
@@ -52,7 +52,7 @@ fn untracked_get_doesnt_trigger_effect() {
|
||||
create_isomorphic_effect(cx, {
|
||||
let b = b.clone();
|
||||
move |_| {
|
||||
let formatted = format!("Values are {} and {}", a(), a2.get_untracked());
|
||||
let formatted = format!("Values are {} and {}", a.get(), a2.get_untracked());
|
||||
*b.borrow_mut() = formatted;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//!
|
||||
//! ### `#[server]`
|
||||
//!
|
||||
//! The [`#[server]` macro](leptos::leptos_macro::server) allows you to annotate a function to
|
||||
//! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to
|
||||
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
|
||||
//! crate that is enabled).
|
||||
//!
|
||||
@@ -326,9 +326,19 @@ where
|
||||
let mut write = REGISTERED_SERVER_FUNCTIONS
|
||||
.write()
|
||||
.map_err(|e| ServerFnError::Registration(e.to_string()))?;
|
||||
write.insert(Self::url(), run_server_fn);
|
||||
let prev = write.insert(Self::url(), run_server_fn);
|
||||
|
||||
Ok(())
|
||||
// if there was already a server function with this key,
|
||||
// return Err
|
||||
match prev {
|
||||
Some(_) => Err(ServerFnError::Registration(format!(
|
||||
"There was already a server function registered at {:?}. \
|
||||
This can happen if you use the same server function name in two different modules
|
||||
on `stable` or in `release` mode.",
|
||||
Self::url()
|
||||
))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,16 @@
|
||||
//! }
|
||||
//!
|
||||
//! }
|
||||
//!
|
||||
//! ```
|
||||
//! # Feature Flags
|
||||
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
|
||||
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
|
||||
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
|
||||
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
|
||||
//! of calling signals as functions. Enable this feature to support `stable` Rust.
|
||||
//!
|
||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||
//! which mode your app is operating in.
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use std::{
|
||||
|
||||
@@ -20,7 +20,7 @@ log = "0.4"
|
||||
regex = { version = "1", optional = true }
|
||||
bincode = "1"
|
||||
url = { version = "2", optional = true }
|
||||
urlencoding = "2"
|
||||
percent-encoding = "2"
|
||||
thiserror = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
serde = "1"
|
||||
|
||||
@@ -11,7 +11,7 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
|
||||
let (outlet, set_outlet) = create_signal(cx, None::<View>);
|
||||
create_isomorphic_effect(cx, move |_| {
|
||||
match (route.child(), &is_showing.get()) {
|
||||
match (route.child(cx), &is_showing.get()) {
|
||||
(None, prev) => {
|
||||
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
|
||||
prev_scope.dispose();
|
||||
|
||||
@@ -79,7 +79,7 @@ impl RouteContext {
|
||||
pub(crate) fn new(
|
||||
cx: Scope,
|
||||
router: &RouterContext,
|
||||
child: impl Fn() -> Option<RouteContext> + 'static,
|
||||
child: impl Fn(Scope) -> Option<RouteContext> + 'static,
|
||||
matcher: impl Fn() -> Option<RouteMatch> + 'static,
|
||||
) -> Option<Self> {
|
||||
let base = router.base();
|
||||
@@ -151,7 +151,7 @@ impl RouteContext {
|
||||
cx,
|
||||
id: 0,
|
||||
base_path: path.to_string(),
|
||||
child: Box::new(|| None),
|
||||
child: Box::new(|_| None),
|
||||
path: RefCell::new(path.to_string()),
|
||||
original_path: path.to_string(),
|
||||
params: create_memo(cx, |_| ParamsMap::new()),
|
||||
@@ -166,8 +166,8 @@ impl RouteContext {
|
||||
}
|
||||
|
||||
/// The nested child route, if any.
|
||||
pub fn child(&self) -> Option<RouteContext> {
|
||||
(self.inner.child)()
|
||||
pub fn child(&self, cx: Scope) -> Option<RouteContext> {
|
||||
(self.inner.child)(cx)
|
||||
}
|
||||
|
||||
/// The view associated with the current route.
|
||||
@@ -180,7 +180,7 @@ pub(crate) struct RouteContextInner {
|
||||
cx: Scope,
|
||||
base_path: String,
|
||||
pub(crate) id: usize,
|
||||
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
|
||||
pub(crate) child: Box<dyn Fn(Scope) -> Option<RouteContext>>,
|
||||
pub(crate) path: RefCell<String>,
|
||||
pub(crate) original_path: String,
|
||||
pub(crate) params: Memo<ParamsMap>,
|
||||
@@ -202,7 +202,6 @@ impl std::fmt::Debug for RouteContextInner {
|
||||
f.debug_struct("RouteContextInner")
|
||||
.field("path", &self.path)
|
||||
.field("ParamsMap", &self.params)
|
||||
.field("child", &(self.child)())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,8 @@ pub fn Routes(
|
||||
#[prop(optional)] base: Option<String>,
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>(cx).unwrap_or_else(|| {
|
||||
log::warn!("<Routes/> component should be nested within a <Router/>.");
|
||||
panic!()
|
||||
});
|
||||
let router = use_context::<RouterContext>(cx)
|
||||
.expect("<Routes/> component should be nested within a <Router/>.");
|
||||
let base_route = router.base();
|
||||
|
||||
let mut branches = Vec::new();
|
||||
@@ -86,6 +84,8 @@ pub fn Routes(
|
||||
.map(|prev_matches| next_matches.len() == prev_matches.len())
|
||||
.unwrap_or(false);
|
||||
|
||||
let prev_cx = Rc::new(Cell::new(cx));
|
||||
|
||||
for i in 0..next_matches.len() {
|
||||
let next = next.clone();
|
||||
let prev_match = prev_matches.and_then(|p| p.get(i));
|
||||
@@ -112,17 +112,19 @@ pub fn Routes(
|
||||
root_equal.set(false);
|
||||
}
|
||||
|
||||
let disposer = cx.child_scope({
|
||||
let disposer = prev_cx.get().child_scope({
|
||||
let next = next.clone();
|
||||
let router = Rc::clone(&router.inner);
|
||||
let prev_cx = Rc::clone(&prev_cx);
|
||||
move |cx| {
|
||||
prev_cx.set(cx);
|
||||
let next = next.clone();
|
||||
let next_ctx = RouteContext::new(
|
||||
cx,
|
||||
&RouterContext { inner: router },
|
||||
{
|
||||
let next = next.clone();
|
||||
move || {
|
||||
move |cx| {
|
||||
if let Some(route_states) =
|
||||
use_context::<Memo<RouterState>>(cx)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn unescape(s: &str) -> String {
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn escape(s: &str) -> String {
|
||||
urlencoding::encode(s).into()
|
||||
percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC).to_string()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
|
||||
@@ -180,6 +180,16 @@
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
|
||||
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
|
||||
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
|
||||
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
|
||||
//! of calling signals as functions. Enable this feature to support `stable` Rust.
|
||||
//!
|
||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||
//! which mode your app is operating in.
|
||||
|
||||
#![cfg_attr(not(feature = "stable"), feature(auto_traits))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
|
||||
|
||||
Reference in New Issue
Block a user