Compare commits

..

29 Commits

Author SHA1 Message Date
Greg Johnston
f0f7d900a1 revert PR #538 2023-02-18 17:27:00 -05:00
Greg Johnston
59ad6a4725 revert accident 2023-02-18 16:03:20 -05:00
Greg Johnston
884dacbc6c fix example 2023-02-18 16:02:19 -05:00
Dmitrii Kuzmin
9c572f7617 fix(examples): hackernews_axum styles href (#536) 2023-02-18 15:17:54 -05:00
jquesada2016
487dba90d8 fix: off-by-one error in <For/> (closes #533) (#538) 2023-02-18 14:57:14 -05:00
Greg Johnston
20f24d2f3a fix: building leptos_reactive in release mode (#540) 2023-02-18 12:45:58 -05:00
Greg Johnston
20cbc240ee v0.2.0-alpha2 (#539) 2023-02-18 12:45:46 -05:00
jquesada2016
f2f52b2533 change: move signal method implementations into traits in signal prelude (#490) 2023-02-18 07:30:03 -05:00
Sean Aye
46d6e3f78c fix compile of leptos dom (#535) 2023-02-17 18:25:58 -05:00
Greg Johnston
586f524015 feature: in-order streaming and async rendering (#496) 2023-02-17 17:31:32 -05:00
Greg Johnston
79781ec20c Fix test import location 2023-02-17 16:18:22 -05:00
Greg Johnston
91f6d9a404 What's in a name? 2023-02-17 07:25:42 -05:00
Greg Johnston
76a74ecde2 fix: hydration IDs for elements following <Suspense/> (closes #527) (#531) 2023-02-16 21:12:52 -05:00
Greg Johnston
0071a48b8a feature: reintroduce limited template-node cloning w/ template macro (#526) 2023-02-16 07:02:01 -05:00
Greg Johnston
8d42e91eb8 fix: top-level SVG in view macro with new exports (#525) 2023-02-15 15:38:06 -05:00
Greg Johnston
00a796d204 change: tweak API of Errors and implement IntoIter (#522) 2023-02-15 14:03:16 -05:00
henrik
bde585dc3e feature: enable cargo-leptos to reload multiple CSS files (#524) 2023-02-14 18:51:47 -05:00
Greg Johnston
0a534bd7fd Reexport web-sys event types in leptos::ev to make it easier to type handlers (#521) 2023-02-13 20:45:46 -05:00
Greg Johnston
50d8eae694 fix: correct namespace for Unit in empty views (closes #518) (#520) 2023-02-13 20:25:26 -05:00
martin frances
e732a4952b leptos_dom erros.rs remove<E>() does not need to be generic. (#516)
* leptos_dom erros.rs remove<E>() does not need to be generic.

* fixed up errors.remove().
2023-02-13 20:25:11 -05:00
Greg Johnston
8a99623fd6 0.2.0-alpha (#515) 2023-02-13 07:49:29 -05:00
Greg Johnston
7d6c4930e4 remove .unwrap() from redirect in Actix integration (#514) 2023-02-13 06:02:43 -05:00
IcosaHedron
81d6689cc0 do not unwrap use_context in integrations axum redirect (#513) 2023-02-12 21:59:12 -05:00
Greg Johnston
989b5b93c3 CI: fix Wasm testing (#511) 2023-02-12 19:39:32 -05:00
Greg Johnston
ca510f72c1 fix: SSR export in Wasm mode (#512) 2023-02-12 19:12:15 -05:00
Greg Johnston
6dd3be75d1 fix: import in leptos_dom and add Wasm build to CI for regressions (#510) 2023-02-12 18:58:57 -05:00
g-re-g
51e11e756a Typos and a small cleanup (#509) 2023-02-12 18:11:31 -05:00
Greg Johnston
1dbcfe2861 change: reorganize module exports and reexports (#503) 2023-02-12 17:04:36 -05:00
Greg Johnston
db3f46c501 Add docs on testing (closes #489) (#508) 2023-02-12 17:03:12 -05:00
113 changed files with 5223 additions and 3034 deletions

View File

@@ -30,6 +30,9 @@ jobs:
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
@@ -43,4 +46,3 @@ jobs:
- name: Run tests with all features
run: cargo make ci

View File

@@ -11,6 +11,7 @@ members = [
# integrations
"integrations/actix",
"integrations/axum",
"integrations/utils",
# libraries
"meta",
@@ -19,17 +20,18 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.1.3"
version = "0.2.0-alpha2"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.1.3" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.3" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.3" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.3" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.3" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.3" }
leptos_router = { path = "./router", version = "0.1.3" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.3" }
leptos = { path = "./leptos", default-features = false, version = "0.2.0-alpha2" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.0-alpha2" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.0-alpha2" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.0-alpha2" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.0-alpha2" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.0-alpha2" }
leptos_router = { path = "./router", version = "0.2.0-alpha2" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.0-alpha2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.0-alpha2" }
[profile.release]
codegen-units = 1

View File

@@ -21,11 +21,7 @@ install_crate = "cargo-all-features"
[tasks.build-wasm]
clear = true
dependencies = [
{ name = "build-wasm", path = "leptos_reactive" },
{ name = "build-wasm", path = "leptos_dom" },
{ name = "build-wasm", path = "leptos_server" },
]
dependencies = [{ name = "build-wasm", path = "leptos" }]
[tasks.check-examples]
clear = true

View File

@@ -99,6 +99,10 @@ Open browser on [http://localhost:3000/](http://localhost:3000/)
## FAQs
### Whats up with the name?
*Leptos* (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
### Is it production ready?
People usually mean one of three things by this question.

View File

@@ -12,8 +12,8 @@
- [Error Handling](./view/07_errors.md)
- [Parent-Child Communication](./view/08_parent_child.md)
- [Passing Children to Components](./view/09_component_children.md)
- [Interlude: Reactivity and Functions](interlude_functions.md)
- [Testing]()
- [Interlude: Reactivity and Functions](./interlude_functions.md)
- [Testing](./testing.md)
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [Async]()
- [Resource]()

180
docs/book/src/testing.md Normal file
View File

@@ -0,0 +1,180 @@
# Testing Your Components
Testing user interfaces can be relatively tricky, but really important. This article
will discuss a couple principles and approaches for testing a Leptos app.
## 1. Test business logic with ordinary Rust tests
In many cases, it makes sense to pull the logic out of your components and test
it separately. For some simple components, theres no particular logic to test, but
for many its worth using a testable wrapping type and implementing the logic in
ordinary Rust `impl` blocks.
For example, instead of embedding logic in a component directly like this:
```rust
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
// ⚠️ this is hard to test because it's embedded in the component
let maximum = move || todos.with(|todos| {
todos.iter().filter(|todo| todo.completed).sum()
});
}
```
You could pull that logic out into a separate data structure and test it:
```rust
pub struct Todos(Vec<Todo>);
impl Todos {
pub fn remaining(&self) -> usize {
todos.iter().filter(|todo| todo.completed).sum()
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_remaining {
// ...
}
}
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
// ✅ this has a test associated with it
let maximum = move || todos.with(Todos::remaining);
}
```
In general, the less of your logic is wrapped into your components themselves, the
more idiomatic your code will feel and the easier it will be to test.
## 2. Test components with `wasm-bindgen-test`
[`wasm-bindgen-test`](https://crates.io/crates/wasm-bindgen-test) is a great utility
for integrating or end-to-end testing WebAssembly apps in a headless browser.
To use this testing utility, you need to add `wasm-bindgen-test` to your `Cargo.toml`:
```toml
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
```
You should create tests in a separate `tests` directory. You can then run your tests in the browser of your choice:
```bash
wasm-pack test --firefox
```
> To see the full setup, check out the tests for the [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) example.
### Writing Your Tests
Most tests will involve some combination of vanilla DOM manipulation and comparison to a `view`. For example, heres a test [for the
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/mod.rs).
First, we set up the testing environment.
```rust
use wasm_bindgen_test::*;
use counter::*;
use leptos::*;
use web_sys::HtmlElement;
// tell the test runner to run tests in the browser
wasm_bindgen_test_configure!(run_in_browser);
```
Im going to create a simpler wrapper for each test case, and mount it there.
This makes it easy to encapsulate the test results.
```rust
// like marking a regular test with #[test]
#[wasm_bindgen_test]
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
// start by rendering our counter and mounting it to the DOM
// note that we start at the initial value of 10
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
);
```
Well use some manual DOM operations to grab the `<div>` that wraps
the whole component, as well as the `clear` button.
```rust
// now we extract the buttons by iterating over the DOM
// this would be easier if they had IDs
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
```
Now we can use ordinary DOM APIs to simulate user interaction.
```rust
// now let's click the `clear` button
clear.click();
```
You can test individual DOM element attributes or text node values. Sometimes
I like to test the whole view at once. We can do this by testing the elements
`outerHTML` against our expectations.
```rust
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system to render the test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
);
```
That test involved us manually replicating the `view` thats inside the component.
There's actually an easier way to do this... We can just test against a `<SimpleCounter/>`
with the initial value `0`. This is where our wrapping element comes in: Ill just test
the wrappers `innerHTML` against another comparison case.
```rust
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
}
```
This is only a very limited introduction to testing. But I hope its useful as you begin to build applications.
> For more, see [the testing section of the `wasm-bindgen` guide](https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/index.html#testing-on-wasm32-unknown-unknown-with-wasm-bindgen-test).

View File

@@ -1,16 +1,86 @@
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counter::*;
use leptos::*;
use web_sys::HtmlElement;
use counter::*;
#[wasm_bindgen_test]
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
// start by rendering our counter and mounting it to the DOM
// note that we start at the initial value of 10
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
);
// now we extract the buttons by iterating over the DOM
// this would be easier if they had IDs
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
// now let's click the `clear` button
clear.click();
// now let's test the <div> against the expected value
// we can do this by testing its `outerHTML`
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system, just to render the
// test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
);
// There's actually an easier way to do this...
// We can just test against a <SimpleCounter/> with the initial value 0
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
}
#[wasm_bindgen_test]
fn inc() {
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> });
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> },
);
// You can do testing with vanilla DOM operations
let document = leptos::document();
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = div
.first_child()
.unwrap()
@@ -47,4 +117,40 @@ fn inc() {
clear.click();
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
// Or you can test against a sample view!
assert_eq!(
div.outer_html(),
run_scope(create_runtime(), |cx| {
let (value, _) = create_signal(cx, 0);
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
}
.outer_html())
);
inc.click();
assert_eq!(
div.outer_html(),
run_scope(create_runtime(), |cx| {
// because we've clicked, it's as if the signal is starting at 1
let (value, _) = create_signal(cx, 1);
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
}
.outer_html())
);
}

View File

@@ -25,6 +25,7 @@ leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "4.0.0"
gloo-net = { git = "https://github.com/rustwasm/gloo" }
wasm-bindgen = "0.2"
[features]
default = []

View File

@@ -194,16 +194,16 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
use futures::StreamExt;
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect_throw("couldn't connect to SSE stream");
.expect("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,
source.subscribe("message").unwrap().map(|value| {
value
.expect_throw("no message event")
.expect("no message event")
.1
.data()
.as_string()
.expect_throw("expected string value")
.expect("expected string value")
}),
);

View File

@@ -1,4 +1,4 @@
use leptos::{ev, *};
use leptos::{ev, html::*, *};
pub struct Props {
/// The starting value for the counter
@@ -25,7 +25,9 @@ pub fn view(cx: Scope, props: Props) -> impl IntoView {
.child((
cx,
button(cx)
.on(ev::click, move |_| set_value.update(|value| *value -= step))
.on(ev::click, move |_| {
set_value.update(|value| *value -= step)
})
.child((cx, "-1")),
))
.child((
@@ -38,7 +40,9 @@ pub fn view(cx: Scope, props: Props) -> impl IntoView {
.child((
cx,
button(cx)
.on(ev::click, move |_| set_value.update(|value| *value += step))
.on(ev::click, move |_| {
set_value.update(|value| *value += step)
})
.child((cx, "+1")),
))
}

View File

@@ -1,5 +1,4 @@
use leptos::*;
use leptos::{For, ForProps};
use leptos::{For, ForProps, *};
const MANY_COUNTERS: usize = 1000;
@@ -84,9 +83,11 @@ fn Counter(
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let input = move |ev| set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default());
let input = move |ev| {
set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
// just an example of how a cleanup function works
// this will run when the scope is disposed, i.e., when this row is deleted

View File

@@ -91,9 +91,12 @@ fn Counter(
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let input = move |ev| set_value.set(event_target_value(&ev).parse::<i32>().unwrap_or_default());
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! { cx,
<li>

View File

@@ -32,45 +32,49 @@ tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
thiserror = "1.0.38"
tracing = "0.1.37"
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]
denylist = [
"axum",
"tower",
"tower-http",
"tokio",
"leptos_axum",
]
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "errors_axum"
output-name = "errors_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [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-addr = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target

View File

@@ -1,8 +1,6 @@
use crate::errors::AppError;
use cfg_if::cfg_if;
use leptos::Errors;
use leptos::*;
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
@@ -23,12 +21,11 @@ pub fn ErrorTemplate(
};
// Get Errors from Signal
let errors = errors.get().0;
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<AppError> = errors
.get()
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
.filter_map(|(_, v)| v.downcast_ref::<AppError>().cloned())
.collect();
log!("Errors: {errors:#?}");
@@ -47,7 +44,7 @@ pub fn ErrorTemplate(
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
key=|(index, _)| *index
// renders each item to a view
view=move |cx, error| {
let error_string = error.1.to_string();

View File

@@ -9,6 +9,12 @@
max-width: 250px;
height: auto;
}
.error {
border: 1px solid red;
color: red;
background-color: lightpink;
}
</style>
<body></body>
</html>

View File

@@ -1,3 +1,4 @@
use anyhow::Result;
use leptos::*;
use serde::{Deserialize, Serialize};
@@ -6,18 +7,18 @@ pub struct Cat {
url: String,
}
async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
if count > 0 {
// make the request
let res = reqwasm::http::Request::get(&format!(
"https://api.thecatapi.com/v1/images/search?limit={}",
count
"https://api.thecatapi.com/v1/images/search?limit={count}",
))
.send()
.await
.map_err(|_| ())?
.await?
// convert it to JSON
.json::<Vec<Cat>>()
.await
.map_err(|_| ())?
.await?
// extract the URL field for each cat
.into_iter()
.map(|cat| cat.url)
.collect::<Vec<_>>();
@@ -29,9 +30,45 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
pub fn fetch_example(cx: Scope) -> impl IntoView {
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
view! { cx,
// we use local_resource here because
// 1) anyhow::Result isn't serializable/deserializable
// 2) we're not doing server-side rendering in this example anyway
// (during SSR, create_resource will begin loading on the server and resolve on the client)
let cats = create_local_resource(cx, cat_count, fetch_cats);
let fallback = move |cx, errors: RwSignal<Errors>| {
let error_list = move || {
errors.with(|errors| {
errors
.iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
})
};
view! { cx,
<div class="error">
<h2>"Error"</h2>
<ul>{error_list}</ul>
</div>
}
};
// the renderer can handle Option<_> and Result<_> states
// by displaying nothing for None if the resource is still loading
// and by using the ErrorBoundary fallback to catch Err(_)
// so we'll just implement our happy path and let the framework handle the rest
let cats_view = move || {
cats.with(|data| {
data.iter()
.flatten()
.map(|cat| view! { cx, <img src={cat}/> })
.collect::<Vec<_>>()
})
};
view! { cx,
<div>
<label>
"How many cats would you like?"
@@ -43,25 +80,11 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
}
/>
</label>
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
{move || {
cats.read().map(|data| match data {
Err(_) => view! { cx, <pre>"Error"</pre> }.into_view(cx),
Ok(cats) => view! { cx,
<div>{
cats.iter()
.map(|src| {
view! { cx,
<img src={src}/>
}
})
.collect::<Vec<_>>()
}</div>
}.into_view(cx),
})
}
}
</Transition>
<ErrorBoundary fallback>
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
{cats_view}
</Transition>
</ErrorBoundary>
</div>
}
}

View File

@@ -2,7 +2,7 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="css" href="./static/style.css"/>
<link data-trunk rel="css" href="/style.css"/>
</head>
<body></body>
</html>

View File

@@ -1,5 +1,7 @@
use leptos::Errors;
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
use leptos::{
signal_prelude::*, view, Errors, For, ForProps, IntoView, RwSignal, Scope,
View,
};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
@@ -7,21 +9,22 @@ pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {cx,
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.get().0.into_iter()}
// a unique key for each item as a reference
key=|error| error.0.clone()
// renders each item to a view
view= move |cx, error| {
let error_string = error.1.to_string();
view! {
cx,
<p>"Error: " {error_string}</p>
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=errors
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
view= move |cx, (_, error)| {
let error_string = error.to_string();
view! {
cx,
<p>"Error: " {error_string}</p>
}
}
}
/>
}
.into_view(cx)

View File

@@ -8,4 +8,4 @@ leptos = { path = "../../leptos" }
console_log = "0.2"
log = "0.4"
console_error_panic_hook = "0.1.7"
web-sys = "0.3"

13
examples/ssr_modes/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Generated by Cargo
# will have compiled files and executables
/target/
pkg
# These are backup files generated by rustfmt
**/*.rs.bk
# node e2e test tools and outputs
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/

View File

@@ -0,0 +1,88 @@
[package]
name = "ssr_modes"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "0.2"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../meta", default-features = false }
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
serde = { version = "1", features = ["derive"] }
simple_logger = "4"
thiserror = "1"
tokio = { version = "1", features = ["time"] }
wasm-bindgen = "0.2"
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "assets"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
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.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 henrik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,54 @@
# Server-Side Rendering Modes
This example shows the different "rendering modes" that can be used while server-side
rendering an application:
1. **Synchronous**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the client, replacing `fallback` once they're loaded.
- *Pros*: App shell appears very quickly: great TTFB (time to first byte).
- *Cons*: Resources load relatively slowly; you need to wait for JS + Wasm to load before even making a request.
2. **Out-of-order streaming**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `Suspense` nodes.
- *Pros*: Combines the best of **synchronous** and **`async`**, with a very fast shell and resources that begin loading on the server.
- *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
3. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
- *Pros*: Does not require JS for HTML to appear in correct order.
- *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
of the page will not be interactive until the suspended chunks have loaded.
4. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
- *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
- *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
## 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)
1. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
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.
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,184 @@
use lazy_static::lazy_static;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context(cx);
view! { cx,
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
<Title text="Welcome to Leptos"/>
<Router>
<main>
<Routes>
// Well load the home page with out-of-order streaming and <Suspense/>
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
// We'll load the posts with async rendering, so they can set
// the title and metadata *after* loading the data
<Route
path="/post/:id"
view=|cx| view! { cx, <Post/> }
ssr=SsrMode::Async
/>
</Routes>
</main>
</Router>
}
}
#[component]
fn HomePage(cx: Scope) -> impl IntoView {
// load the posts
let posts =
create_resource(cx, || (), |_| async { list_post_metadata().await });
let posts_view = move || {
posts.with(|posts| posts
.clone()
.map(|posts| {
posts.iter()
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a></li>})
.collect::<Vec<_>>()
})
)
};
view! { cx,
<h1>"My Great Blog"</h1>
<Suspense fallback=move || view! { cx, <p>"Loading posts..."</p> }>
<ul>{posts_view}</ul>
</Suspense>
}
}
#[derive(Params, Copy, Clone, Debug, PartialEq, Eq)]
pub struct PostParams {
id: usize,
}
#[component]
fn Post(cx: Scope) -> impl IntoView {
let query = use_params::<PostParams>(cx);
let id = move || {
query.with(|q| {
q.as_ref().map(|q| q.id).map_err(|_| PostError::InvalidId)
})
};
let post = create_resource(cx, id, |id| async move {
match id {
Err(e) => Err(e),
Ok(id) => get_post(id)
.await
.map(|data| data.ok_or(PostError::PostNotFound))
.map_err(|_| PostError::ServerError)
.flatten(),
}
});
let post_view = move || {
post.with(|post| {
post.clone().map(|post| {
view! { cx,
// render content
<h1>{&post.title}</h1>
<p>{&post.content}</p>
// since we're using async rendering for this page,
// this metadata should be included in the actual HTML <head>
// when it's first served
<Title text=post.title/>
<Meta name="description" content=post.content/>
}
})
})
};
view! { cx,
<Suspense fallback=move || view! { cx, <p>"Loading post..."</p> }>
<ErrorBoundary fallback=|cx, errors| {
view! { cx,
<div class="error">
<h1>"Something went wrong."</h1>
<ul>
{move || errors.get()
.into_iter()
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
.collect::<Vec<_>>()
}
</ul>
</div>
}
}>
{post_view}
</ErrorBoundary>
</Suspense>
}
}
// Dummy API
lazy_static! {
static ref POSTS: Vec<Post> = vec![
Post {
id: 0,
title: "My first post".to_string(),
content: "This is my first post".to_string(),
},
Post {
id: 1,
title: "My second post".to_string(),
content: "This is my second post".to_string(),
},
Post {
id: 2,
title: "My third post".to_string(),
content: "This is my third post".to_string(),
},
];
}
#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PostError {
#[error("Invalid post ID.")]
InvalidId,
#[error("Post not found.")]
PostNotFound,
#[error("Server error.")]
ServerError,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Post {
id: usize,
title: String,
content: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PostMetadata {
id: usize,
title: String,
}
#[server(ListPostMetadata, "/api")]
pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS
.iter()
.map(|data| PostMetadata {
id: data.id,
title: data.title.clone(),
})
.collect())
}
#[server(GetPost, "/api")]
pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS.iter().find(|post| post.id == id).cloned())
}

View File

@@ -0,0 +1,25 @@
#![feature(result_flattening)]
pub mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
use app::*;
use leptos::*;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move |cx| {
view! { cx, <App/> }
});
}
}
}

View File

@@ -0,0 +1,42 @@
#[cfg(feature = "ssr")]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use actix_files::Files;
use actix_web::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
use ssr_modes::app::*;
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
GetPost::register();
ListPostMetadata::register();
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
|cx| view! { cx, <App/> },
)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -0,0 +1,3 @@
body {
font-family: sans-serif;
}

View File

@@ -29,6 +29,7 @@ sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
wasm-bindgen = "0.2"
[features]
default = ["ssr"]

View File

@@ -36,22 +36,26 @@ sqlx = { version = "0.6.2", features = [
], optional = true }
thiserror = "1.0.38"
tracing = "0.1.37"
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:sqlx",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]
denylist = [
"axum",
"tower",
"tower-http",
"tokio",
"sqlx",
"leptos_axum",
]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]

View File

@@ -1,8 +1,6 @@
use crate::errors::TodoAppError;
use cfg_if::cfg_if;
use leptos::Errors;
use leptos::*;
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
@@ -23,14 +21,12 @@ pub fn ErrorTemplate(
};
// Get Errors from Signal
let errors = errors.get().0;
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<TodoAppError> = errors
.get()
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
.collect();
println!("Errors: {errors:#?}");
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application

View File

@@ -11,7 +11,7 @@ console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
web-sys = { version = "0.3", features = ["Storage"] }
web-sys = { version = "0.3.60", features = ["Storage"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -1,4 +1,4 @@
use leptos::{web_sys::HtmlInputElement, *};
use leptos::{html::Input, leptos_dom::helpers::location_hash, *};
use storage::TodoSerialized;
use uuid::Uuid;
@@ -143,17 +143,18 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
});
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
let input_ref = NodeRef::<Input>::new(cx);
let add_todo = move |ev: web_sys::KeyboardEvent| {
let target = event_target::<HtmlInputElement>(&ev);
let input = input_ref.get().unwrap();
ev.stop_propagation();
let key_code = ev.key_code();
if key_code == ENTER_KEY {
let title = event_target_value(&ev);
let title = input.value();
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(cx, Uuid::new_v4(), title.to_string());
set_todos.update(|t| t.add(new));
target.set_value("");
input.set_value("");
}
}
};
@@ -211,6 +212,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
placeholder="What needs to be done?"
autofocus
on:keydown=add_todo
node_ref=input_ref
/>
</header>
<section

View File

@@ -1,27 +1,33 @@
use crate::Todo;
use leptos::Scope;
use serde::{Deserialize, Serialize};
use leptos::{
signal_prelude::*,
Scope,
};
use serde::{
Deserialize,
Serialize,
};
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
pub struct TodoSerialized {
pub id: Uuid,
pub title: String,
pub completed: bool,
pub id: Uuid,
pub title: String,
pub completed: bool,
}
impl TodoSerialized {
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
}
impl From<&Todo> for TodoSerialized {
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: todo.completed.get(),
}
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: todo.completed.get(),
}
}
}

View File

@@ -13,5 +13,6 @@ futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
parking_lot = "0.12.1"
regex = "1.7.0"

View File

@@ -13,9 +13,14 @@ use actix_web::{
web::Bytes,
*,
};
use futures::{Future, StreamExt};
use futures::{Future, Stream, StreamExt};
use http::StatusCode;
use leptos::*;
use leptos::{
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
leptos_server::{server_fn_by_path, Payload},
*,
};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
@@ -93,13 +98,14 @@ impl ResponseOptions {
/// 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 fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
if let Some(response_options) = use_context::<ResponseOptions>(cx) {
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
}
}
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
@@ -269,13 +275,15 @@ pub fn handle_server_fns_with_context(
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
/// to route it using [leptos_router], serving an HTML stream of your application. The stream
/// will include fallback content for any `<Suspense/>` nodes, and be immediately interactive,
/// but requires some client-side JavaScript.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
/// the documentation for that function.
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -330,6 +338,133 @@ where
render_app_to_stream_with_context(options, |_cx| {}, app_fn)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use std::{env, net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
/// let addr = conf.leptos_options.site_addr.clone();
/// HttpServer::new(move || {
/// let leptos_options = &conf.leptos_options;
///
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route(
/// "/{tail:.*}",
/// leptos_actix::render_app_to_stream_in_order(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// ),
/// )
/// })
/// .bind(&addr)?
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
pub fn render_app_to_stream_in_order<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_in_order_with_context(options, |_cx| {}, app_fn)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` [Resource](leptos::Resource)s have loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to the apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_string_async], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use std::{env, net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
/// let addr = conf.leptos_options.site_addr.clone();
/// HttpServer::new(move || {
/// let leptos_options = &conf.leptos_options;
///
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route(
/// "/{tail:.*}",
/// leptos_actix::render_app_async(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// ),
/// )
/// })
/// .bind(&addr)?
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
pub fn render_app_async<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
render_app_async_with_context(options, |_cx| {}, app_fn)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
@@ -371,14 +506,104 @@ where
})
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
///
/// This function allows you to provide additional information to Leptos for your route.
/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
///
/// ## 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_in_order_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let res_options = ResponseOptions::default();
async move {
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx).into_view(cx)
}
};
stream_app_in_order(&options, app, res_options, additional_context)
.await
}
})
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously serving the page once all `async`
/// [Resource](leptos::Resource)s have loaded.
///
/// 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_async_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let res_options = ResponseOptions::default();
async move {
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx).into_view(cx)
}
};
render_app_async_helper(
&options,
app,
res_options,
additional_context,
)
.await
}
})
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
/// the documentation for that function.
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -429,6 +654,9 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[deprecated = "You can now use `render_app_async` with `create_resource` and \
`<Suspense/>` to achieve async rendering without manually \
preloading data."]
pub fn render_preloaded_data_app<Data, Fut, IV>(
options: LeptosOptions,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
@@ -499,22 +727,39 @@ async fn stream_app(
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| generate_head_metadata(cx).into(),
additional_context,
);
build_stream_response(options, res_options, stream, runtime, scope).await
}
async fn stream_app_in_order(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
move |cx| {
let meta = use_context::<MetaContext>(cx);
let head = meta
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let body_meta = meta
.as_ref()
.and_then(|meta| meta.body.as_string())
.unwrap_or_default();
format!("{head}</head><body{body_meta}>").into()
generate_head_metadata(cx).into()
},
additional_context,
);
build_stream_response(options, res_options, stream, runtime, scope).await
}
async fn build_stream_response(
options: &LeptosOptions,
res_options: ResponseOptions,
stream: impl Stream<Item = String> + 'static,
runtime: RuntimeId,
scope: ScopeId,
) -> HttpResponse {
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts(options, use_context::<MetaContext>(cx).as_ref());
@@ -558,68 +803,40 @@ async fn stream_app(
res
}
fn html_parts(
async fn render_app_async_helper(
options: &LeptosOptions,
meta_context: Option<&MetaContext>,
) -> (String, String) {
// 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 output_name = &options.output_name;
let mut wasm_output_name = output_name.clone();
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
app: impl FnOnce(leptos::Scope) -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
move |_| "".into(),
additional_context,
);
let html = build_async_response(stream, options, runtime, scope).await;
let res_options = res_options.0.read();
let (status, mut headers) =
(res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
let mut res = HttpResponse::Ok().content_type("text/html").body(html);
// Add headers manipulated in the response
for (key, value) in headers.drain() {
if let Some(key) = key {
res.headers_mut().append(key, value);
}
}
let site_ip = &options.site_addr.ip().to_string();
let reload_port = options.reload_port;
let pkg_path = &options.site_pkg_dir;
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 html_metadata = meta_context
.and_then(|mc| mc.html.as_string())
.unwrap_or_default();
let head = format!(
r#"<!DOCTYPE html>
<html{html_metadata}>
<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>".to_string();
(head, tail)
// Set status to what is returned in the function
let res_status = res.status_mut();
*res_status = status;
// Return the response
res
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@@ -627,7 +844,7 @@ fn html_parts(
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
) -> Vec<String>
) -> Vec<(String, SsrMode)>
where
IV: IntoView + 'static,
{
@@ -635,12 +852,12 @@ where
// Empty strings screw with Actix pathing, they need to be "/"
routes = routes
.iter()
.map(|s| {
.into_iter()
.map(|(s, mode)| {
if s.is_empty() {
return "/".to_string();
return ("/".to_string(), mode);
}
s.to_string()
(s, mode)
})
.collect();
@@ -650,14 +867,14 @@ where
// Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}`
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
let routes: Vec<String> = routes
.iter()
.map(|s| wildcard_re.replace_all(s, "{tail:.*}").to_string())
.map(|s| capture_re.replace_all(&s, "{$1}").to_string())
let routes: Vec<(String, SsrMode)> = routes
.into_iter()
.map(|(s, m)| (wildcard_re.replace_all(&s, "{tail:.*}").to_string(), m))
.map(|(s, m)| (capture_re.replace_all(&s, "{$1}").to_string(), m))
.collect();
if routes.is_empty() {
vec!["/".to_string()]
vec![("/".to_string(), Default::default())]
} else {
routes
}
@@ -668,18 +885,22 @@ pub enum DataResponse<T> {
Response(actix_web::dev::Response<BoxBody>),
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
#[deprecated = "You can now use `leptos_routes` and a `<Route \
mode=SsrMode::Async/>`
to achieve async rendering without manually preloading \
data."]
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
options: LeptosOptions,
@@ -695,7 +916,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -717,20 +938,13 @@ where
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
render_app_to_stream(options.clone(), app_fn.clone()),
);
}
router
self.leptos_routes_with_context(options, paths, |_| {}, app_fn)
}
fn leptos_preloaded_data_routes<Data, Fut, IV>(
@@ -750,6 +964,7 @@ where
for path in paths.iter() {
router = router.route(
path,
#[allow(deprecated)]
render_preloaded_data_app(
options.clone(),
data_fn.clone(),
@@ -763,7 +978,7 @@ where
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -771,14 +986,28 @@ where
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
for (path, mode) in paths.iter() {
router = router.route(
path,
render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
match mode {
SsrMode::OutOfOrder => render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)
}
SsrMode::Async => render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
},
);
}
router

View File

@@ -16,5 +16,6 @@ leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }
leptos_integration_utils = { workspace = true }
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"

View File

@@ -16,11 +16,19 @@ use axum::{
response::IntoResponse,
routing::get,
};
use futures::{Future, SinkExt, Stream, StreamExt};
use futures::{
channel::mpsc::{Receiver, Sender},
Future, SinkExt, Stream, StreamExt,
};
use http::{header, method::Method, uri::Uri, version::Version, Response};
use hyper::body;
use leptos::*;
use leptos_meta::MetaContext;
use leptos::{
leptos_server::{server_fn_by_path, Payload},
ssr::*,
*,
};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::{generate_head_metadata, MetaContext};
use leptos_router::*;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc};
@@ -91,13 +99,14 @@ impl ResponseOptions {
/// 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
pub fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
if let Some(response_options) = use_context::<ResponseOptions>(cx) {
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
}
}
/// Decomposes an HTTP request into its parts, allowing you to read its headers
@@ -333,8 +342,8 @@ pub type PinnedHtmlStream =
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
/// the documentation for that function.
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -397,6 +406,79 @@ where
render_app_to_stream_with_context(options, |_| {}, app_fn)
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::*;
/// use leptos_config::get_configuration;
/// use std::{env, net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[tokio::main]
/// async fn main() {
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
/// let leptos_options = conf.leptos_options;
/// let addr = leptos_options.site_addr.clone();
///
/// // build our application with a route
/// let app =
/// Router::new().fallback(leptos_axum::render_app_to_stream_in_order(
/// leptos_options,
/// |cx| view! { cx, <MyApp/> },
/// ));
///
/// // run our app with hyper
/// // `axum::Server` is a re-export of `hyper::Server`
/// axum::Server::bind(&addr)
/// .serve(app.into_make_service())
/// .await
/// .unwrap();
/// }
/// # }
/// ```
///
/// ## 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_in_order<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<
Box<
dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>>
+ Send
+ 'static,
>,
> + Clone
+ Send
+ 'static
where
IV: IntoView,
{
render_app_to_stream_in_order_with_context(options, |_| {}, app_fn)
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
@@ -457,7 +539,7 @@ where
let full_path = format!("http://leptos.dev{path}");
let (mut tx, rx) = futures::channel::mpsc::channel(8);
let (tx, rx) = futures::channel::mpsc::channel(8);
spawn_blocking({
let app_fn = app_fn.clone();
@@ -475,55 +557,19 @@ where
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);
provide_server_redirect(cx, move |path| redirect(cx, path));
provide_contexts(cx, full_path, req_parts, default_res_options);
app_fn(cx).into_view(cx)
}
};
let (bundle, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context(
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
app,
|cx| {
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>").into()
},
|cx| generate_head_metadata(cx).into(),
add_context,
);
let cx = Scope { runtime, id: scope };
let (head, tail) = html_parts(&options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
while let Some(fragment) = shell.next().await {
_ = tx.send(fragment).await;
}
_ = tx.send(tail.to_string()).await;
// Extract the value of ResponseOptions from here
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
let new_res_parts = res_options.0.read().clone();
let mut writable = res_options2.0.write();
*writable = new_res_parts;
runtime.dispose();
tx.close_channel();
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
})
.await;
}
@@ -531,26 +577,372 @@ where
}
});
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
generate_response(res_options3, rx).await
}
})
}
}
// Get the first and second 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;
async fn generate_response(
res_options: ResponseOptions,
rx: Receiver<String>,
) -> Response<StreamBody<PinnedHtmlStream>> {
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
// Get the first and second 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;
// Extract the resources now that they've been rendered
let res_options = res_options.0.read();
let complete_stream =
futures::stream::iter([first_chunk.unwrap(), second_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
}
async fn forward_stream(
options: &LeptosOptions,
res_options2: ResponseOptions,
bundle: impl Stream<Item = String> + 'static,
runtime: RuntimeId,
scope: ScopeId,
mut tx: Sender<String>,
) {
let cx = Scope { runtime, id: scope };
let (head, tail) =
html_parts(options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
while let Some(fragment) = shell.next().await {
_ = tx.send(fragment).await;
}
_ = tx.send(tail.to_string()).await;
// Extract the value of ResponseOptions from here
let res_options = use_context::<ResponseOptions>(cx).unwrap();
let new_res_parts = res_options.0.read().clone();
let mut writable = res_options2.0.write();
*writable = new_res_parts;
runtime.dispose();
tx.close_channel();
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```ignore
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
/// let handler = leptos_axum::render_app_to_stream_in_order_with_context((*options).clone(),
/// move |cx| {
/// provide_context(cx, id.clone());
/// },
/// |cx| view! { cx, <TodoApp/> }
/// );
/// handler(req).await.into_response()
/// }
/// ```
/// 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_in_order_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<
Box<
dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>>
+ Send
+ 'static,
>,
> + Clone
+ Send
+ 'static
where
IV: IntoView,
{
move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.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 (tx, rx) = futures::channel::mpsc::channel(8);
spawn_blocking({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let app_fn = app_fn.clone();
let add_context = add_context.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| {
provide_contexts(cx, full_path, req_parts, default_res_options);
app_fn(cx).into_view(cx)
}
};
let (bundle, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata(cx).into(),
add_context,
);
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
})
.await;
}
});
}
});
generate_response(res_options3, rx).await
}
})
}
}
fn provide_contexts(
cx: Scope,
path: String,
req_parts: RequestParts,
default_res_options: ResponseOptions,
) {
let integration = ServerIntegration { path };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, MetaContext::new());
provide_context(cx, req_parts);
provide_context(cx, default_res_options);
provide_server_redirect(cx, move |path| redirect(cx, path));
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` [Resource](leptos::Resource)s have loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_string_async], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::*;
/// use leptos_config::get_configuration;
/// use std::{env, net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[tokio::main]
/// async fn main() {
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
/// let leptos_options = conf.leptos_options;
/// let addr = leptos_options.site_addr.clone();
///
/// // build our application with a route
/// let app = Router::new().fallback(leptos_axum::render_app_async(
/// leptos_options,
/// |cx| view! { cx, <MyApp/> },
/// ));
///
/// // run our app with hyper
/// // `axum::Server` is a re-export of `hyper::Server`
/// axum::Server::bind(&addr)
/// .serve(app.into_make_service())
/// .await
/// .unwrap();
/// }
/// # }
/// ```
///
/// ## 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_async<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<String>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView,
{
render_app_async_with_context(options, |_| {}, app_fn)
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` [Resource](leptos::Resource)s have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```ignore
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
/// let handler = leptos_axum::render_app_async_with_context((*options).clone(),
/// move |cx| {
/// provide_context(cx, id.clone());
/// },
/// |cx| view! { cx, <TodoApp/> }
/// );
/// handler(req).await.into_response()
/// }
/// ```
/// 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_async_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<String>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView,
{
move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.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 (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let app_fn = app_fn.clone();
let add_context = add_context.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| {
provide_contexts(cx, full_path, req_parts, default_res_options);
app_fn(cx).into_view(cx)
}
};
let (stream, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context(
app,
|_| "".into(),
add_context,
);
// Extract the value of ResponseOptions from here
let cx = leptos::Scope { runtime, id: scope };
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
let html = build_async_response(stream, &options, runtime, scope).await;
let new_res_parts = res_options.0.read().clone();
let mut writable = res_options2.0.write();
*writable = new_res_parts;
_ = tx.send(html);
})
.await;
}
});
}
});
let html = rx.await.expect("to complete HTML rendering");
let mut res = Response::new(html);
// Extract the resources now that they've been rendered
let res_options = res_options3.0.read();
let complete_stream = futures::stream::iter([
first_chunk.unwrap(),
second_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
}
@@ -563,80 +955,17 @@ where
}
}
fn html_parts(
options: &LeptosOptions,
meta: Option<&MetaContext>,
) -> (String, &'static str) {
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_addr.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 html_metadata =
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
let head = format!(
r#"<!DOCTYPE html>
<html{html_metadata}>
<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>";
(head, tail)
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<String>
) -> Vec<(String, SsrMode)>
where
IV: IntoView + 'static,
{
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<String>>>);
pub struct Routes(pub Arc<RwLock<Vec<(String, SsrMode)>>>);
let routes = Routes::default();
let routes_inner = routes.clone();
@@ -658,13 +987,19 @@ where
let routes = routes.0.read().to_owned();
// Axum's Router defines Root routes as "/" not ""
let routes: Vec<String> = routes
let routes = routes
.into_iter()
.map(|s| if s.is_empty() { "/".to_string() } else { s })
.collect();
.map(|(s, m)| {
if s.is_empty() {
("/".to_string(), m)
} else {
(s, m)
}
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec!["/".to_string()]
vec![("/".to_string(), Default::default())]
} else {
routes
}
@@ -676,7 +1011,7 @@ pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
@@ -685,7 +1020,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -694,7 +1029,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
handler: H,
) -> Self
where
@@ -707,26 +1042,19 @@ impl LeptosRoutes for axum::Router {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
get(render_app_to_stream(options.clone(), app_fn.clone())),
);
}
router
self.leptos_routes_with_context(options, paths, |_| {}, app_fn)
}
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -734,14 +1062,30 @@ impl LeptosRoutes for axum::Router {
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
for (path, mode) in paths.iter() {
router = router.route(
path,
get(render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)),
match mode {
SsrMode::OutOfOrder => {
get(render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
))
}
SsrMode::InOrder => {
get(render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
))
}
SsrMode::Async => get(render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)),
},
);
}
router
@@ -749,7 +1093,7 @@ impl LeptosRoutes for axum::Router {
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<String>,
paths: Vec<(String, SsrMode)>,
handler: H,
) -> Self
where
@@ -757,7 +1101,7 @@ impl LeptosRoutes for axum::Router {
T: 'static,
{
let mut router = self;
for path in paths.iter() {
for (path, _) in paths.iter() {
router = router.route(path, get(handler.clone()));
}
router

View File

@@ -0,0 +1,15 @@
[package]
name = "leptos_integration_utils"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Utilities to help build server integrations for the Leptos web framework."
[dependencies]
futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }

View File

@@ -0,0 +1,100 @@
use futures::{Stream, StreamExt};
use leptos::{use_context, RuntimeId, ScopeId};
use leptos_config::LeptosOptions;
use leptos_meta::MetaContext;
pub fn html_parts(
options: &LeptosOptions,
meta: Option<&MetaContext>,
) -> (String, &'static str) {
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_addr.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) {{
let found = false;
document.querySelectorAll("link").forEach((link) => {{
if (link.getAttribute('href').includes(msg.css)) {{
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
found = true;
}}
}});
if (!found) console.warn(`CSS hot-reload: Could not find a <link href=/\"${{msg.css}}\"> element`);
}};
}};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
}})()
</script>
"#
),
false => "".to_string(),
};
let html_metadata =
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
let head = format!(
r#"<!DOCTYPE html>
<html{html_metadata}>
<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>";
(head, tail)
}
pub async fn build_async_response(
stream: impl Stream<Item = String> + 'static,
options: &LeptosOptions,
runtime: RuntimeId,
scope: ScopeId,
) -> String {
let mut buf = String::new();
let mut stream = Box::pin(stream);
while let Some(chunk) = stream.next().await {
buf.push_str(&chunk);
}
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts(options, use_context::<MetaContext>(cx).as_ref());
// in async, we load the meta content *now*, after the suspenses have resolved
let meta = use_context::<MetaContext>(cx);
let head_meta = meta
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let body_meta = meta
.as_ref()
.and_then(|meta| meta.body.as_string())
.unwrap_or_default();
runtime.dispose();
format!("{head}{head_meta}</head><body{body_meta}>{buf}{tail}")
}

21
leptos/Makefile.toml Normal file
View File

@@ -0,0 +1,21 @@
[tasks.build-wasm]
clear = true
dependencies = ["build-hydrate", "build-csr"]
[tasks.build-hydrate]
command = "cargo"
args = [
"build",
"--no-default-features",
"--features=hydrate",
"--target=wasm32-unknown-unknown",
]
[tasks.build-csr]
command = "cargo"
args = [
"build",
"--no-default-features",
"--features=csr",
"--target=wasm32-unknown-unknown",
]

View File

@@ -1,7 +1,9 @@
use crate::Children;
use leptos_dom::{Errors, IntoView};
use leptos_macro::{component, view};
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
use leptos_reactive::{
create_rw_signal, provide_context, signal_prelude::*, RwSignal, Scope,
};
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
/// render nothing, and search up through the view tree for an `<ErrorBoundary/>`.
@@ -46,15 +48,15 @@ where
let children = children(cx);
move || {
match errors.get().0.is_empty() {
true => children.clone().into_view(cx),
false => view! { cx,
<>
{fallback(cx, errors)}
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
</>
match errors.with(Errors::is_empty) {
true => children.clone().into_view(cx),
false => view! { cx,
<>
{fallback(cx, errors)}
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
</>
}
.into_view(cx),
}
.into_view(cx),
}
}
}

View File

@@ -141,16 +141,32 @@
//! # }
//! ```
pub use leptos_config::*;
pub use leptos_config::{self, get_configuration, LeptosOptions};
#[cfg(not(all(
target_arch = "wasm32",
any(feature = "csr", feature = "hydrate")
)))]
/// Utilities for server-side rendering HTML.
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
pub use leptos_dom::{
self,
wasm_bindgen::{JsCast, UnwrapThrowExt},
*,
self, create_node_ref, debug_warn, document, error, ev,
helpers::{
event_target, event_target_checked, event_target_value,
request_animation_frame, request_idle_callback, set_interval,
set_timeout, window_event_listener,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,
IntoProperty, IntoView, NodeRef, Property, View,
};
pub use leptos_macro::*;
pub use leptos_reactive::*;
pub use leptos_server::{self, *};
pub use tracing;
pub use leptos_server::{
self, create_action, create_multi_action, create_server_action,
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
};
pub use typed_builder;
mod error_boundary;
pub use error_boundary::*;
@@ -161,7 +177,9 @@ pub use show::*;
mod suspense;
pub use suspense::*;
mod transition;
pub use leptos_dom::debug_warn;
#[cfg(debug_assertions)]
#[doc(hidden)]
pub use tracing;
pub use transition::*;
extern crate self as leptos;
@@ -179,9 +197,7 @@ pub type ChildrenFn = Box<dyn Fn(Scope) -> Fragment>;
pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
/// A type for taking anything that implements [`IntoAttribute`].
/// Very usefull inside components.
///
/// ## Example
/// ```rust
/// use leptos::*;
///

View File

@@ -1,6 +1,6 @@
use leptos::component;
use leptos_dom::{Fragment, IntoView};
use leptos_reactive::{create_memo, Scope};
use leptos_reactive::{create_memo, signal_prelude::*, Scope};
/// A component that will show its children when the `when` condition is `true`,
/// and show the fallback when it is `false`, without rerendering every time

View File

@@ -62,8 +62,6 @@ where
F: Fn() -> E + 'static,
E: IntoView,
{
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
let id_before_suspense = HydrationCtx::peek();
let context = SuspenseContext::new(cx);
// provide this SuspenseContext to any resources below it
@@ -85,8 +83,11 @@ where
fallback().into_view(cx)
}
} else {
use leptos_reactive::signal_prelude::*;
// run the child; we'll probably throw this away, but it will register resource reads
let child = orig_child(cx).into_view(cx);
let after_original_child = HydrationCtx::id();
let initial = {
// no resources were read under this, so just return the child
@@ -99,10 +100,11 @@ where
cx.register_suspense(
context,
&id_before_suspense.to_string(),
&current_id.to_string(),
// out-of-order streaming
{
let current_id = current_id.clone();
let orig_child = Rc::clone(&orig_child);
move || {
HydrationCtx::continue_from(current_id.clone());
DynChild::new(move || orig_child(cx))
@@ -110,6 +112,16 @@ where
.render_to_string(cx)
.to_string()
}
},
// in-order streaming
{
let current_id = current_id.clone();
move || {
HydrationCtx::continue_from(current_id.clone());
DynChild::new(move || orig_child(cx))
.into_view(cx)
.into_stream_chunks(cx)
}
}
);
@@ -118,8 +130,7 @@ where
}
};
HydrationCtx::continue_from(current_id.clone());
HydrationCtx::continue_from(after_original_child);
initial
}
}

View File

@@ -54,8 +54,8 @@ fn ssr_test_with_components() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" \
class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><div \
"<div class=\"counters\" \
id=\"_0-1\"><!--hk=_0-1-0o|leptos-counter-start--><div \
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
id=\"_0-1-3\">Value: \
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
@@ -102,8 +102,8 @@ fn ssr_test_with_snake_case_components() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" \
class=\"counters\"><!\
"<div class=\"counters\" \
id=\"_0-1\"><!\
--hk=_0-1-0o|leptos-snake-case-counter-start--><div \
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
id=\"_0-1-3\">Value: \
@@ -136,7 +136,7 @@ fn test_classes() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" class=\"my big red car\"></div>"
"<div class=\"my big red car\" id=\"_0-1\"></div>"
);
});
}
@@ -147,7 +147,7 @@ fn ssr_with_styles() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
let (_, set_value) = create_signal(cx, 0);
let styles = "myclass";
let rendered = view! {
cx, class = styles,
@@ -158,8 +158,8 @@ fn ssr_with_styles() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" \
class=\"btn myclass\">-1</button></div>"
"<div class=\"myclass\" id=\"_0-1\"><button class=\"btn myclass\" \
id=\"_0-2\">-1</button></div>"
);
});
}
@@ -170,7 +170,7 @@ fn ssr_option() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
let (_, _) = create_signal(cx, 0);
let rendered = view! {
cx,
<option/>

View File

@@ -8,6 +8,7 @@ repository = "https://github.com/leptos-rs/leptos"
description = "DOM operations for the Leptos web framework."
[dependencies]
async-recursion = "1"
cfg-if = "1"
drain_filter_polyfill = "0.1"
educe = "0.4"

View File

@@ -1,4 +0,0 @@
[tasks.build-wasm]
command = "cargo"
args = ["+nightly", "build-all-features", "--target=wasm32-unknown-unknown"]
install_crate = "cargo-all-features"

View File

@@ -138,11 +138,13 @@ where
{
/// Creates a new dynamic child which will re-render whenever it's
/// signal dependencies change.
#[track_caller]
pub fn new(child_fn: CF) -> Self {
Self::new_with_id(HydrationCtx::id(), child_fn)
}
#[doc(hidden)]
#[track_caller]
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
Self { id, child_fn }
}

View File

@@ -1,11 +1,70 @@
use crate::{HydrationCtx, IntoView};
use cfg_if::cfg_if;
use leptos_reactive::{use_context, RwSignal};
use leptos_reactive::{signal_prelude::*, use_context, RwSignal};
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<String, Arc<dyn Error + Send + Sync>>);
pub struct Errors(HashMap<ErrorKey, Arc<dyn Error + Send + Sync>>);
/// A unique key for an error that occurs at a particular location in the user interface.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct ErrorKey(String);
impl<T> From<T> for ErrorKey
where
T: Into<String>,
{
fn from(key: T) -> ErrorKey {
ErrorKey(key.into())
}
}
impl IntoIterator for Errors {
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
type IntoIter = IntoIter;
fn into_iter(self) -> Self::IntoIter {
IntoIter(self.0.into_iter())
}
}
/// An owning iterator over all the errors contained in the [Errors] struct.
pub struct IntoIter(
std::collections::hash_map::IntoIter<
ErrorKey,
Arc<dyn Error + Send + Sync>,
>,
);
impl Iterator for IntoIter {
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
fn next(
&mut self,
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
self.0.next()
}
}
/// An iterator over all the errors contained in the [Errors] struct.
pub struct Iter<'a>(
std::collections::hash_map::Iter<
'a,
ErrorKey,
Arc<dyn Error + Send + Sync>,
>,
);
impl<'a> Iterator for Iter<'a> {
type Item = (&'a ErrorKey, &'a Arc<dyn Error + Send + Sync>);
fn next(
&mut self,
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
self.0.next()
}
}
impl<T, E> IntoView for Result<T, E>
where
@@ -13,7 +72,7 @@ where
E: Error + Send + Sync + 'static,
{
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
let id = HydrationCtx::peek().previous;
let id = ErrorKey(HydrationCtx::peek().previous);
let errors = use_context::<RwSignal<Errors>>(cx);
match self {
Ok(stuff) => {
@@ -45,7 +104,7 @@ where
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove::<E>(&id);
errors.remove(&id);
});
});
});
@@ -67,25 +126,37 @@ where
}
}
impl Errors {
/// Returns `true` if there are no errors.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: String, error: E)
pub fn insert<E>(&mut self, key: ErrorKey, error: E)
where
E: Error + Send + Sync + 'static,
{
self.0.insert(key, Arc::new(error));
}
/// Add an error with the default key for errors outside the reactive system
pub fn insert_with_default_key<E>(&mut self, error: E)
where
E: Error + Send + Sync + 'static,
{
self.0.insert(String::new(), Arc::new(error));
self.0.insert(Default::default(), Arc::new(error));
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove<E>(&mut self, key: &str)
where
E: Error + Send + Sync + 'static,
{
self.0.remove(key);
pub fn remove(
&mut self,
key: &ErrorKey,
) -> Option<Arc<dyn Error + Send + Sync>> {
self.0.remove(key)
}
/// An iterator over all the errors, in arbitrary order.
pub fn iter(&self) -> Iter<'_> {
Iter(self.0.iter())
}
}

View File

@@ -8,10 +8,29 @@ use wasm_bindgen::{
};
thread_local! {
pub static GLOBAL_EVENTS: RefCell<HashSet<Cow<'static, str>>> = RefCell::new(HashSet::new());
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Cow<'static, str>>> = RefCell::new(HashSet::new());
}
// Used in template macro
#[doc(hidden)]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
target: &web_sys::Element,
event: E,
#[allow(unused_mut)] // used for tracing in debug
mut event_handler: impl FnMut(E::EventType) + 'static,
) {
let event_name = event.name();
if event.bubbles() {
add_event_listener(target, event_name, event_handler);
} else {
add_event_listener_undelegated(target, &event_name, event_handler);
}
}
/// Adds an event listener to the target DOM element using implicit event delegation.
#[doc(hidden)]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn add_event_listener<E>(
target: &web_sys::Element,
@@ -39,7 +58,7 @@ pub fn add_event_listener<E>(
#[doc(hidden)]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn add_event_listener_undelegated<E>(
pub(crate) fn add_event_listener_undelegated<E>(
target: &web_sys::Element,
event_name: &str,
mut cb: impl FnMut(E) + 'static,

View File

@@ -1,4 +1,4 @@
//! Collection of typed events.
//! Types for all DOM events.
use std::{borrow::Cow, marker::PhantomData};
use wasm_bindgen::convert::FromWasmAbi;
@@ -268,3 +268,13 @@ generate_event_types! {
readystatechange: Event,
visibilitychange: Event,
}
// Export `web_sys` event types
pub use web_sys::{
AnimationEvent, BeforeUnloadEvent, CompositionEvent, DeviceMotionEvent,
DeviceOrientationEvent, DragEvent, ErrorEvent, FocusEvent, GamepadEvent,
HashChangeEvent, InputEvent, KeyboardEvent, MouseEvent,
PageTransitionEvent, PointerEvent, PopStateEvent, ProgressEvent,
PromiseRejectionEvent, SecurityPolicyViolationEvent, StorageEvent,
SubmitEvent, TouchEvent, TransitionEvent, UiEvent, WheelEvent,
};

View File

@@ -1,3 +1,5 @@
//! A variety of DOM utility functions.
use crate::{is_server, window};
use std::time::Duration;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};

View File

@@ -1,5 +1,4 @@
pub mod math;
pub mod svg;
//! Exports types for working with HTML elements.
use cfg_if::cfg_if;
@@ -25,7 +24,7 @@ cfg_if! {
use crate::hydration::HydrationKey;
use smallvec::{smallvec, SmallVec};
const HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG: &str =
pub(crate) const HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG: &str =
"`Deref<Target = web_sys::HtmlElement>` and `AsRef<web_sys::HtmlElement>` \
can only be used on web targets. \
This is for the same reason that normal `wasm_bindgen` methods can be used \
@@ -162,6 +161,7 @@ pub struct Custom {
}
impl Custom {
/// Creates a new custom element with the given tag name.
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
let name = name.into();
let id = HydrationCtx::id();
@@ -295,7 +295,7 @@ where
}
impl<El: ElementDescriptor + 'static> HtmlElement<El> {
fn new(cx: Scope, element: El) -> Self {
pub(crate) fn new(cx: Scope, element: El) -> Self {
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
Self {

View File

@@ -110,7 +110,7 @@ impl HydrationCtx {
ID.with(|id| *id.borrow_mut() = Default::default());
}
/// Resums hydration from the provided `id`. Usefull for
/// Resumes hydration from the provided `id`. Useful for
/// `Suspense` and other fancy things.
pub fn continue_from(id: HydrationKey) {
ID.with(|i| *i.borrow_mut() = id);

View File

@@ -5,30 +5,33 @@
//! The DOM implementation for `leptos`.
#[doc(hidden)]
#[cfg_attr(debug_assertions, macro_use)]
pub extern crate tracing;
mod components;
mod events;
mod helpers;
#[doc(hidden)]
pub mod helpers;
pub mod html;
mod hydration;
mod logging;
mod macro_helpers;
pub mod math;
mod node_ref;
mod ssr;
pub mod ssr;
pub mod ssr_in_order;
pub mod svg;
mod transparent;
use cfg_if::cfg_if;
pub use components::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub use events::add_event_helper;
pub use events::typed as ev;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use events::{add_event_listener, add_event_listener_undelegated};
pub use helpers::*;
pub use html::*;
pub use html::HtmlElement;
use html::{AnyElement, ElementDescriptor};
pub use hydration::{HydrationCtx, HydrationKey};
pub use js_sys;
use leptos_reactive::Scope;
pub use logging::*;
pub use macro_helpers::*;
@@ -37,17 +40,13 @@ pub use node_ref::*;
use once_cell::unsync::Lazy as LazyCell;
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
use smallvec::SmallVec;
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub use ssr::*;
use std::{borrow::Cow, fmt};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::{cell::RefCell, rc::Rc};
pub use transparent::*;
pub use wasm_bindgen;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::JsCast;
use wasm_bindgen::UnwrapThrowExt;
pub use web_sys;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
thread_local! {
@@ -64,7 +63,8 @@ pub trait IntoView {
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
trait Mountable {
#[doc(hidden)]
pub trait Mountable {
/// Gets the [`web_sys::Node`] that can be directly inserted as
/// a child of another node. Typically, this is a [`web_sys::DocumentFragment`]
/// for components, and [`web_sys::HtmlElement`] for elements.
@@ -123,6 +123,7 @@ where
debug_assertions,
instrument(level = "trace", name = "Fn() -> impl IntoView", skip_all)
)]
#[track_caller]
fn into_view(self, cx: Scope) -> View {
DynChild::new(self).into_view(cx)
}
@@ -142,9 +143,11 @@ cfg_if! {
/// HTML element.
#[derive(Clone, PartialEq, Eq)]
pub struct Element {
#[doc(hidden)]
#[cfg(debug_assertions)]
name: Cow<'static, str>,
element: web_sys::HtmlElement,
pub name: Cow<'static, str>,
#[doc(hidden)]
pub element: web_sys::HtmlElement,
}
impl fmt::Debug for Element {
@@ -619,7 +622,11 @@ impl View {
#[cfg_attr(debug_assertions, instrument)]
#[track_caller]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
fn mount_child<GWSN: Mountable + fmt::Debug>(kind: MountKind, child: &GWSN) {
#[doc(hidden)]
pub fn mount_child<GWSN: Mountable + fmt::Debug>(
kind: MountKind,
child: &GWSN,
) {
let child = child.get_mountable_node();
match kind {
@@ -682,7 +689,8 @@ fn prepare_to_move(
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[derive(Debug)]
enum MountKind<'a> {
#[doc(hidden)]
pub enum MountKind<'a> {
Before(
// The closing node
&'a web_sys::Node,
@@ -690,7 +698,7 @@ enum MountKind<'a> {
Append(&'a web_sys::Node),
}
/// Runs the provided closure and mounts the result to eht `<body>`.
/// Runs the provided closure and mounts the result to the `<body>`.
pub fn mount_to_body<F, N>(f: F)
where
F: FnOnce(Scope) -> N + 'static,

View File

@@ -6,7 +6,8 @@ use wasm_bindgen::UnwrapThrowExt;
/// Represents the different possible values an attribute node could have.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly.
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
#[derive(Clone)]
pub enum Attribute {
/// A plain string value.
@@ -103,7 +104,7 @@ impl std::fmt::Debug for Attribute {
pub trait IntoAttribute {
/// Converts the object into an [Attribute].
fn into_attribute(self, cx: Scope) -> Attribute;
/// Helper function for dealing with [Box<dyn IntoAttribute>]
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute;
}
@@ -253,7 +254,8 @@ attr_type!(char);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_helper(
#[doc(hidden)]
pub fn attribute_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Attribute,

View File

@@ -7,7 +7,8 @@ use wasm_bindgen::UnwrapThrowExt;
/// in [`Element.classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly.
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
pub enum Class {
/// Whether the class is present.
Value(bool),
@@ -70,7 +71,8 @@ impl<T: IntoClass> IntoClass for (Scope, T) {
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_helper(
#[doc(hidden)]
pub fn class_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Class,

View File

@@ -7,7 +7,8 @@ use wasm_bindgen::UnwrapThrowExt;
/// allowing you to do fine-grained updates to single fields.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly.
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
pub enum Property {
/// A static JavaScript value.
Value(JsValue),

View File

@@ -1,4 +1,4 @@
//! MathML elements.
//! Exports types for working with MathML elements.
use super::{ElementDescriptor, HtmlElement};
use crate::HydrationCtx;
@@ -10,7 +10,7 @@ cfg_if! {
use once_cell::unsync::Lazy as LazyCell;
use wasm_bindgen::JsCast;
} else {
use super::{HydrationKey, HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
use super::{HydrationKey, html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
}
}

View File

@@ -1,15 +1,20 @@
use crate::{ElementDescriptor, HtmlElement};
use leptos_reactive::{create_effect, create_rw_signal, RwSignal, Scope};
use crate::{html::ElementDescriptor, HtmlElement};
use leptos_reactive::{
create_effect, create_rw_signal, signal_prelude::*, RwSignal, Scope,
};
use std::cell::Cell;
/// Contains a shared reference to a DOM node creating while using the `view`
/// Contains a shared reference to a DOM node created while using the `view`
/// macro to create your UI.
///
/// ```
/// # use leptos::*;
///
/// use leptos::html::Input;
///
/// #[component]
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
/// let input_ref = NodeRef::<Input>::new(cx);
/// let input_ref = create_node_ref::<Input>(cx);
///
/// let on_click = move |_| {
/// let node =
@@ -34,8 +39,46 @@ pub struct NodeRef<T: ElementDescriptor + 'static>(
RwSignal<Option<HtmlElement<T>>>,
);
/// Creates a shared reference to a DOM node created while using the `view`
/// macro to create your UI.
///
/// ```
/// # use leptos::*;
///
/// use leptos::html::Input;
///
/// #[component]
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
/// let input_ref = create_node_ref::<Input>(cx);
///
/// let on_click = move |_| {
/// let node =
/// input_ref.get().expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
///
/// view! {
/// cx,
/// <div>
/// // `node_ref` loads the input
/// <input _ref=input_ref type="text"/>
/// // the button consumes it
/// <button on:click=on_click>"Click me"</button>
/// </div>
/// }
/// }
/// ```
pub fn create_node_ref<T: ElementDescriptor + 'static>(
cx: Scope,
) -> NodeRef<T> {
NodeRef(create_rw_signal(cx, None))
}
impl<T: ElementDescriptor + 'static> NodeRef<T> {
/// Creates an empty reference.
#[deprecated = "Use `create_node_ref` instead of `NodeRef::new()`."]
pub fn new(cx: Scope) -> Self {
Self(create_rw_signal(cx, None))
}

View File

@@ -1,18 +1,22 @@
#![cfg(not(all(target_arch = "wasm32", feature = "web")))]
//! Server-side HTML rendering utilities.
use crate::{CoreComponent, HydrationCtx, IntoView, View};
use cfg_if::cfg_if;
use futures::{stream::FuturesUnordered, Stream, StreamExt};
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
use itertools::Itertools;
use leptos_reactive::*;
use std::borrow::Cow;
use std::{borrow::Cow, pin::Pin};
type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
/// Renders the given function to a static HTML string.
///
/// ```
/// # cfg_if::cfg_if! { if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
/// # use leptos::*;
/// let html = render_to_string(|cx| view! { cx,
/// let html = leptos::ssr::render_to_string(|cx| view! { cx,
/// <p>"Hello, world!"</p>
/// });
/// // static HTML includes some hydration info
@@ -152,14 +156,14 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
});
let fragments = FuturesUnordered::new();
for (fragment_id, (key_before, fut)) in pending_fragments {
fragments.push(async move { (fragment_id, key_before, fut.await) })
for (fragment_id, (fut, _)) in pending_fragments {
fragments.push(async move { (fragment_id, fut.await) })
}
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
// TODO can remove id_before_suspense entirely now
let fragments = fragments.map(|(fragment_id, _, html)| {
let fragments = fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
@@ -186,18 +190,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
)
});
// stream data for each Resource as it resolves
let resources = serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
});
let resources = render_serializers(serializers);
// HTML for the view function and script to store resources
let stream = futures::stream::once(async move {
@@ -435,7 +428,7 @@ impl View {
}
#[cfg(debug_assertions)]
fn to_kebab_case(name: &str) -> String {
pub(crate) fn to_kebab_case(name: &str) -> String {
if name.is_empty() {
return String::new();
}
@@ -470,6 +463,23 @@ fn to_kebab_case(name: &str) -> String {
new_name
}
pub(crate) fn render_serializers(
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> impl Stream<Item = String> {
serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
})
}
#[doc(hidden)]
pub fn escape_attr<T>(value: &T) -> Cow<'_, str>
where

View File

@@ -0,0 +1,354 @@
#![cfg(not(all(target_arch = "wasm32", feature = "web")))]
//! Server-side HTML rendering utilities for in-order streaming and async rendering.
use crate::{ssr::render_serializers, CoreComponent, HydrationCtx, View};
use async_recursion::async_recursion;
use cfg_if::cfg_if;
use futures::{channel::mpsc::Sender, Stream, StreamExt};
use itertools::Itertools;
use leptos_reactive::{
create_runtime, run_scope_undisposed, suspense::StreamChunk, RuntimeId,
Scope, ScopeId,
};
use std::borrow::Cow;
/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s
/// loaded in `<Suspense/>` elements have finished loading.
pub async fn render_to_string_async(
view: impl FnOnce(Scope) -> View + 'static,
) -> String {
let mut buf = String::new();
let mut stream = Box::pin(render_to_stream_in_order(view));
while let Some(chunk) = stream.next().await {
buf.push_str(&chunk);
}
buf
}
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
/// in order:
/// 1. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
/// 2. any serialized [Resource](leptos_reactive::Resource)s
pub fn render_to_stream_in_order(
view: impl FnOnce(Scope) -> View + 'static,
) -> impl Stream<Item = String> {
render_to_stream_in_order_with_prefix(view, |_| "".into())
}
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
/// in order:
/// 1. `prefix`
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
/// 3. any serialized [Resource](leptos_reactive::Resource)s
///
/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated
/// after the `view` is rendered, but before `<Suspense/>` nodes have resolved.
pub fn render_to_stream_in_order_with_prefix(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
) -> impl Stream<Item = String> {
let (stream, runtime, _) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
view,
prefix,
|_| {},
);
runtime.dispose();
stream
}
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
/// in order:
/// 1. `prefix`
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
/// 3. any serialized [Resource](leptos_reactive::Resource)s
///
/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated
/// after the `view` is rendered, but before `<Suspense/>` nodes have resolved.
pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
additional_context: impl FnOnce(Scope) + 'static,
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
HydrationCtx::reset_id();
// create the runtime
let runtime = create_runtime();
let ((chunks, prefix, pending_resources, serializers), scope_id, _) =
run_scope_undisposed(runtime, |cx| {
// add additional context
additional_context(cx);
// render view and return chunks
let view = view(cx);
let prefix = prefix(cx);
(
view.into_stream_chunks(cx),
prefix,
serde_json::to_string(&cx.pending_resources()).unwrap(),
cx.serialization_resolvers(),
)
});
let (tx, rx) = futures::channel::mpsc::channel(1);
leptos_reactive::spawn_local(async move {
handle_chunks(tx, chunks).await;
});
let stream = futures::stream::once(async move {
format!(
r#"
{prefix}
<script>
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
</script>
"#
)
})
.chain(rx)
.chain(render_serializers(serializers));
(stream, runtime, scope_id)
}
#[async_recursion(?Send)]
async fn handle_chunks(mut tx: Sender<String>, chunks: Vec<StreamChunk>) {
let mut buffer = String::new();
for chunk in chunks {
match chunk {
StreamChunk::Sync(sync) => buffer.push_str(&sync),
StreamChunk::Async(suspended) => {
// add static HTML before the Suspense and stream it down
_ = tx.try_send(std::mem::take(&mut buffer));
// send the inner stream
let suspended = suspended.await;
handle_chunks(tx.clone(), suspended).await;
}
}
}
// send final sync chunk
_ = tx.try_send(std::mem::take(&mut buffer));
}
impl View {
/// Renders the view into a set of HTML chunks that can be streamed.
pub fn into_stream_chunks(self, cx: Scope) -> Vec<StreamChunk> {
let mut chunks = Vec::new();
self.into_stream_chunks_helper(cx, &mut chunks);
chunks
}
fn into_stream_chunks_helper(
self,
cx: Scope,
chunks: &mut Vec<StreamChunk>,
) {
match self {
View::Suspense(id, _) => {
let id = id.to_string();
if let Some((_, fragment)) = cx.take_pending_fragment(&id) {
chunks.push(StreamChunk::Async(fragment));
}
}
View::Text(node) => chunks.push(StreamChunk::Sync(node.content)),
View::Component(node) => {
cfg_if! {
if #[cfg(debug_assertions)] {
let name = crate::ssr::to_kebab_case(&node.name);
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-start-->"#, HydrationCtx::to_string(&node.id, false)).into()));
for child in node.children {
child.into_stream_chunks_helper(cx, chunks);
}
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-end-->"#, HydrationCtx::to_string(&node.id, true)).into()));
} else {
for child in node.children {
child.into_stream_chunks_helper(cx, chunks);
}
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}-->"#, HydrationCtx::to_string(&node.id, true)).into()))
}
}
}
View::Element(el) => {
if let Some(prerendered) = el.prerendered {
chunks.push(StreamChunk::Sync(prerendered))
} else {
let tag_name = el.name;
let mut inner_html = None;
let attrs = el
.attrs
.into_iter()
.filter_map(
|(name, value)| -> Option<Cow<'static, str>> {
if value.is_empty() {
Some(format!(" {name}").into())
} else if name == "inner_html" {
inner_html = Some(value);
None
} else {
Some(
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into(),
)
}
},
)
.join("");
if el.is_void {
chunks.push(StreamChunk::Sync(
format!("<{tag_name}{attrs}/>").into(),
));
} else if let Some(inner_html) = inner_html {
chunks.push(StreamChunk::Sync(
format!(
"<{tag_name}{attrs}>{inner_html}</{tag_name}>"
)
.into(),
));
} else {
chunks.push(StreamChunk::Sync(
format!("<{tag_name}{attrs}>").into(),
));
for child in el.children {
child.into_stream_chunks_helper(cx, chunks);
}
chunks.push(StreamChunk::Sync(
format!("</{tag_name}>").into(),
));
}
}
}
View::Transparent(_) => {}
View::CoreComponent(node) => {
let (id, name, wrap, content) = match node {
CoreComponent::Unit(u) => (
u.id.clone(),
"",
false,
Box::new(move |chunks: &mut Vec<StreamChunk>| {
#[cfg(debug_assertions)]
{
chunks.push(StreamChunk::Sync(
format!(
"<!--hk={}|leptos-unit-->",
HydrationCtx::to_string(&u.id, true)
)
.into(),
));
}
#[cfg(not(debug_assertions))]
chunks.push(StreamChunk::Sync(
format!(
"<!--hk={}-->",
HydrationCtx::to_string(&u.id, true)
)
.into(),
));
})
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
(
node.id,
"dyn-child",
true,
Box::new(move |chunks: &mut Vec<StreamChunk>| {
if let Some(child) = *child {
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
if let View::Text(t) = child {
chunks.push(
if !cfg!(debug_assertions) {
StreamChunk::Sync(
format!("<!>{}", t.content)
.into(),
)
} else {
StreamChunk::Sync(t.content)
},
);
} else {
child.into_stream_chunks_helper(
cx, chunks,
);
}
}
})
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
)
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
true,
Box::new(move |chunks: &mut Vec<StreamChunk>| {
for node in children.into_iter().flatten() {
let id = node.id;
#[cfg(debug_assertions)]
{
chunks.push(StreamChunk::Sync(
format!(
"<!--hk={}|leptos-each-item-start-->",
HydrationCtx::to_string(&id, false)
)
.into(),
));
node.child.into_stream_chunks_helper(
cx, chunks,
);
chunks.push(StreamChunk::Sync(
format!(
"<!--hk={}|leptos-each-item-end-->",
HydrationCtx::to_string(&id, true)
)
.into(),
));
}
}
})
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
)
}
};
if wrap {
cfg_if! {
if #[cfg(debug_assertions)] {
chunks.push(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-start-->", HydrationCtx::to_string(&id, false)).into()));
content(chunks);
chunks.push(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-end-->", HydrationCtx::to_string(&id, true)).into()));
} else {
let _ = name;
content(chunks);
chunks.push(StreamChunk::Sync(format!("<!--hk={}-->", HydrationCtx::to_string(&id, true)).into()))
}
}
} else {
content(chunks);
}
}
}
}
}

View File

@@ -1,8 +1,8 @@
//! SVG elements.
//! Exports types for working with SVG elements.
use super::{ElementDescriptor, HtmlElement};
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
use super::{HydrationKey, HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
use super::{html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG, HydrationKey};
use super::{ElementDescriptor, HtmlElement};
use crate::HydrationCtx;
use leptos_reactive::Scope;
#[cfg(all(target_arch = "wasm32", feature = "web"))]

View File

@@ -25,6 +25,7 @@ leptos_dom = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
convert_case = "0.6.0"
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]
log = "0.4"

View File

@@ -178,7 +178,7 @@ impl ToTokens for Model {
}
} else {
quote! {
::leptos::Component::new(
::leptos::leptos_dom::Component::new(
stringify!(#name),
move |cx| {
#tracing_guard_expr

View File

@@ -8,7 +8,7 @@ use proc_macro::TokenStream;
use proc_macro2::TokenTree;
use quote::ToTokens;
use server::server_macro_impl;
use syn::{parse_macro_input, DeriveInput};
use syn::parse_macro_input;
use syn_rsx::{parse, NodeElement};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -32,10 +32,11 @@ impl Default for Mode {
mod params;
mod view;
use template::render_template;
use view::render_view;
mod component;
mod props;
mod server;
mod template;
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
/// same rules as HTML, with the following differences:
@@ -121,7 +122,7 @@ mod server;
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! {
/// cx,
/// <button on:click=|ev: web_sys::MouseEvent| {
/// <button on:click=|ev| {
/// log::debug!("click event: {ev:#?}");
/// }>
/// "Click me"
@@ -260,9 +261,9 @@ mod server;
///
/// // create event handlers for our buttons
/// // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
/// let clear = move |_ev: web_sys::MouseEvent| set_value(0);
/// let decrement = move |_ev: web_sys::MouseEvent| set_value.update(|value| *value -= 1);
/// let increment = move |_ev: web_sys::MouseEvent| set_value.update(|value| *value += 1);
/// let clear = move |_ev| set_value(0);
/// let decrement = move |_ev| set_value.update(|value| *value -= 1);
/// let increment = move |_ev| set_value.update(|value| *value += 1);
///
/// // this JSX is compiled to an HTML template string for performance
/// view! {
@@ -329,7 +330,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
Ok(nodes) => render_view(
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
&nodes,
Mode::default(),
Mode::Client, //Mode::default(),
global_class.as_ref(),
),
Err(error) => error.to_compile_error(),
@@ -345,6 +346,43 @@ pub fn view(tokens: TokenStream) -> TokenStream {
}
}
/// An optimized, cached template for client-side rendering. Follows the same
/// syntax as the [view!] macro. In hydration or server-side rendering mode,
/// behaves exactly as the `view` macro. In client-side rendering mode, uses a `<template>`
/// node to efficiently render the element. Should only be used with a single root element.
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn template(tokens: TokenStream) -> TokenStream {
if cfg!(feature = "csr") {
let tokens: proc_macro2::TokenStream = tokens.into();
let mut tokens = tokens.into_iter();
let (cx, comma) = (tokens.next(), tokens.next());
match (cx, comma) {
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct)))
if punct.as_char() == ',' =>
{
match parse(tokens.collect::<proc_macro2::TokenStream>().into())
{
Ok(nodes) => render_template(
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
&nodes,
),
Err(error) => error.to_compile_error(),
}
.into()
}
_ => {
panic!(
"view! macro needs a context and RSX: e.g., view! {{ cx, \
<div>...</div> }}"
)
}
}
} else {
view(tokens)
}
}
/// 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
@@ -468,6 +506,8 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// ```compile_error
/// // ❌ This won't work.
/// # use leptos::*;
/// use leptos::html::Div;
///
/// #[component]
/// fn MyComponent<T: Fn() -> HtmlElement<Div>>(cx: Scope, render_prop: T) -> impl IntoView {
/// todo!()
@@ -477,6 +517,8 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// ```
/// // ✅ Do this instead
/// # use leptos::*;
/// use leptos::html::Div;
///
/// #[component]
/// fn MyComponent<T>(cx: Scope, render_prop: T) -> impl IntoView
/// where
@@ -629,7 +671,9 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// - **Return types must be [Serializable](leptos_reactive::Serializable).**
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
/// need to deserialize the result to return it to the client.
/// - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
/// - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
@@ -643,16 +687,8 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
}
}
#[proc_macro_derive(Props, attributes(builder))]
pub fn derive_prop(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
props::impl_derive_prop(&input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
// Derive Params trait for routing
/// Derives a trait that parses a map of string keys and values into a typed
/// data structure, e.g., for route params.
#[proc_macro_derive(Params, attributes(params))]
pub fn params_derive(
input: proc_macro::TokenStream,
@@ -662,9 +698,7 @@ pub fn params_derive(
}
pub(crate) fn is_component_node(node: &NodeElement) -> bool {
let name = node.name.to_string();
let first_char = name.chars().next();
first_char
.map(|first_char| first_char.is_ascii_uppercase())
.unwrap_or(false)
node.name
.to_string()
.starts_with(|c: char| c.is_ascii_uppercase())
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,8 +35,8 @@ pub fn server_macro_impl(
} = syn::parse::<ServerFnName>(args)?;
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
let encoding = match encoding {
Encoding::Cbor => quote! { ::leptos::Encoding::Cbor },
Encoding::Url => quote! { ::leptos::Encoding::Url },
Encoding::Cbor => quote! { ::leptos::leptos_server::Encoding::Cbor },
Encoding::Url => quote! { ::leptos::leptos_server::Encoding::Url },
};
let body = syn::parse::<ServerFnBody>(s.into())?;
@@ -166,7 +166,7 @@ pub fn server_macro_impl(
#url
}
fn encoding() -> ::leptos::Encoding {
fn encoding() -> ::leptos::leptos_server::Encoding {
#encoding
}
@@ -192,7 +192,7 @@ pub fn server_macro_impl(
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
let prefix = #struct_name::prefix().to_string();
let url = prefix + "/" + #struct_name::url();
::leptos::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
::leptos::leptos_server::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
}
})
}

View File

@@ -0,0 +1,522 @@
use crate::is_component_node;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeValueExpr};
use uuid::Uuid;
pub(crate) fn render_template(cx: &Ident, nodes: &[Node]) -> TokenStream {
let template_uid = Ident::new(
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
Span::call_site(),
);
if nodes.len() == 1 {
first_node_to_tokens(cx, &template_uid, &nodes[0])
} else {
panic!("template! takes a single root element.")
}
}
fn first_node_to_tokens(
cx: &Ident,
template_uid: &Ident,
node: &Node,
) -> TokenStream {
match node {
Node::Element(node) => root_element_to_tokens(cx, template_uid, node),
_ => panic!("template! takes a single root element."),
}
}
fn root_element_to_tokens(
cx: &Ident,
template_uid: &Ident,
node: &NodeElement,
) -> TokenStream {
let mut template = String::new();
let mut navigations = Vec::new();
let mut expressions = Vec::new();
if is_component_node(node) {
crate::view::component_to_tokens(cx, node, None)
} else {
element_to_tokens(
cx,
node,
&Ident::new("root", Span::call_site()),
None,
&mut 0,
&mut 0,
&mut template,
&mut navigations,
&mut expressions,
true,
);
// create the root element from which navigations and expressions will begin
let generate_root = quote! {
let root = #template_uid.with(|tpl| tpl.content().clone_node_with_deep(true))
.unwrap()
.first_child()
.unwrap();
};
let span = node.name.span();
let navigations = if navigations.is_empty() {
quote! {}
} else {
quote! { #(#navigations);* }
};
let expressions = if expressions.is_empty() {
quote! {}
} else {
quote! { #(#expressions;);* }
};
let tag_name = node.name.to_string();
quote_spanned! {
span => {
thread_local! {
static #template_uid: web_sys::HtmlTemplateElement = {
let document = leptos::document();
let el = document.create_element("template").unwrap();
el.set_inner_html(#template);
el.unchecked_into()
};
}
#generate_root
#navigations
#expressions
leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
#[cfg(debug_assertions)]
name: #tag_name.into(),
element: root.unchecked_into()
})
}
}
}
}
#[derive(Clone, Debug)]
enum PrevSibChange {
Sib(Ident),
Parent,
Skip,
}
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
node.attributes.iter().filter_map(|node| {
if let Node::Attribute(attribute) = node {
Some(attribute)
} else {
None
}
})
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens(
cx: &Ident,
node: &NodeElement,
parent: &Ident,
prev_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
expressions: &mut Vec<TokenStream>,
is_root_el: bool,
) -> Ident {
// create this element
*next_el_id += 1;
let this_el_ident = child_ident(*next_el_id, node.name.span());
// Open tag
let name_str = node.name.to_string();
let span = node.name.span();
// CSR/hydrate, push to template
template.push('<');
template.push_str(&name_str);
// attributes
for attr in attributes(node) {
attr_to_tokens(cx, attr, &this_el_ident, template, expressions);
}
// navigation for this el
let debug_name = node.name.to_string();
let this_nav = if is_root_el {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
//debug!("=> got {}", #this_el_ident.node_name());
}
} else if let Some(prev_sib) = &prev_sib {
quote_spanned! {
span => let #this_el_ident = #debug_name;
//log::debug!("next_sibling ({})", #debug_name);
let #this_el_ident = #prev_sib.next_sibling().unwrap_or_else(|| panic!("error : {} => {} ", #debug_name, "nextSibling"));
//log::debug!("=> got {}", #this_el_ident.node_name());
}
} else {
quote_spanned! {
span => let #this_el_ident = #debug_name;
//log::debug!("first_child ({})", #debug_name);
let #this_el_ident = #parent.first_child().unwrap_or_else(|| panic!("error: {} => {}", #debug_name, "firstChild"));
//log::debug!("=> got {}", #this_el_ident.node_name());
}
};
navigations.push(this_nav);
// self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
if matches!(
name_str.as_str(),
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
) {
template.push_str("/>");
return this_el_ident;
} else {
template.push('>');
}
// iterate over children
let mut prev_sib = prev_sib;
for (idx, child) in node.children.iter().enumerate() {
// set next sib (for any insertions)
let next_sib = next_sibling_node(&node.children, idx + 1, next_el_id);
let curr_id = child_to_tokens(
cx,
child,
&this_el_ident,
if idx == 0 { None } else { prev_sib.clone() },
next_sib,
next_el_id,
next_co_id,
template,
navigations,
expressions,
);
prev_sib = match curr_id {
PrevSibChange::Sib(id) => Some(id),
PrevSibChange::Parent => None,
PrevSibChange::Skip => prev_sib,
};
}
// close tag
template.push_str("</");
template.push_str(&name_str);
template.push('>');
this_el_ident
}
fn next_sibling_node(
children: &[Node],
idx: usize,
next_el_id: &mut usize,
) -> Option<Ident> {
if children.len() <= idx {
None
} else {
let sibling = &children[idx];
match sibling {
Node::Element(sibling) => {
if is_component_node(sibling) {
next_sibling_node(children, idx + 1, next_el_id)
} else {
Some(child_ident(*next_el_id + 1, sibling.name.span()))
}
}
Node::Block(sibling) => {
Some(child_ident(*next_el_id + 1, sibling.value.span()))
}
Node::Text(sibling) => {
Some(child_ident(*next_el_id + 1, sibling.value.span()))
}
_ => panic!("expected either an element or a block"),
}
}
}
fn attr_to_tokens(
cx: &Ident,
node: &NodeAttribute,
el_id: &Ident,
template: &mut String,
expressions: &mut Vec<TokenStream>,
) {
let name = node.key.to_string();
let name = if name.starts_with('_') {
name.replacen('_', "", 1)
} else {
name
};
let name = if name.starts_with("attr:") {
name.replacen("attr:", "", 1)
} else {
name
};
let value = match &node.value {
Some(expr) => match expr.as_ref() {
syn::Expr::Lit(expr_lit) => {
if let syn::Lit::Str(s) = &expr_lit.lit {
AttributeValue::Static(s.value())
} else {
AttributeValue::Dynamic(expr)
}
}
_ => AttributeValue::Dynamic(expr),
},
None => AttributeValue::Empty,
};
let span = node.key.span();
// refs
if name == "ref" {
panic!("node_ref not yet supported in template! macro")
}
// Event Handlers
else if name.starts_with("on:") {
let (event_type, handler) =
crate::view::event_from_attribute_node(node, false);
expressions.push(quote! {
leptos::leptos_dom::add_event_helper(#el_id.unchecked_ref(), #event_type, #handler);
})
}
// Properties
else if name.starts_with("prop:") {
let name = name.replacen("prop:", "", 1);
let value = node
.value
.as_ref()
.expect("prop: blocks need values")
.as_ref();
expressions.push(quote_spanned! {
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
});
}
// Classes
else if name.starts_with("class:") {
let name = name.replacen("class:", "", 1);
let value = node
.value
.as_ref()
.expect("class: attributes need values")
.as_ref();
expressions.push(quote_spanned! {
span => leptos::leptos_dom::class_helper(#el_id.unchecked_ref(), #name.into(), #value.into_class(#cx))
});
}
// Attributes
else {
match value {
AttributeValue::Empty => {
template.push(' ');
template.push_str(&name);
}
// Static attributes (i.e., just a literal given as value, not an expression)
// are just set in the template — again, nothing programmatic
AttributeValue::Static(value) => {
template.push(' ');
template.push_str(&name);
template.push_str("=\"");
template.push_str(&value);
template.push('"');
}
AttributeValue::Dynamic(value) => {
// For client-side rendering, dynamic attributes don't need to be rendered in the template
// They'll immediately be set synchronously before the cloned template is mounted
expressions.push(quote_spanned! {
span => leptos::leptos_dom::attribute_helper(#el_id.unchecked_ref(), #name.into(), {#value}.into_attribute(#cx))
});
}
}
}
}
enum AttributeValue<'a> {
Static(String),
Dynamic(&'a syn::Expr),
Empty,
}
#[allow(clippy::too_many_arguments)]
fn child_to_tokens(
cx: &Ident,
node: &Node,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
expressions: &mut Vec<TokenStream>,
) -> PrevSibChange {
match node {
Node::Element(node) => {
if is_component_node(node) {
proc_macro_error::emit_error!(
node.name.span(),
"component children not allowed in template!, use view! \
instead"
);
PrevSibChange::Skip
} else {
PrevSibChange::Sib(element_to_tokens(
cx,
node,
parent,
prev_sib,
next_el_id,
next_co_id,
template,
navigations,
expressions,
false,
))
}
}
Node::Text(node) => block_to_tokens(
cx,
&node.value,
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
Node::Block(node) => block_to_tokens(
cx,
&node.value,
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
_ => panic!("unexpected child node type"),
}
}
#[allow(clippy::too_many_arguments)]
fn block_to_tokens(
_cx: &Ident,
value: &NodeValueExpr,
span: Span,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
) -> PrevSibChange {
let value = value.as_ref();
let str_value = match value {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(s.value()),
syn::Lit::Char(c) => Some(c.value().to_string()),
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
_ => None,
},
_ => None,
};
// code to navigate to this text node
let (name, location) = /* if is_first_child && mode == Mode::Client {
(None, quote! { })
}
else */ {
*next_el_id += 1;
let name = child_ident(*next_el_id, span);
let location = if let Some(sibling) = &prev_sib {
quote_spanned! {
span => //log::debug!("-> next sibling");
let #name = #sibling.next_sibling().unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "nextSibling"));
//log::debug!("\tnext sibling = {}", #name.node_name());
}
} else {
quote_spanned! {
span => //log::debug!("\\|/ first child on {}", #parent.node_name());
let #name = #parent.first_child().unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "firstChild"));
//log::debug!("\tfirst child = {}", #name.node_name());
}
};
(Some(name), location)
};
let mount_kind = match &next_sib {
Some(child) => {
quote! { leptos::leptos_dom::MountKind::Before(#child.clone()) }
}
None => {
quote! { leptos::leptos_dom::MountKind::Append(&#parent) }
}
};
if let Some(v) = str_value {
navigations.push(location);
template.push_str(&v);
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
} else {
template.push_str("<!>");
navigations.push(location);
expressions.push(quote! {
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
});
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
}
fn child_ident(el_id: usize, span: Span) -> Ident {
let id = format!("_el{el_id}");
Ident::new(&id, span)
}

View File

@@ -152,7 +152,7 @@ pub(crate) fn render_view(
0 => {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
span => leptos::leptos_dom::Unit
}
}
1 => root_node_to_tokens_ssr(cx, &nodes[0], global_class),
@@ -168,7 +168,7 @@ pub(crate) fn render_view(
0 => {
let span = Span::call_site();
quote_spanned! {
span => leptos::Unit
span => leptos::leptos_dom::Unit
}
}
1 => node_to_tokens(cx, &nodes[0], TagType::Unknown, global_class),
@@ -200,7 +200,7 @@ fn root_node_to_tokens_ssr(
Node::Text(node) => {
let value = node.value.as_ref();
quote! {
leptos::text(#value)
leptos::leptos_dom::html::text(#value)
}
}
Node::Block(node) => {
@@ -287,11 +287,11 @@ fn root_element_to_tokens_ssr(
} else if is_math_ml_element(&tag_name) {
quote! { math::#typed_element_name }
} else {
quote! { #typed_element_name }
quote! { html::#typed_element_name }
};
let full_name = if is_custom_element {
quote! {
leptos::leptos_dom::Custom::new(#tag_name)
leptos::leptos_dom::html::Custom::new(#tag_name)
}
} else {
quote! {
@@ -347,9 +347,9 @@ fn element_to_tokens_ssr(
// insert hydration ID
let hydration_id = if is_root {
quote! { leptos::HydrationCtx::peek(), }
quote! { leptos::leptos_dom::HydrationCtx::peek(), }
} else {
quote! { leptos::HydrationCtx::id(), }
quote! { leptos::leptos_dom::HydrationCtx::id(), }
};
match node
.attributes
@@ -456,7 +456,7 @@ fn attribute_to_tokens_ssr<'a>(
} else if name.strip_prefix("on:").is_some() {
let (event_type, handler) = event_from_attribute_node(node, false);
exprs_for_compiler.push(quote! {
leptos::ssr_event_listener(#event_type, #handler);
leptos::leptos_dom::helpers::ssr_event_listener(#event_type, #handler);
})
} else if name.strip_prefix("prop:").is_some()
|| name.strip_prefix("class:").is_some()
@@ -483,7 +483,7 @@ fn attribute_to_tokens_ssr<'a>(
holes.push(quote! {
&{#value}.into_attribute(#cx)
.as_nameless_value_string()
.map(|a| format!("{}=\"{}\"", #name, leptos::escape_attr(&a)))
.map(|a| format!("{}=\"{}\"", #name, leptos::leptos_dom::ssr::escape_attr(&a)))
.unwrap_or_default(),
})
}
@@ -607,7 +607,7 @@ fn set_class_attribute_ssr(
let value = value.as_ref();
holes.push(quote! {
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::escape_attr(&a).to_string())
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
.unwrap_or_default(),
});
}
@@ -682,7 +682,7 @@ fn node_to_tokens(
Node::Text(node) => {
let value = node.value.as_ref();
quote! {
leptos::text(#value)
leptos::leptos_dom::html::text(#value)
}
}
Node::Block(node) => {
@@ -708,7 +708,7 @@ fn element_to_tokens(
let tag = node.name.to_string();
let name = if is_custom_element(&tag) {
let name = node.name.to_string();
quote! { leptos::leptos_dom::custom(#cx, leptos::leptos_dom::Custom::new(#name)) }
quote! { leptos::leptos_dom::html::custom(#cx, leptos::leptos_dom::html::Custom::new(#name)) }
} else if is_svg_element(&tag) {
let name = &node.name;
parent_type = TagType::Svg;
@@ -725,10 +725,12 @@ fn element_to_tokens(
/* proc_macro_error::emit_warning!(name.span(), "The view macro is assuming this is an HTML element, \
but it is ambiguous; if it is an SVG or MathML element, prefix with svg:: or math::"); */
quote! {
leptos::leptos_dom::#name(#cx)
leptos::leptos_dom::html::#name(#cx)
}
}
TagType::Html => quote! { leptos::leptos_dom::#name(#cx) },
TagType::Html => {
quote! { leptos::leptos_dom::html::#name(#cx) }
}
TagType::Svg => quote! { leptos::leptos_dom::svg::#name(#cx) },
TagType::Math => {
quote! { leptos::leptos_dom::math::#name(#cx) }
@@ -737,7 +739,7 @@ fn element_to_tokens(
} else {
let name = &node.name;
parent_type = TagType::Html;
quote! { leptos::leptos_dom::#name(#cx) }
quote! { leptos::leptos_dom::html::#name(#cx) }
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
@@ -830,7 +832,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
.expect("couldn't parse event name");
let event_type = if is_custom {
quote! { Custom::new(#name) }
quote! { leptos::ev::Custom::new(#name) }
} else {
event_type
};
@@ -967,7 +969,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
}
}
fn component_to_tokens(
pub(crate) fn component_to_tokens(
cx: &Ident,
node: &NodeElement,
global_class: Option<&TokenTree>,
@@ -1079,7 +1081,7 @@ fn component_to_tokens(
}
}
fn event_from_attribute_node(
pub(crate) fn event_from_attribute_node(
attr: &NodeAttribute,
force_undelegated: bool,
) -> (TokenStream, &Expr) {
@@ -1105,9 +1107,9 @@ fn event_from_attribute_node(
.expect("couldn't parse event name");
let event_type = if force_undelegated || name_undelegated {
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
quote! { ::leptos::leptos_dom::ev::undelegated(::leptos::leptos_dom::ev::#event_type) }
} else {
quote! { ::leptos::ev::#event_type }
quote! { ::leptos::leptos_dom::ev::#event_type }
};
(event_type, handler)
}

View File

@@ -1,4 +0,0 @@
[tasks.build-wasm]
command = "cargo"
args = ["+nightly", "build-all-features", "--target=wasm32-unknown-unknown"]
install_crate = "cargo-all-features"

View File

@@ -1,5 +1,5 @@
#![forbid(unsafe_code)]
use crate::{runtime::PinnedFuture, ResourceId};
use crate::{runtime::PinnedFuture, suspense::StreamChunk, ResourceId};
use cfg_if::cfg_if;
use std::collections::{HashMap, HashSet};
@@ -8,8 +8,13 @@ pub struct SharedContext {
pub pending_resources: HashSet<ResourceId>,
pub resolved_resources: HashMap<ResourceId, String>,
#[allow(clippy::type_complexity)]
// index String is the fragment ID: tuple is (ID of previous component, Future of <Suspense/> HTML when resolved)
pub pending_fragments: HashMap<String, (String, PinnedFuture<String>)>,
// index String is the fragment ID: tuple is
// `(
// Future of <Suspense/> HTML when resolved (out-of-order)
// Future of additional stream chunks when resolved (in-order)
// )`
pub pending_fragments:
HashMap<String, (PinnedFuture<String>, PinnedFuture<Vec<StreamChunk>>)>,
}
impl std::fmt::Debug for SharedContext {

View File

@@ -67,8 +67,10 @@
//! ```
#[cfg_attr(debug_assertions, macro_use)]
pub extern crate tracing;
extern crate tracing;
#[macro_use]
mod signal;
mod context;
mod effect;
mod hydration;
@@ -78,14 +80,13 @@ mod runtime;
mod scope;
mod selector;
mod serialization;
mod signal;
mod signal_wrappers_read;
mod signal_wrappers_write;
mod slice;
mod spawn;
mod spawn_microtask;
mod stored_value;
mod suspense;
pub mod suspense;
pub use context::*;
pub use effect::*;
@@ -96,51 +97,14 @@ pub use runtime::{create_runtime, RuntimeId};
pub use scope::*;
pub use selector::*;
pub use serialization::*;
pub use signal::*;
pub use signal::{prelude as signal_prelude, *};
pub use signal_wrappers_read::*;
pub use signal_wrappers_write::*;
pub use slice::*;
pub use spawn::*;
pub use spawn_microtask::*;
pub use stored_value::*;
pub use suspense::*;
/// Trait implemented for all signal types which you can `get` a value
/// from, such as [`ReadSignal`],
/// [`Memo`], etc., which allows getting the inner value without
/// subscribing to the current scope.
pub trait UntrackedGettableSignal<T> {
/// Gets the signal's value without creating a dependency on the
/// current scope.
fn get_untracked(&self) -> T
where
T: Clone;
/// Runs the provided closure with a reference to the current
/// value without creating a dependency on the current scope.
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O;
}
/// Trait implemented for all signal types which you can `set` the inner
/// value, such as [`WriteSignal`] and [`RwSignal`], which allows setting
/// the inner value without causing effects which depend on the signal
/// from being run.
pub trait UntrackedSettableSignal<T> {
/// Sets the signal's value without notifying dependents.
fn set_untracked(&self, new_value: T);
/// Runs the provided closure with a mutable reference to the current
/// value without notifying dependents.
fn update_untracked(&self, f: impl FnOnce(&mut T));
/// Runs the provided closure with a mutable reference to the current
/// value without notifying dependents and returns
/// the value the closure returned.
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U>;
}
pub use suspense::SuspenseContext;
mod macros {
macro_rules! debug_warn {

View File

@@ -1,5 +1,8 @@
#![forbid(unsafe_code)]
use crate::{ReadSignal, Scope, SignalError, UntrackedGettableSignal};
use crate::{
create_effect, on_cleanup, ReadSignal, Scope, SignalGet,
SignalGetUntracked, SignalStream, SignalWith, SignalWithUntracked,
};
use std::fmt::Debug;
/// Creates an efficient derived reactive value based on other reactive values.
@@ -91,6 +94,19 @@ where
/// As with [create_effect](crate::create_effect), the argument to the memo function is the previous value,
/// i.e., the current value of the memo, which will be `None` for the initial calculation.
///
/// ## Core Trait Implementations
/// - [`.get()`](#impl-SignalGet<T>-for-Memo<T>) (or calling the signal as a function) clones the current
/// value of the signal. If you call it within an effect, it will cause that effect
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-Memo<T>) clones the value of the signal
/// without reactively tracking it.
/// - [`.with()`](#impl-SignalWith<T>-for-Memo<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-Memo<T>) allows you to access the signals
/// value without reactively tracking it.
/// - [`.to_stream()`](#impl-SignalStream<T>-for-Memo<T>) converts the signal to an `async` stream of values.
///
/// ## Examples
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
@@ -151,7 +167,7 @@ where
impl<T> Copy for Memo<T> {}
impl<T> UntrackedGettableSignal<T> for Memo<T> {
impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
#[cfg_attr(
debug_assertions,
instrument(
@@ -165,15 +181,31 @@ impl<T> UntrackedGettableSignal<T> for Memo<T> {
)
)
)]
fn get_untracked(&self) -> T
where
T: Clone,
{
fn get_untracked(&self) -> T {
// Unwrapping is fine because `T` will already be `Some(T)` by
// the time this method can be called
self.0.get_untracked().unwrap()
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Memo::try_get_untracked()",
skip_all,
fields(
id = ?self.0.id,
defined_at = %self.1,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_get_untracked(&self) -> Option<T> {
self.0.try_get_untracked().flatten()
}
}
impl<T> SignalWithUntracked<T> for Memo<T> {
#[cfg_attr(
debug_assertions,
instrument(
@@ -192,28 +224,42 @@ impl<T> UntrackedGettableSignal<T> for Memo<T> {
// UntrackedSignal>::get_untracked
self.0.with_untracked(|v| f(v.as_ref().unwrap()))
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Memo::try_with_untracked()",
skip_all,
fields(
id = ?self.0.id,
defined_at = %self.1,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.0.try_with_untracked(|t| f(t.as_ref().unwrap()))
}
}
impl<T> Memo<T>
where
T: 'static,
{
/// Clones and returns the current value of the memo, and subscribes
/// the running effect to the memo.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
/// let double_count = create_memo(cx, move |_| count() * 2);
///
/// assert_eq!(double_count.get(), 0);
/// set_count(1);
///
/// // double_count() is shorthand for double_count.get()
/// assert_eq!(double_count(), 2);
/// # }).dispose();
/// #
/// ```
/// # Examples
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
/// let double_count = create_memo(cx, move |_| count() * 2);
///
/// assert_eq!(double_count.get(), 0);
/// set_count(1);
///
/// // double_count() is shorthand for double_count.get()
/// assert_eq!(double_count(), 2);
/// # }).dispose();
/// #
/// ```
impl<T: Clone> SignalGet<T> for Memo<T> {
#[cfg_attr(
debug_assertions,
instrument(
@@ -226,38 +272,15 @@ where
)
)
)]
pub fn get(&self) -> T
where
T: Clone,
{
self.with(T::clone)
fn get(&self) -> T {
self.0.get().unwrap()
}
/// Applies a function to the current value of the memo, and subscribes
/// the running effect to this memo.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = create_memo(cx, move |_| name().to_uppercase());
///
/// // ❌ unnecessarily clones the string
/// let first_char = move || name_upper().chars().next().unwrap();
/// assert_eq!(first_char(), 'A');
///
/// // ✅ gets the first char without cloning the `String`
/// let first_char = move || name_upper.with(|n| n.chars().next().unwrap());
/// assert_eq!(first_char(), 'A');
/// set_name("Bob".to_string());
/// assert_eq!(first_char(), 'B');
/// # }).dispose();
/// #
/// ```
#[cfg_attr(
debug_assertions,
instrument(
name = "Memo::with()",
level = "trace",
name = "Memo::try_get()",
skip_all,
fields(
id = ?self.0.id,
@@ -266,56 +289,76 @@ where
)
)
)]
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
// okay to unwrap here, because the value will *always* have initially
// been set by the effect, synchronously
self.0
.with(|n| f(n.as_ref().expect("Memo is missing its initial value")))
fn try_get(&self) -> Option<T> {
self.0.try_get().flatten()
}
}
impl<T> SignalWith<T> for Memo<T> {
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Memo::with()",
skip_all,
fields(
id = ?self.0.id,
defined_at = %self.1,
ty = %std::any::type_name::<T>()
)
)
)]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
self.0.with(|t| f(t.as_ref().unwrap()))
}
pub(crate) fn try_with<U>(
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Memo::try_with()",
skip_all,
fields(
id = ?self.0.id,
defined_at = %self.1,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.0.try_with(|t| f(t.as_ref().unwrap())).ok()
}
}
impl<T: Clone> SignalStream<T> for Memo<T> {
fn to_stream(
&self,
f: impl Fn(&T) -> U,
) -> Result<U, SignalError> {
self.0.try_with(|n| {
f(n.as_ref().expect("Memo is missing its initial value"))
})
}
cx: Scope,
) -> std::pin::Pin<Box<dyn futures::Stream<Item = T>>> {
let (tx, rx) = futures::channel::mpsc::unbounded();
let close_channel = tx.clone();
on_cleanup(cx, move || close_channel.close_channel());
let this = *self;
create_effect(cx, move |_| {
let _ = tx.unbounded_send(this.get());
});
Box::pin(rx)
}
}
impl<T> Memo<T>
where
T: 'static,
{
#[cfg(feature = "hydrate")]
pub(crate) fn subscribe(&self) {
self.0.subscribe()
}
}
#[cfg(not(feature = "stable"))]
impl<T> FnOnce<()> for Memo<T>
where
T: Clone,
{
type Output = T;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(not(feature = "stable"))]
impl<T> FnMut<()> for Memo<T>
where
T: Clone,
{
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(not(feature = "stable"))]
impl<T> Fn<()> for Memo<T>
where
T: Clone,
{
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}
impl_get_fn_traits![Memo];

View File

@@ -5,8 +5,8 @@ use crate::{
runtime::{with_runtime, RuntimeId},
serialization::Serializable,
spawn::spawn_local,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext,
WriteSignal,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SignalUpdate,
SignalWith, SuspenseContext, WriteSignal,
};
use std::{
any::Any,
@@ -509,8 +509,8 @@ slotmap::new_key_type! {
impl<S, T> Clone for Resource<S, T>
where
S: Clone + 'static,
T: Clone + 'static,
S: 'static,
T: 'static,
{
fn clone(&self) -> Self {
Self {
@@ -526,8 +526,8 @@ where
impl<S, T> Copy for Resource<S, T>
where
S: Clone + 'static,
T: Clone + 'static,
S: 'static,
T: 'static,
{
}

View File

@@ -2,7 +2,7 @@
use crate::{
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo,
ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer,
ScopeId, ScopeProperty, SerializableResource, SignalId,
ScopeId, ScopeProperty, SerializableResource, SignalId, SignalUpdate,
UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
@@ -167,6 +167,57 @@ impl RuntimeId {
)
}
#[track_caller]
pub(crate) fn create_many_signals_with_map<T, U>(
self,
cx: Scope,
values: impl IntoIterator<Item = T>,
map_fn: impl Fn((ReadSignal<T>, WriteSignal<T>)) -> U,
) -> Vec<U>
where
T: Any + 'static,
{
with_runtime(self, move |runtime| {
let mut signals = runtime.signals.borrow_mut();
let properties = runtime.scopes.borrow();
let mut properties = properties
.get(cx.id)
.expect(
"tried to add signals to a scope that has been disposed",
)
.borrow_mut();
let values = values.into_iter();
let size = values.size_hint().0;
signals.reserve(size);
properties.reserve(size);
values
.map(|value| signals.insert(Rc::new(RefCell::new(value))))
.map(|id| {
properties.push(ScopeProperty::Signal(id));
(
ReadSignal {
runtime: self,
id,
ty: PhantomData,
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
},
WriteSignal {
runtime: self,
id,
ty: PhantomData,
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
},
)
})
.map(map_fn)
.collect()
})
.expect("tried to create a signal in a runtime that has been disposed")
}
#[track_caller]
pub(crate) fn create_rw_signal<T>(self, value: T) -> RwSignal<T>
where
T: Any + 'static,
@@ -343,7 +394,7 @@ impl Runtime {
}
}
/// Returns IDs for all [Resource]s found on any scope.
/// Returns IDs for all [resources](crate::Resource) found on any scope.
pub(crate) fn all_resources(&self) -> Vec<ResourceId> {
self.resources
.borrow()
@@ -352,7 +403,8 @@ impl Runtime {
.collect()
}
/// Returns IDs for all [Resource]s found on any scope, pending from the server.
/// Returns IDs for all [resources](crate::Resource) found on any
/// scope, pending from the server.
pub(crate) fn pending_resources(&self) -> Vec<ResourceId> {
self.resources
.borrow()

View File

@@ -1,6 +1,7 @@
#![forbid(unsafe_code)]
use crate::{
runtime::{with_runtime, RuntimeId},
suspense::StreamChunk,
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
};
use futures::stream::FuturesUnordered;
@@ -361,32 +362,37 @@ impl Scope {
pub fn register_suspense(
&self,
context: SuspenseContext,
key_before_suspense: &str,
key: &str,
resolver: impl FnOnce() -> String + 'static,
out_of_order_resolver: impl FnOnce() -> String + 'static,
in_order_resolver: impl FnOnce() -> Vec<StreamChunk> + 'static,
) {
use crate::create_isomorphic_effect;
use futures::StreamExt;
_ = with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let (tx1, mut rx1) = futures::channel::mpsc::unbounded();
let (tx2, mut rx2) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(*self, move |_| {
let pending =
context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.unbounded_send(());
_ = tx1.unbounded_send(());
_ = tx2.unbounded_send(());
}
});
shared_context.pending_fragments.insert(
key.to_string(),
(
key_before_suspense.to_string(),
Box::pin(async move {
rx.next().await;
resolver()
rx1.next().await;
out_of_order_resolver()
}),
Box::pin(async move {
rx2.next().await;
in_order_resolver()
}),
),
);
@@ -394,17 +400,35 @@ impl Scope {
}
/// 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.
///
/// The keys are hydration IDs. Valeus are tuples of two pinned
/// `Future`s that return content for out-of-order and in-order streaming, respectively.
pub fn pending_fragments(
&self,
) -> HashMap<String, (String, PinnedFuture<String>)> {
) -> HashMap<String, (PinnedFuture<String>, PinnedFuture<Vec<StreamChunk>>)>
{
with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
std::mem::take(&mut shared_context.pending_fragments)
})
.unwrap_or_default()
}
/// Takes the pending HTML for a single `<Suspense/>` node.
///
/// Returns a tuple of two pinned `Future`s that return content for out-of-order
/// and in-order streaming, respectively.
pub fn take_pending_fragment(
&self,
id: &str,
) -> Option<(PinnedFuture<String>, PinnedFuture<Vec<StreamChunk>>)> {
with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context.pending_fragments.remove(id)
})
.ok()
.flatten()
}
}
impl fmt::Debug for ScopeDisposer {

View File

@@ -1,6 +1,7 @@
#![forbid(unsafe_code)]
use crate::{
create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSignal,
create_isomorphic_effect, create_signal, ReadSignal, Scope, SignalUpdate,
WriteSignal,
};
use std::{
cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, rc::Rc,

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
#![forbid(unsafe_code)]
use crate::{
store_value, Memo, ReadSignal, RwSignal, Scope, StoredValue,
UntrackedGettableSignal,
create_effect, on_cleanup, store_value, Memo, ReadSignal, RwSignal, Scope,
SignalGet, SignalGetUntracked, SignalStream, SignalWith,
SignalWithUntracked, StoredValue,
};
/// Helper trait for converting `Fn() -> T` closures into
@@ -27,6 +28,19 @@ where
/// rather than adding a generic `F: Fn() -> T`. Values can be access with the same
/// function call, `with()`, and `get()` APIs as other signals.
///
/// ## Core Trait Implementations
/// - [`.get()`](#impl-SignalGet<T>-for-Signal<T>) (or calling the signal as a function) clones the current
/// value of the signal. If you call it within an effect, it will cause that effect
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-Signal<T>) clones the value of the signal
/// without reactively tracking it.
/// - [`.with()`](#impl-SignalWith<T>-for-Signal<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-Signal<T>) allows you to access the signals
/// value without reactively tracking it.
/// - [`.to_stream()`](#impl-SignalStream<T>-for-Signal<T>) converts the signal to an `async` stream of values.
///
/// ## Examples
/// ```rust
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@@ -71,21 +85,65 @@ impl<T> Copy for Signal<T> {}
/// Please note that using `Signal::with_untracked` still clones the inner value,
/// so there's no benefit to using it as opposed to calling
/// `Signal::get_untracked`.
impl<T> UntrackedGettableSignal<T> for Signal<T>
where
T: 'static,
{
fn get_untracked(&self) -> T
where
T: Clone,
{
impl<T: Clone> SignalGetUntracked<T> for Signal<T> {
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Signal::get_untracked()",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn get_untracked(&self) -> T {
match &self.inner {
SignalTypes::ReadSignal(s) => s.get_untracked(),
SignalTypes::Memo(m) => m.get_untracked(),
SignalTypes::DerivedSignal(cx, f) => cx.untrack(|| f.with(|f| f())),
SignalTypes::DerivedSignal(cx, f) => {
cx.untrack(|| f.with_value(|f| f()))
}
}
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Signal::try_get_untracked()",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_get_untracked(&self) -> Option<T> {
match &self.inner {
SignalTypes::ReadSignal(s) => s.try_get_untracked(),
SignalTypes::Memo(m) => m.try_get_untracked(),
SignalTypes::DerivedSignal(cx, f) => {
cx.untrack(|| f.try_with_value(|f| f()))
}
}
}
}
impl<T> SignalWithUntracked<T> for Signal<T> {
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Signal::with_untracked()",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
match &self.inner {
SignalTypes::ReadSignal(s) => s.with_untracked(f),
@@ -93,12 +151,168 @@ where
SignalTypes::DerivedSignal(cx, v_f) => {
let mut o = None;
cx.untrack(|| o = Some(f(&v_f.with(|v_f| v_f()))));
cx.untrack(|| o = Some(f(&v_f.with_value(|v_f| v_f()))));
o.unwrap()
}
}
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Signal::try_with_untracked()",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
match self.inner {
SignalTypes::ReadSignal(r) => r.try_with_untracked(f),
SignalTypes::Memo(m) => m.try_with_untracked(f),
SignalTypes::DerivedSignal(_, s) => s.try_with_value(|t| f(&t())),
}
}
}
/// # Examples
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper =
/// Signal::derive(cx, move || name.with(|n| n.to_uppercase()));
/// let memoized_lower =
/// create_memo(cx, move |_| name.with(|n| n.to_lowercase()));
///
/// // this function takes any kind of wrapped signal
/// fn current_len_inefficient(arg: Signal<String>) -> usize {
/// // ❌ unnecessarily clones the string
/// arg().len()
/// }
///
/// fn current_len(arg: &Signal<String>) -> usize {
/// // ✅ gets the length without cloning the `String`
/// arg.with(|value| value.len())
/// }
///
/// assert_eq!(current_len(&name.into()), 5);
/// assert_eq!(current_len(&name_upper), 5);
/// assert_eq!(current_len(&memoized_lower.into()), 5);
///
/// assert_eq!(name(), "Alice");
/// assert_eq!(name_upper(), "ALICE");
/// assert_eq!(memoized_lower(), "alice");
/// # });
/// ```
impl<T> SignalWith<T> for Signal<T> {
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Signal::with()",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
match &self.inner {
SignalTypes::ReadSignal(s) => s.with(f),
SignalTypes::Memo(s) => s.with(f),
SignalTypes::DerivedSignal(_, s) => f(&s.with_value(|s| s())),
}
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Signal::try_with()",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
match self.inner {
SignalTypes::ReadSignal(r) => r.try_with(f).ok(),
SignalTypes::Memo(m) => m.try_with(f),
SignalTypes::DerivedSignal(_, s) => s.try_with_value(|t| f(&t())),
}
}
}
/// # Examples
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &Signal<i32>) -> bool {
/// arg.get() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
/// assert_eq!(above_3(&double_count), true);
/// assert_eq!(above_3(&memoized_double_count.into()), true);
/// # });
/// ```
impl<T: Clone> SignalGet<T> for Signal<T> {
fn get(&self) -> T {
match self.inner {
SignalTypes::ReadSignal(r) => r.get(),
SignalTypes::Memo(m) => m.get(),
SignalTypes::DerivedSignal(_, s) => s.with_value(|t| t()),
}
}
fn try_get(&self) -> Option<T> {
match self.inner {
SignalTypes::ReadSignal(r) => r.try_get(),
SignalTypes::Memo(m) => m.try_get(),
SignalTypes::DerivedSignal(_, s) => s.try_with_value(|t| t()),
}
}
}
impl<T: Clone> SignalStream<T> for Signal<T> {
fn to_stream(
&self,
cx: Scope,
) -> std::pin::Pin<Box<dyn futures::Stream<Item = T>>> {
match self.inner {
SignalTypes::ReadSignal(r) => r.to_stream(cx),
SignalTypes::Memo(m) => m.to_stream(cx),
SignalTypes::DerivedSignal(_, s) => {
let (tx, rx) = futures::channel::mpsc::unbounded();
let close_channel = tx.clone();
on_cleanup(cx, move || close_channel.close_channel());
create_effect(cx, move |_| {
let _ = s.try_with_value(|t| tx.unbounded_send(t()));
});
Box::pin(rx)
}
}
}
}
impl<T> Signal<T>
@@ -115,7 +329,7 @@ where
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &Signal<i32>) -> bool {
/// arg() > 3
/// arg.get() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
@@ -151,98 +365,6 @@ where
}
}
/// Applies a function to the current value of the signal, and subscribes
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = Signal::derive(cx, move || name.with(|n| n.to_uppercase()));
/// let memoized_lower = create_memo(cx, move |_| name.with(|n| n.to_lowercase()));
///
/// // this function takes any kind of wrapped signal
/// fn current_len_inefficient(arg: Signal<String>) -> usize {
/// // ❌ unnecessarily clones the string
/// arg().len()
/// }
///
/// fn current_len(arg: &Signal<String>) -> usize {
/// // ✅ gets the length without cloning the `String`
/// arg.with(|value| value.len())
/// }
///
/// assert_eq!(current_len(&name.into()), 5);
/// assert_eq!(current_len(&name_upper), 5);
/// assert_eq!(current_len(&memoized_lower.into()), 5);
///
/// assert_eq!(name(), "Alice");
/// assert_eq!(name_upper(), "ALICE");
/// assert_eq!(memoized_lower(), "alice");
/// });
/// ```
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
match &self.inner {
SignalTypes::ReadSignal(s) => s.with(f),
SignalTypes::Memo(s) => s.with(f),
SignalTypes::DerivedSignal(_, s) => f(&s.with(|s| s())),
}
}
/// Clones and returns the current value of the signal, and subscribes
/// the running effect to this signal.
///
/// If you want to get the value without cloning it, use [ReadSignal::with].
/// (Theres no difference in behavior for derived signals: they re-run in any case.)
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &Signal<i32>) -> bool {
/// arg.get() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
/// assert_eq!(above_3(&double_count), true);
/// assert_eq!(above_3(&memoized_double_count.into()), true);
/// # });
/// ```
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
pub fn get(&self) -> T
where
T: Clone,
{
match &self.inner {
SignalTypes::ReadSignal(s) => s.get(),
SignalTypes::Memo(s) => s.get(),
SignalTypes::DerivedSignal(_, s) => s.with(|s| s()),
}
}
/// Creates a signal that yields the default value of `T` when
/// you call `.get()` or `signal()`.
pub fn default(cx: Scope) -> Self
@@ -344,43 +466,24 @@ where
impl<T> Eq for SignalTypes<T> where T: PartialEq {}
#[cfg(not(feature = "stable"))]
impl<T> FnOnce<()> for Signal<T>
where
T: Clone,
{
type Output = T;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(not(feature = "stable"))]
impl<T> FnMut<()> for Signal<T>
where
T: Clone,
{
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(not(feature = "stable"))]
impl<T> Fn<()> for Signal<T>
where
T: Clone,
{
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}
/// A wrapper for a value that is *either* `T` or [`Signal<T>`](crate::Signal).
///
/// This allows you to create APIs that take either a reactive or a non-reactive value
/// of the same type. This is especially useful for component properties.
///
/// ## Core Trait Implementations
/// - [`.get()`](#impl-SignalGet<T>-for-MaybeSignal<T>) (or calling the signal as a function) clones the current
/// value of the signal. If you call it within an effect, it will cause that effect
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-MaybeSignal<T>) clones the value of the signal
/// without reactively tracking it.
/// - [`.with()`](#impl-SignalWith<T>-for-MaybeSignal<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeSignal<T>) allows you to access the signals
/// value without reactively tracking it.
/// - [`.to_stream()`](#impl-SignalStream<T>-for-MaybeSignal<T>) converts the signal to an `async` stream of values.
///
/// ## Examples
/// ```rust
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@@ -419,24 +522,157 @@ impl<T: Default> Default for MaybeSignal<T> {
}
}
impl<T> UntrackedGettableSignal<T> for MaybeSignal<T>
where
T: 'static,
{
fn get_untracked(&self) -> T
where
T: Clone,
{
/// # Examples
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = MaybeSignal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
/// let static_value: MaybeSignal<i32> = 5.into();
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &MaybeSignal<i32>) -> bool {
/// arg.get() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
/// assert_eq!(above_3(&double_count), true);
/// assert_eq!(above_3(&memoized_double_count.into()), true);
/// assert_eq!(above_3(&static_value.into()), true);
/// # });
/// ```
impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
fn get(&self) -> T {
match self {
Self::Static(t) => t.clone(),
Self::Dynamic(s) => s.get(),
}
}
fn try_get(&self) -> Option<T> {
match self {
Self::Static(t) => Some(t.clone()),
Self::Dynamic(s) => s.try_get(),
}
}
}
/// # Examples
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper =
/// MaybeSignal::derive(cx, move || name.with(|n| n.to_uppercase()));
/// let memoized_lower =
/// create_memo(cx, move |_| name.with(|n| n.to_lowercase()));
/// let static_value: MaybeSignal<String> = "Bob".to_string().into();
///
/// // this function takes any kind of wrapped signal
/// fn current_len_inefficient(arg: &MaybeSignal<String>) -> usize {
/// // ❌ unnecessarily clones the string
/// arg().len()
/// }
///
/// fn current_len(arg: &MaybeSignal<String>) -> usize {
/// // ✅ gets the length without cloning the `String`
/// arg.with(|value| value.len())
/// }
///
/// assert_eq!(current_len(&name.into()), 5);
/// assert_eq!(current_len(&name_upper), 5);
/// assert_eq!(current_len(&memoized_lower.into()), 5);
/// assert_eq!(current_len(&static_value), 3);
///
/// assert_eq!(name(), "Alice");
/// assert_eq!(name_upper(), "ALICE");
/// assert_eq!(memoized_lower(), "alice");
/// assert_eq!(static_value(), "Bob");
/// # });
/// ```
impl<T> SignalWith<T> for MaybeSignal<T> {
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "MaybeSignal::with()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
match self {
Self::Static(t) => f(t),
Self::Dynamic(s) => s.with(f),
}
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "MaybeSignal::try_with()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
match self {
Self::Static(t) => Some(f(t)),
Self::Dynamic(s) => s.try_with(f),
}
}
}
impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
match self {
Self::Static(t) => f(t),
Self::Dynamic(s) => s.with_untracked(f),
}
}
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
match self {
Self::Static(t) => Some(f(t)),
Self::Dynamic(s) => s.try_with_untracked(f),
}
}
}
impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
fn get_untracked(&self) -> T {
match self {
Self::Static(t) => t.clone(),
Self::Dynamic(s) => s.get_untracked(),
}
}
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
fn try_get_untracked(&self) -> Option<T> {
match self {
Self::Static(t) => f(t),
Self::Dynamic(s) => s.with_untracked(f),
Self::Static(t) => Some(t.clone()),
Self::Dynamic(s) => s.try_get_untracked(),
}
}
}
impl<T: Clone> SignalStream<T> for MaybeSignal<T> {
fn to_stream(
&self,
cx: Scope,
) -> std::pin::Pin<Box<dyn futures::Stream<Item = T>>> {
match self {
Self::Static(t) => {
let t = t.clone();
let stream = futures::stream::once(async move { t });
Box::pin(stream)
}
Self::Dynamic(s) => s.to_stream(cx),
}
}
}
@@ -454,12 +690,13 @@ where
/// let double_count = Signal::derive(cx, move || count() * 2);
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &Signal<i32>) -> bool {
/// arg() > 3
/// fn above_3(arg: &MaybeSignal<i32>) -> bool {
/// arg.get() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
/// assert_eq!(above_3(&double_count), true);
/// assert_eq!(above_3(&double_count.into()), true);
/// assert_eq!(above_3(&2.into()), false);
/// # });
/// ```
#[cfg_attr(
@@ -477,97 +714,6 @@ where
pub fn derive(cx: Scope, derived_signal: impl Fn() -> T + 'static) -> Self {
Self::Dynamic(Signal::derive(cx, derived_signal))
}
/// Applies a function to the current value of the signal, and subscribes
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = MaybeSignal::derive(cx, move || name.with(|n| n.to_uppercase()));
/// let memoized_lower = create_memo(cx, move |_| name.with(|n| n.to_lowercase()));
/// let static_value: MaybeSignal<String> = "Bob".to_string().into();
///
/// // this function takes any kind of wrapped signal
/// fn current_len_inefficient(arg: &MaybeSignal<String>) -> usize {
/// // ❌ unnecessarily clones the string
/// arg().len()
/// }
///
/// fn current_len(arg: &MaybeSignal<String>) -> usize {
/// // ✅ gets the length without cloning the `String`
/// arg.with(|value| value.len())
/// }
///
/// assert_eq!(current_len(&name.into()), 5);
/// assert_eq!(current_len(&name_upper), 5);
/// assert_eq!(current_len(&memoized_lower.into()), 5);
/// assert_eq!(current_len(&static_value), 3);
///
/// assert_eq!(name(), "Alice");
/// assert_eq!(name_upper(), "ALICE");
/// assert_eq!(memoized_lower(), "alice");
/// assert_eq!(static_value(), "Bob");
/// });
/// ```
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "MaybeSignal::derive()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
match &self {
Self::Static(value) => f(value),
Self::Dynamic(signal) => signal.with(f),
}
}
/// Clones and returns the current value of the signal, and subscribes
/// the running effect to this signal.
///
/// If you want to get the value without cloning it, use [ReadSignal::with].
/// (Theres no difference in behavior for derived signals: they re-run in any case.)
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = MaybeSignal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
/// let static_value: MaybeSignal<i32> = 5.into();
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &MaybeSignal<i32>) -> bool {
/// arg.get() > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
/// assert_eq!(above_3(&double_count), true);
/// assert_eq!(above_3(&memoized_double_count.into()), true);
/// assert_eq!(above_3(&static_value.into()), true);
/// # });
/// ```
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "MaybeSignal::derive()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
pub fn get(&self) -> T
where
T: Clone,
{
match &self {
Self::Static(value) => value.clone(),
Self::Dynamic(signal) => signal.get(),
}
}
}
impl<T> From<T> for MaybeSignal<T> {
@@ -606,34 +752,4 @@ impl From<&str> for MaybeSignal<String> {
}
}
#[cfg(not(feature = "stable"))]
impl<T> FnOnce<()> for MaybeSignal<T>
where
T: Clone,
{
type Output = T;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(not(feature = "stable"))]
impl<T> FnMut<()> for MaybeSignal<T>
where
T: Clone,
{
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(not(feature = "stable"))]
impl<T> Fn<()> for MaybeSignal<T>
where
T: Clone,
{
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}
impl_get_fn_traits![Signal, MaybeSignal];

View File

@@ -1,5 +1,7 @@
#![forbid(unsafe_code)]
use crate::{store_value, RwSignal, Scope, StoredValue, WriteSignal};
use crate::{
store_value, RwSignal, Scope, SignalSet, StoredValue, WriteSignal,
};
/// Helper trait for converting `Fn(T)` into [`SignalSetter<T>`].
pub trait IntoSignalSetter<T>: Sized {
@@ -24,6 +26,12 @@ where
/// rather than adding a generic `F: Fn(T)`. Values can be set with the same
/// function call or `set()`, API as other signals.
///
/// ## Core Trait Implementations
/// - [`.set()`](#impl-SignalSet<T>-for-SignalSetter<T>) (or calling the setter as a function)
/// sets the signals value, and notifies all subscribers that the signals value has changed.
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
///
/// ## Examples
/// ```rust
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@@ -76,6 +84,33 @@ impl<T: Default + 'static> Default for SignalSetter<T> {
impl<T> Copy for SignalSetter<T> {}
impl<T> SignalSet<T> for SignalSetter<T> {
fn set(&self, new_value: T) {
match self.inner {
SignalSetterTypes::Default => {}
SignalSetterTypes::Write(w) => w.set(new_value),
SignalSetterTypes::Mapped(_, s) => {
s.with_value(|setter| setter(new_value))
}
}
}
fn try_set(&self, new_value: T) -> Option<T> {
match self.inner {
SignalSetterTypes::Default => Some(new_value),
SignalSetterTypes::Write(w) => w.try_set(new_value),
SignalSetterTypes::Mapped(_, s) => {
let mut new_value = Some(new_value);
let _ = s
.try_with_value(|setter| setter(new_value.take().unwrap()));
new_value
}
}
}
}
impl<T> SignalSetter<T>
where
T: 'static,
@@ -157,7 +192,7 @@ where
pub fn set(&self, value: T) {
match &self.inner {
SignalSetterTypes::Write(s) => s.set(value),
SignalSetterTypes::Mapped(_, s) => s.with(|s| s(value)),
SignalSetterTypes::Mapped(_, s) => s.with_value(|s| s(value)),
SignalSetterTypes::Default => {}
}
}
@@ -236,34 +271,4 @@ where
impl<T> Eq for SignalSetterTypes<T> where T: PartialEq {}
#[cfg(not(feature = "stable"))]
impl<T> FnOnce<(T,)> for SignalSetter<T>
where
T: 'static,
{
type Output = ();
extern "rust-call" fn call_once(self, args: (T,)) -> Self::Output {
self.set(args.0)
}
}
#[cfg(not(feature = "stable"))]
impl<T> FnMut<(T,)> for SignalSetter<T>
where
T: 'static,
{
extern "rust-call" fn call_mut(&mut self, args: (T,)) -> Self::Output {
self.set(args.0)
}
}
#[cfg(not(feature = "stable"))]
impl<T> Fn<(T,)> for SignalSetter<T>
where
T: 'static,
{
extern "rust-call" fn call(&self, args: (T,)) -> Self::Output {
self.set(args.0)
}
}
impl_set_fn_traits![SignalSetter];

View File

@@ -1,5 +1,6 @@
use crate::{
create_memo, IntoSignalSetter, RwSignal, Scope, Signal, SignalSetter,
SignalUpdate, SignalWith,
};
/// Derives a reactive slice of an [RwSignal](crate::RwSignal).

View File

@@ -1,7 +1,7 @@
#![forbid(unsafe_code)]
use crate::{
create_rw_signal, RwSignal, Scope, UntrackedGettableSignal,
UntrackedSettableSignal,
create_rw_signal, RwSignal, Scope, SignalGetUntracked, SignalSetUntracked,
SignalUpdateUntracked, SignalWithUntracked,
};
/// A **non-reactive** wrapper for any value, which can be created with [store_value].
@@ -26,21 +26,21 @@ impl<T> Clone for StoredValue<T> {
impl<T> Copy for StoredValue<T> {}
impl<T> StoredValue<T>
where
T: 'static,
{
/// Clones and returns the current stored value.
impl<T> StoredValue<T> {
/// Returns a clone of the signals current value, subscribing the effect
/// to this signal.
///
/// If you want to get the value without cloning it, use [StoredValue::with].
/// (`value.get()` is equivalent to `value.with(T::clone)`.)
/// # Panics
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// #[derive(Clone)]
/// pub struct MyCloneableData {
/// pub value: String
/// pub value: String,
/// }
/// let data = store_value(cx, MyCloneableData { value: "a".into() });
///
@@ -48,16 +48,77 @@ where
/// assert_eq!(data.get().value, "a");
/// // there's a short-hand getter form
/// assert_eq!(data().value, "a");
/// });
/// # });
/// ```
#[track_caller]
#[deprecated = "Please use `get_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn get(&self) -> T
where
T: Clone,
{
self.with(T::clone)
self.get_value()
}
/// Returns a clone of the signals current value, subscribing the effect
/// to this signal.
///
/// # Panics
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// #[derive(Clone)]
/// pub struct MyCloneableData {
/// pub value: String,
/// }
/// let data = store_value(cx, MyCloneableData { value: "a".into() });
///
/// // calling .get() clones and returns the value
/// assert_eq!(data.get().value, "a");
/// // there's a short-hand getter form
/// assert_eq!(data().value, "a");
/// # });
/// ```
#[track_caller]
pub fn get_value(&self) -> T
where
T: Clone,
{
self.0.get_untracked()
}
/// Same as [`StoredValue::get`] but will not panic by default.
#[track_caller]
#[deprecated = "Please use `try_get_value` instead, as this method does \
not track the stored value. This method will also be \
removed in a future version of `leptos`"]
pub fn try_get(&self) -> Option<T>
where
T: Clone,
{
self.try_get_value()
}
/// Same as [`StoredValue::get`] but will not panic by default.
#[track_caller]
pub fn try_get_value(&self) -> Option<T>
where
T: Clone,
{
self.0.try_get_untracked()
}
/// Applies a function to the current stored value.
///
/// # Panics
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@@ -71,11 +132,58 @@ where
/// assert_eq!(data.with(|data| data.value.clone()), "a");
/// });
/// ```
#[track_caller]
#[deprecated = "Please use `with_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
self.with_value(f)
}
/// Applies a function to the current stored value.
///
/// # Panics
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
///
/// // calling .with() to extract the value
/// assert_eq!(data.with(|data| data.value.clone()), "a");
/// # });
/// ```
#[track_caller]
// track the stored value. This method will also be removed in \
// a future version of `leptos`"]
pub fn with_value<U>(&self, f: impl FnOnce(&T) -> U) -> U {
self.0.with_untracked(f)
}
/// Applies a function to the current value to mutate it in place.
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
/// the signal is still valid. [`None`] otherwise.
#[deprecated = "Please use `try_with_value` instead, as this method does \
not track the stored value. This method will also be \
removed in a future version of `leptos`"]
pub fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.try_with_value(f)
}
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
/// the signal is still valid. [`None`] otherwise.
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.0.try_with_untracked(f)
}
/// Updates the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@@ -88,38 +196,93 @@ where
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// });
/// ```
pub fn update(&self, f: impl FnOnce(&mut T)) {
self.0.update_untracked(f);
}
/// Applies a function to the current value to mutate it in place.
/// Forwards the return value of the closure if the closure was called.
///
/// ```
/// use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
///
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let updated = data.update_returning(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let updated = data.update_returning(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
///
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// # });
/// ```
#[track_caller]
#[deprecated = "Please use `update_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn update(&self, f: impl FnOnce(&mut T)) {
self.update_value(f);
}
/// Updates the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.update(|data| data.value = "b".into());
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// });
/// ```
///
/// ```
/// use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
///
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let updated = data.update_returning(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
///
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// # });
/// ```
#[track_caller]
pub fn update_value(&self, f: impl FnOnce(&mut T)) {
self.0.update_untracked(f);
}
/// Updates the stored value.
#[track_caller]
#[deprecated = "Please use `try_update_value` instead, as this method does \
not track the stored value. This method will also be \
removed in a future version of `leptos`"]
pub fn update_returning<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.0.update_returning_untracked(f)
self.try_update_value(f)
}
/// Same as [`Self::update`], but returns [`Some(O)`] if the
/// signal is still valid, [`None`] otherwise.
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
self.0.try_update_untracked(f)
}
/// Sets the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@@ -132,9 +295,39 @@ where
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// });
/// ```
#[track_caller]
#[deprecated = "Please use `set_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn set(&self, value: T) {
self.set_value(value);
}
/// Sets the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.set(MyUncloneableData { value: "b".into() });
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// # });
/// ```
#[track_caller]
pub fn set_value(&self, value: T) {
self.0.set_untracked(value);
}
/// Same as [`Self::set`], but returns [`None`] if the signal is
/// still valid, [`Some(T)`] otherwise.
pub fn try_set_value(&self, value: T) -> Option<T> {
self.0.try_set_untracked(value)
}
}
/// Creates a **non-reactive** wrapper for any value by storing it within
@@ -179,34 +372,4 @@ where
StoredValue(create_rw_signal(cx, value))
}
#[cfg(not(feature = "stable"))]
impl<T> FnOnce<()> for StoredValue<T>
where
T: Clone,
{
type Output = T;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(not(feature = "stable"))]
impl<T> FnMut<()> for StoredValue<T>
where
T: Clone,
{
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(not(feature = "stable"))]
impl<T> Fn<()> for StoredValue<T>
where
T: Clone,
{
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}
impl_get_fn_traits!(StoredValue(get_value));

View File

@@ -1,5 +1,12 @@
//! Types that handle asynchronous data loading via `<Suspense/>`.
#![forbid(unsafe_code)]
use crate::{create_signal, queue_microtask, ReadSignal, Scope, WriteSignal};
use crate::{
create_signal, queue_microtask, ReadSignal, Scope, SignalUpdate,
WriteSignal,
};
use futures::Future;
use std::{borrow::Cow, pin::Pin};
/// Tracks [Resource](crate::Resource)s that are read under a suspense context,
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
@@ -61,3 +68,20 @@ impl SuspenseContext {
.unwrap_or(false)
}
}
/// Represents a chunk in a stream of HTML.
pub enum StreamChunk {
/// A chunk of synchronous HTML.
Sync(Cow<'static, str>),
/// A future that resolves to be a list of additional chunks.
Async(Pin<Box<dyn Future<Output = Vec<StreamChunk>>>>),
}
impl std::fmt::Debug for StreamChunk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StreamChunk::Sync(data) => write!(f, "StreamChunk::Sync({data:?})"),
StreamChunk::Async(_) => write!(f, "StreamChunk::Async(_)"),
}
}
}

View File

@@ -1,10 +1,10 @@
//#[cfg(not(feature = "stable"))]
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_runtime, create_scope, create_signal,
UntrackedGettableSignal, UntrackedSettableSignal,
signal_prelude::*, SignalGetUntracked, SignalSetUntracked,
};
//#[cfg(not(feature = "stable"))]
#[cfg(not(feature = "stable"))]
#[test]
fn untracked_set_doesnt_trigger_effect() {
use std::{cell::RefCell, rc::Rc};
@@ -36,6 +36,7 @@ fn untracked_set_doesnt_trigger_effect() {
.dispose()
}
#[cfg(not(feature = "stable"))]
#[test]
fn untracked_get_doesnt_trigger_effect() {
use std::{cell::RefCell, rc::Rc};

View File

@@ -13,6 +13,7 @@ leptos_dom = { workspace = true }
leptos_reactive = { workspace = true }
form_urlencoded = "1"
gloo-net = "0.2"
js-sys = "0.3"
lazy_static = "1"
serde = { version = "1", features = ["derive"] }
serde_urlencoded = "0.7"

View File

@@ -1,4 +0,0 @@
[tasks.build-wasm]
command = "cargo"
args = ["+nightly", "build-all-features", "--target=wasm32-unknown-unknown"]
install_crate = "cargo-all-features"

View File

@@ -1,7 +1,7 @@
use crate::{ServerFn, ServerFnError};
use leptos_reactive::{
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope,
StoredValue,
create_rw_signal, signal_prelude::*, spawn_local, store_value, ReadSignal,
RwSignal, Scope, StoredValue,
};
use std::{future::Future, pin::Pin, rc::Rc};
@@ -90,30 +90,30 @@ where
{
/// Calls the `async` function with a reference to the input type as its argument.
pub fn dispatch(&self, input: I) {
self.0.with(|a| a.dispatch(input))
self.0.with_value(|a| a.dispatch(input))
}
/// Whether the action has been dispatched and is currently waiting for its future to be resolved.
pub fn pending(&self) -> ReadSignal<bool> {
self.0.with(|a| a.pending.read_only())
self.0.with_value(|a| a.pending.read_only())
}
/// Updates whether the action is currently pending.
pub fn set_pending(&self, pending: bool) {
self.0.with(|a| a.pending.set(pending))
self.0.with_value(|a| a.pending.set(pending))
}
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<String> {
self.0.with(|a| a.url.as_ref().cloned())
self.0.with_value(|a| a.url.as_ref().cloned())
}
/// Associates the URL of the given server function with this action.
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn using_server_fn<T: ServerFn>(self) -> Self {
let prefix = T::prefix();
self.0.update(|state| {
self.0.update_value(|state| {
state.url = if prefix.is_empty() {
Some(T::url().to_string())
} else {
@@ -125,18 +125,18 @@ where
/// How many times the action has successfully resolved.
pub fn version(&self) -> RwSignal<usize> {
self.0.with(|a| a.version)
self.0.with_value(|a| a.version)
}
/// The current argument that was dispatched to the `async` function.
/// `Some` while we are waiting for it to resolve, `None` if it has resolved.
pub fn input(&self) -> RwSignal<Option<I>> {
self.0.with(|a| a.input)
self.0.with_value(|a| a.input)
}
/// The most recent return value of the `async` function.
pub fn value(&self) -> RwSignal<Option<O>> {
self.0.with(|a| a.value)
self.0.with_value(|a| a.value)
}
}

View File

@@ -78,7 +78,6 @@
//! can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
//! or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
pub use form_urlencoded;
use leptos_reactive::*;
use proc_macro2::{Literal, TokenStream};
use quote::TokenStreamExt;
@@ -384,7 +383,7 @@ where
T: serde::Serialize + serde::de::DeserializeOwned + Sized,
{
use ciborium::ser::into_writer;
use leptos_dom::js_sys::Uint8Array;
use js_sys::Uint8Array;
use serde_json::Deserializer as JSONDeserializer;
#[derive(Debug)]

View File

@@ -1,7 +1,7 @@
use crate::{ServerFn, ServerFnError};
use leptos_reactive::{
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope,
StoredValue,
create_rw_signal, signal_prelude::*, spawn_local, store_value, ReadSignal,
RwSignal, Scope, StoredValue,
};
use std::{future::Future, pin::Pin, rc::Rc};
@@ -20,9 +20,9 @@ use std::{future::Future, pin::Pin, rc::Rc};
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// // do something...
/// // return a task id
/// 42
/// }
/// let add_todo = create_multi_action(cx, |task: &String| {
/// // `task` is given as `&String` because its value is available in `input`
@@ -95,30 +95,30 @@ where
{
/// Calls the `async` function with a reference to the input type as its argument.
pub fn dispatch(&self, input: I) {
self.0.with(|a| a.dispatch(input))
self.0.with_value(|a| a.dispatch(input))
}
/// The set of all submissions to this multi-action.
pub fn submissions(&self) -> ReadSignal<Vec<Submission<I, O>>> {
self.0.with(|a| a.submissions())
self.0.with_value(|a| a.submissions())
}
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<String> {
self.0.with(|a| a.url.as_ref().cloned())
self.0.with_value(|a| a.url.as_ref().cloned())
}
/// How many times an action has successfully resolved.
pub fn version(&self) -> RwSignal<usize> {
self.0.with(|a| a.version)
self.0.with_value(|a| a.version)
}
/// Associates the URL of the given server function with this action.
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
pub fn using_server_fn<T: ServerFn>(self) -> Self {
let prefix = T::prefix();
self.0.update(|a| {
self.0.update_value(|a| {
a.url = if prefix.is_empty() {
Some(T::url().to_string())
} else {
@@ -243,9 +243,9 @@ where
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// // do something...
/// // return a task id
/// 42
/// }
/// let add_todo = create_multi_action(cx, |task: &String| {
/// // `task` is given as `&String` because its value is available in `input`

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.1.3"
version = "0.2.0-alpha2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -12,6 +12,7 @@ cfg-if = "1"
leptos = { workspace = true }
tracing = "0.1"
typed-builder = "0.12"
wasm-bindgen = "0.2"
[dependencies.web-sys]
version = "0.3"

View File

@@ -45,13 +45,18 @@
//! which mode your app is operating in.
use cfg_if::cfg_if;
use leptos::{leptos_dom::debug_warn, *};
use leptos::{
leptos_dom::{debug_warn, html::AnyElement},
*,
};
use std::{
cell::{Cell, RefCell},
collections::HashMap,
fmt::Debug,
rc::Rc,
};
#[cfg(any(feature = "csr", feature = "hydrate"))]
use wasm_bindgen::{JsCast, UnwrapThrowExt};
mod body;
mod html;
@@ -255,6 +260,8 @@ impl MetaContext {
/// # }
/// ```
pub fn dehydrate(&self) -> String {
use leptos::leptos_dom::HydrationCtx;
let prev_key = HydrationCtx::peek();
let mut tags = String::new();
@@ -271,6 +278,23 @@ impl MetaContext {
}
}
/// Extracts the metadata that should be used to close the `<head>` tag
/// and open the `<body>` tag. This is a helper function used in implementing
/// server-side HTML rendering across crates.
#[cfg(feature = "ssr")]
pub fn generate_head_metadata(cx: Scope) -> String {
let meta = use_context::<MetaContext>(cx);
let head = meta
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let body_meta = meta
.as_ref()
.and_then(|meta| meta.body.as_string())
.unwrap_or_default();
format!("{head}</head><body{body_meta}>")
}
/// Describes a value that is either a static or a reactive string, i.e.,
/// a [String], a [&str], or a reactive `Fn() -> String`.
#[derive(Clone)]

View File

@@ -85,7 +85,7 @@ pub fn Link(
let next_id = meta.tags.get_next_id();
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
let builder_el = leptos::link(cx)
let builder_el = leptos::leptos_dom::html::link(cx)
.attr("id", &id)
.attr("as_", as_)
.attr("crossorigin", crossorigin)

View File

@@ -41,7 +41,7 @@ pub fn Meta(
let next_id = meta.tags.get_next_id();
let id = format!("leptos-link-{}", next_id.0);
let builder_el = leptos::meta(cx)
let builder_el = leptos::leptos_dom::html::meta(cx)
.attr("charset", move || charset.as_ref().map(|v| v.get()))
.attr("name", move || name.as_ref().map(|v| v.get()))
.attr("http-equiv", move || http_equiv.as_ref().map(|v| v.get()))

View File

@@ -67,7 +67,7 @@ pub fn Script(
let next_id = meta.tags.get_next_id();
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
let builder_el = leptos::script(cx)
let builder_el = leptos::leptos_dom::html::script(cx)
.attr("id", &id)
.attr("async", async_)
.attr("crossorigin", crossorigin)

View File

@@ -46,7 +46,7 @@ pub fn Style(
let next_id = meta.tags.get_next_id();
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
let builder_el = leptos::style(cx)
let builder_el = leptos::leptos_dom::html::style(cx)
.attr("id", &id)
.attr("media", media)
.attr("nonce", nonce)

View File

@@ -2,6 +2,8 @@ use crate::{use_head, TextProp};
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, rc::Rc};
#[cfg(any(feature = "csr", feature = "hydrate"))]
use wasm_bindgen::{JsCast, UnwrapThrowExt};
/// Contains the current state of the document's `<title>`.
#[derive(Clone, Default)]

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.1.3"
version = "0.2.0-alpha2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,7 +1,7 @@
use crate::{use_navigate, use_resolved_path, ToHref};
use leptos::*;
use std::{error::Error, rc::Rc};
use wasm_bindgen::JsCast;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
type OnFormData = Rc<dyn Fn(&web_sys::FormData)>;
@@ -58,7 +58,7 @@ where
on_response: Option<OnResponse>,
class: Option<Attribute>,
children: Children,
) -> HtmlElement<Form> {
) -> HtmlElement<html::Form> {
let action_version = version;
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {

Some files were not shown because too many files have changed in this diff Show More