mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 10:11:56 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e78ce7e6b9 | ||
|
|
a3327f8841 | ||
|
|
f727dd773b | ||
|
|
952646f066 | ||
|
|
1e037ecb60 | ||
|
|
c9f75d82d6 | ||
|
|
de3849c20c | ||
|
|
c391c2e938 | ||
|
|
1cde4b1f8a | ||
|
|
42360d109b | ||
|
|
7aa4d9e6db | ||
|
|
9ed3390b81 | ||
|
|
1ff56f7bfd | ||
|
|
16917997cd | ||
|
|
f42568d262 | ||
|
|
97bbdf561a | ||
|
|
f4043cbd9f | ||
|
|
e9ff26abb4 | ||
|
|
e6b1298915 | ||
|
|
98a9ec8335 | ||
|
|
5329561687 | ||
|
|
89ca047f2f | ||
|
|
a94711fcf0 | ||
|
|
97d88c65ae | ||
|
|
e482e3748d | ||
|
|
8ab9c08448 | ||
|
|
56de70b714 | ||
|
|
38d97babd8 | ||
|
|
4cfecb5d82 | ||
|
|
08b5970b2b | ||
|
|
af20f80b2b | ||
|
|
c2fdd2cd70 | ||
|
|
286f3eebe4 | ||
|
|
509223ab2e | ||
|
|
665b0b8ed2 | ||
|
|
508ad52582 |
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.3"
|
||||
version = "0.2.5"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.3" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.3" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.3" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.3" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.3" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.3" }
|
||||
leptos_router = { path = "./router", version = "0.2.3" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.3" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.5" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.5" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.5" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.5" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.5" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.5" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.5" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.5" }
|
||||
leptos_router = { path = "./router", version = "0.2.5" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.5" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -61,3 +61,19 @@ view! {
|
||||
<input prop:value=a on:input=on_input />
|
||||
}
|
||||
```
|
||||
|
||||
## Build configuration
|
||||
|
||||
### Cargo feature resolution in workspaces
|
||||
|
||||
A new [version](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) of Cargo's feature resolver was introduced for the 2021 edition of Rust.
|
||||
For single crate projects it will select a resolver version based on the Rust edition in `Cargo.toml`. As there is no Rust edition present for `Cargo.toml` in a workspace, Cargo will default to the pre 2021 edition resolver.
|
||||
This can cause issues resulting in non WASM compatible code being built for a WASM target. Seeing `mio` failing to build is often a sign that none WASM compatible code is being included in the build.
|
||||
|
||||
The resolver version can be set in the workspace `Cargo.toml` to remedy this issue.
|
||||
|
||||
```toml
|
||||
[workspace]
|
||||
members = ["member1", "member2"]
|
||||
resolver = "2"
|
||||
```
|
||||
|
||||
@@ -84,7 +84,7 @@ fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
This kind of “provide a signal in a parent, consume it in a child” should be familiar
|
||||
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
|
||||
pattern you use to communicate between parents and children works for grandparents and
|
||||
grandchildren, or any ancestors and descendents: in other words, between “global” state
|
||||
grandchildren, or any ancestors and descendants: in other words, between “global” state
|
||||
in the root component of your app and any other components anywhere else in the app.
|
||||
|
||||
Because of the fine-grained nature of updates, this is usually all you need. However,
|
||||
@@ -122,6 +122,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
provide_context(cx, state);
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Then child components can access “slices” of that state with fine-grained
|
||||
|
||||
@@ -4,6 +4,6 @@ You’ll notice in the `<Suspense/>` example that if you keep reloading the data
|
||||
|
||||
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
|
||||
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data laods. This can be a much better user experience than constantly falling back to a loading message.
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
|
||||
@@ -58,8 +58,8 @@ let id = move || {
|
||||
The untyped versions return `Memo<ParamsMap>`. Again, it’s memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
|
||||
|
||||
```rust
|
||||
let params = use_params::<ContactParams>(cx);
|
||||
let query = use_query::<ContactSearch>(cx);
|
||||
let params = use_params_map(cx);
|
||||
let query = use_query_map(cx);
|
||||
|
||||
// id: || -> Option<String>
|
||||
let id = move || {
|
||||
|
||||
@@ -16,7 +16,7 @@ The Leptos Router works with the path and query (`/blog/search?q=Search`). Given
|
||||
|
||||
## The Philosophy
|
||||
|
||||
In most cases, the path should drive what is displayed on the page. From the user’s perspective, for most appliations, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
|
||||
In most cases, the path should drive what is displayed on the page. From the user’s perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
|
||||
|
||||
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
|
||||
|
||||
|
||||
@@ -107,27 +107,28 @@ fn clear() {
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
We’ll 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 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();
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
```
|
||||
|
||||
You can test individual DOM element attributes or text node values. Sometimes
|
||||
@@ -135,27 +136,27 @@ I like to test the whole view at once. We can do this by testing the element’s
|
||||
`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);
|
||||
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()
|
||||
})
|
||||
);
|
||||
// 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` that’s inside the component.
|
||||
@@ -164,15 +165,14 @@ with the initial value `0`. This is where our wrapping element comes in: I’ll
|
||||
the wrapper’s `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()
|
||||
});
|
||||
}
|
||||
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 it’s useful as you begin to build applications.
|
||||
|
||||
@@ -20,6 +20,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
"Click me: "
|
||||
{move || count()}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So far, this is just the example from the last chapter.
|
||||
|
||||
@@ -24,6 +24,7 @@ view! {
|
||||
max="50"
|
||||
value=double_count
|
||||
/>
|
||||
}
|
||||
```
|
||||
|
||||
But of course, this doesn’t scale very well. If you want to add a third progress
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
|
||||
@@ -198,13 +198,13 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
source.subscribe("message").unwrap().map(|value| {
|
||||
value
|
||||
.expect("no message event")
|
||||
.1
|
||||
.data()
|
||||
.as_string()
|
||||
.expect("expected string value")
|
||||
}),
|
||||
match value {
|
||||
Ok(value) => {
|
||||
value.1.data().as_string().expect("expected string value")
|
||||
},
|
||||
Err(_) => "0".to_string(),
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
on_cleanup(cx, move || source.close());
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
@@ -1,48 +1,44 @@
|
||||
use leptos::{ev, html::*, *};
|
||||
|
||||
pub struct Props {
|
||||
/// The starting value for the counter
|
||||
pub initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
pub step: i32,
|
||||
}
|
||||
|
||||
/// A simple counter view.
|
||||
pub fn view(cx: Scope, props: Props) -> impl IntoView {
|
||||
let Props {
|
||||
initial_value,
|
||||
step,
|
||||
} = props;
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
// elements are created by calling a function with a Scope argument
|
||||
// the function name is the same as the HTML tag name
|
||||
div(cx)
|
||||
.child((
|
||||
cx,
|
||||
// children can be added with .child()
|
||||
// this takes any type that implements IntoView as its argument
|
||||
// for example, a string or an HtmlElement<_>
|
||||
.child(
|
||||
button(cx)
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_value.update(|value| *value = 0))
|
||||
.child((cx, "Clear")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("Clear"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value -= step)
|
||||
})
|
||||
.child((cx, "-1")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("-1"),
|
||||
)
|
||||
.child(
|
||||
span(cx)
|
||||
.child((cx, "Value: "))
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child((cx, move || value.get()))
|
||||
.child((cx, "!")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("!"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value += step)
|
||||
})
|
||||
.child((cx, "+1")),
|
||||
))
|
||||
.child("+1"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
use counter_without_macros as counter;
|
||||
use counter_without_macros::counter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
})
|
||||
mount_to_body(|cx| counter(cx, 0, 1))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -5,6 +5,6 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
|
||||
@@ -70,8 +70,8 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
</p>
|
||||
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
|
||||
<div>
|
||||
// note that the error boundries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribue to the
|
||||
// note that the error boundaries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribute to the
|
||||
// final status code sent by the server when producing ssr pages.
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
|
||||
@@ -9,7 +9,7 @@ leptos = { path = "../../leptos" }
|
||||
reqwasm = "0.5.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
|
||||
@@ -12,7 +12,7 @@ leptos_router = { version = "0.2.0-alpha2", features = ["stable", "csr"] }
|
||||
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
gloo-net = "0.2"
|
||||
gloo-storage = "0.2"
|
||||
serde = "1.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[[proxy]]
|
||||
rewrite = "/api/"
|
||||
backend = "http://0.0.0.0:3000/"
|
||||
backend = "http://127.0.0.1:3000/"
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
web-sys = "0.3"
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
|
||||
@@ -28,19 +28,7 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=""
|
||||
view=move |cx| view! { cx, <ContactList/> }
|
||||
>
|
||||
<Route
|
||||
path=":id"
|
||||
view=move |cx| view! { cx, <Contact/> }
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
view=move |_| view! { cx, <p>"Select a contact."</p> }
|
||||
/>
|
||||
</Route>
|
||||
<ContactRoutes/>
|
||||
<Route
|
||||
path="about"
|
||||
view=move |cx| view! { cx, <About/> }
|
||||
@@ -59,6 +47,27 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
// You can define other routes in their own component.
|
||||
// Use a #[component(transparent)] that returns a <Route/>.
|
||||
#[component(transparent)]
|
||||
pub fn ContactRoutes(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<Route
|
||||
path=""
|
||||
view=move |cx| view! { cx, <ContactList/> }
|
||||
>
|
||||
<Route
|
||||
path=":id"
|
||||
view=move |cx| view! { cx, <Contact/> }
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
view=move |_| view! { cx, <p>"Select a contact."</p> }
|
||||
/>
|
||||
</Route>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
log::debug!("rendering <ContactList/>");
|
||||
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
|
||||
@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
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"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
|
||||
@@ -22,7 +22,7 @@ cfg-if = "1.0"
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
console_log = { version = "0.2", optional = true }
|
||||
console_log = { version = "1", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# dependecies for server (enable when ssr set)
|
||||
|
||||
@@ -16,6 +16,6 @@ gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
console_log = { version = "0.2"}
|
||||
console_log = { version = "1"}
|
||||
console_error_panic_hook = { version = "0.1"}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ actix-files = { version = "0.6.2", optional = true }
|
||||
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
|
||||
anyhow = "1.0.68"
|
||||
broadcaster = "1.0.0"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
futures = "0.3.25"
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", default-features = false }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
uuid = { version = "1", features = ["v4", "js", "serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -20,7 +20,7 @@ use leptos::{
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
@@ -94,7 +94,7 @@ impl ResponseOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
|
||||
/// 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) {
|
||||
@@ -340,14 +340,15 @@ 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 stream will pause at each `<Suspense/>` node and wait for it to resolve befores
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in
|
||||
/// the documentation for that function.
|
||||
/// The HTML stream is rendered using
|
||||
/// [render_to_stream_in_order](leptos::ssr::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:
|
||||
/// ```
|
||||
@@ -409,8 +410,8 @@ where
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to the app’s 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.
|
||||
/// The HTML stream is rendered using [render_to_string_async](leptos::ssr::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:
|
||||
/// ```
|
||||
@@ -728,7 +729,7 @@ async fn stream_app(
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| generate_head_metadata(cx).into(),
|
||||
move |cx| generate_head_metadata_separated(cx).1.into(),
|
||||
additional_context,
|
||||
);
|
||||
|
||||
@@ -745,7 +746,7 @@ async fn stream_app_in_order(
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| {
|
||||
generate_head_metadata(cx).into()
|
||||
generate_head_metadata_separated(cx).1.into()
|
||||
},
|
||||
additional_context,
|
||||
);
|
||||
@@ -762,7 +763,7 @@ async fn build_stream_response(
|
||||
) -> HttpResponse {
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
|
||||
@@ -27,8 +27,8 @@ use leptos::{
|
||||
ssr::*,
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts};
|
||||
use leptos_meta::{generate_head_metadata, MetaContext};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
use leptos_meta::{generate_head_metadata_separated, MetaContext};
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use std::{io, pin::Pin, sync::Arc};
|
||||
@@ -95,7 +95,7 @@ impl ResponseOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
|
||||
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
|
||||
pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
@@ -128,7 +128,7 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
/// and other data without consuming the body. Creates a new Request from the
|
||||
/// original parts for further processsing
|
||||
/// original parts for further processing
|
||||
pub async fn generate_request_and_parts(
|
||||
req: Request<Body>,
|
||||
) -> (Request<Body>, RequestParts) {
|
||||
@@ -147,8 +147,9 @@ pub async fn generate_request_and_parts(
|
||||
(request, request_parts)
|
||||
}
|
||||
|
||||
/// A struct to hold the http::request::Request and allow users to take ownership of it
|
||||
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
|
||||
/// A struct to hold the [`http::request::Request`] and allow users to take ownership of it
|
||||
/// Required by `Request` not being `Clone`. See
|
||||
/// [this issue](https://github.com/hyperium/http/pull/574) for eventual resolution:
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LeptosRequest<B>(Arc<RwLock<Option<Request<B>>>>);
|
||||
|
||||
@@ -158,12 +159,12 @@ impl<B> Clone for LeptosRequest<B> {
|
||||
}
|
||||
}
|
||||
impl<B> LeptosRequest<B> {
|
||||
/// Overwrite the contents of a LeptosRequest with a new Request<B>
|
||||
/// Overwrite the contents of a LeptosRequest with a new `Request<B>`
|
||||
pub fn overwrite(&self, req: Option<Request<B>>) {
|
||||
let mut writable = self.0.write();
|
||||
*writable = req
|
||||
}
|
||||
/// Consume the inner Request<B> inside the LeptosRequest and return it
|
||||
/// Consume the inner `Request<B>` inside the LeptosRequest and return it
|
||||
///```rust, ignore
|
||||
/// use axum::{
|
||||
/// RequestPartsExt,
|
||||
@@ -198,8 +199,9 @@ impl<B> LeptosRequest<B> {
|
||||
}
|
||||
}
|
||||
/// Generate a wrapper for the http::Request::Request type that allows one to
|
||||
/// processs it, access the body, and use axum Extractors on it.
|
||||
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
|
||||
/// process it, access the body, and use axum Extractors on it.
|
||||
/// Required by Request not being Clone. See
|
||||
/// [this issue](https://github.com/hyperium/http/pull/574) for eventual resolution:
|
||||
pub async fn generate_leptos_request<B>(req: Request<B>) -> LeptosRequest<B>
|
||||
where
|
||||
B: Default + std::fmt::Debug,
|
||||
@@ -495,7 +497,7 @@ where
|
||||
|
||||
/// 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
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
@@ -653,7 +655,7 @@ where
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata(cx).into(),
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
@@ -711,7 +713,7 @@ async fn forward_stream(
|
||||
) {
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
_ = tx.send(head).await;
|
||||
let mut shell = Box::pin(bundle);
|
||||
@@ -735,7 +737,7 @@ async fn forward_stream(
|
||||
|
||||
/// 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
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// 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
|
||||
@@ -822,7 +824,7 @@ where
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata(cx).into(),
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
|
||||
@@ -3,25 +3,10 @@ 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");
|
||||
}
|
||||
|
||||
fn autoreload(options: &LeptosOptions) -> String {
|
||||
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() {
|
||||
match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
@@ -52,7 +37,25 @@ pub fn html_parts(
|
||||
leptos_hot_reload::HOT_RELOAD_JS
|
||||
),
|
||||
false => "".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 leptos_autoreload = autoreload(options);
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
@@ -72,6 +75,46 @@ pub fn html_parts(
|
||||
(head, tail)
|
||||
}
|
||||
|
||||
pub fn html_parts_separated(
|
||||
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 leptos_autoreload = autoreload(options);
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let head = meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.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"/>
|
||||
{head}
|
||||
<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,
|
||||
@@ -86,7 +129,7 @@ pub async fn build_async_response(
|
||||
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
html_parts_separated(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);
|
||||
|
||||
@@ -17,8 +17,8 @@ use leptos::{
|
||||
ssr::*,
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts};
|
||||
use leptos_meta::{generate_head_metadata, MetaContext};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
use leptos_meta::{generate_head_metadata_separated, MetaContext};
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
@@ -90,7 +90,7 @@ impl ResponseOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
|
||||
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
|
||||
pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
@@ -385,7 +385,7 @@ where
|
||||
|
||||
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
@@ -536,7 +536,7 @@ where
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata(cx).into(),
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
@@ -593,7 +593,7 @@ async fn forward_stream(
|
||||
) {
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
_ = tx.send(head).await;
|
||||
let mut shell = Box::pin(bundle);
|
||||
@@ -617,7 +617,7 @@ async fn forward_stream(
|
||||
|
||||
/// Returns a Viz [Handler](viz::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
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This version allows us to pass Viz State/Extractor or other infro from Viz or network
|
||||
@@ -700,7 +700,7 @@ where
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata(cx).into(),
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
|
||||
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
|
||||
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
|
||||
//! a fully-functional application that works as intended even before WASM has loaded and begun to run.
|
||||
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite),
|
||||
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum), and
|
||||
//! [`todo_app_sqlite_viz`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_viz)
|
||||
|
||||
@@ -126,7 +126,7 @@ where
|
||||
}
|
||||
);
|
||||
|
||||
// return the fallback for now, wrapped in fragment identifer
|
||||
// return the fallback for now, wrapped in fragment identifier
|
||||
fallback().into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,6 +34,11 @@ leptos = { path = "../leptos" }
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"DocumentFragment",
|
||||
"Element",
|
||||
"HtmlTemplateElement",
|
||||
"NodeList",
|
||||
"Window",
|
||||
"console",
|
||||
"Comment",
|
||||
"Document",
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
@@ -74,13 +74,13 @@ pub trait ElementDescriptor: ElementDescriptorBounds {
|
||||
/// The name of the element, i.e., `div`, `p`, `custom-element`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
|
||||
/// Determains if the tag is void, i.e., `<input>` and `<br>`.
|
||||
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
|
||||
fn is_void(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// A unique `id` that should be generated for each new instance of
|
||||
/// this element, and be consistant for both SSR and CSR.
|
||||
/// this element, and be consistent for both SSR and CSR.
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
fn hydration_id(&self) -> &HydrationKey;
|
||||
}
|
||||
@@ -573,6 +573,23 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks to see if this element is mounted to the DOM as a child
|
||||
/// of `body`.
|
||||
///
|
||||
/// This method will always return [`None`] on non-wasm CSR targets.
|
||||
pub fn is_mounted(&self) -> bool {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
crate::document()
|
||||
.body()
|
||||
.unwrap()
|
||||
.contains(Some(self.element.as_ref()))
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
false
|
||||
}
|
||||
|
||||
/// Adds an attribute to this element.
|
||||
#[track_caller]
|
||||
pub fn attr(
|
||||
@@ -679,6 +696,104 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
this
|
||||
}
|
||||
|
||||
/// Sets the class on the element as the class signal changes.
|
||||
#[track_caller]
|
||||
pub fn dyn_classes<I, C>(
|
||||
self,
|
||||
classes_signal: impl Fn() -> I + 'static,
|
||||
) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = C>,
|
||||
C: Into<Cow<'static, str>>,
|
||||
{
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
use smallvec::SmallVec;
|
||||
|
||||
let class_list = self.element.as_ref().class_list();
|
||||
|
||||
leptos_reactive::create_effect(
|
||||
self.cx,
|
||||
move |prev_classes: Option<
|
||||
SmallVec<[Cow<'static, str>; 4]>,
|
||||
>| {
|
||||
let classes = classes_signal()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
|
||||
);
|
||||
|
||||
let mut new_classes = classes
|
||||
.iter()
|
||||
.flat_map(|classes| classes.split_whitespace());
|
||||
|
||||
if let Some(prev_classes) = prev_classes {
|
||||
let mut old_classes = prev_classes
|
||||
.iter()
|
||||
.flat_map(|classes| classes.split_whitespace());
|
||||
|
||||
// Remove old classes
|
||||
for prev_class in old_classes.clone() {
|
||||
if !new_classes.any(|c| c == prev_class) {
|
||||
class_list.remove_1(prev_class).unwrap_or_else(
|
||||
|err| {
|
||||
panic!(
|
||||
"failed to add class \
|
||||
`{prev_class}`, error: {err:#?}"
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new classes
|
||||
for class in new_classes {
|
||||
if !old_classes.any(|c| c == class) {
|
||||
class_list.add_1(class).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to remove class `{class}`, \
|
||||
error: {err:#?}"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let new_classes = new_classes
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
|
||||
for class in &new_classes {
|
||||
class_list.add_1(class).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to add class `{class}`, error: \
|
||||
{err:#?}"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
classes
|
||||
},
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
classes_signal()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.flat_map(|classes| {
|
||||
classes
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect::<SmallVec<[_; 4]>>()
|
||||
})
|
||||
.fold(self, |this, class| this.class(class, true))
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a property on an element.
|
||||
#[track_caller]
|
||||
pub fn prop(
|
||||
|
||||
@@ -94,12 +94,7 @@ pub(crate) fn property_helper(
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
if old.as_ref() != Some(&new)
|
||||
&& !(old.is_none()
|
||||
&& new == wasm_bindgen::JsValue::UNDEFINED)
|
||||
{
|
||||
property_expression(&el, prop_name, new.clone())
|
||||
}
|
||||
property_expression(&el, prop_name, new.clone());
|
||||
new
|
||||
});
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
let runtime = create_runtime();
|
||||
|
||||
let (
|
||||
(shell, prefix, pending_resources, pending_fragments, serializers),
|
||||
(shell, pending_resources, pending_fragments, serializers),
|
||||
scope,
|
||||
disposer,
|
||||
) = run_scope_undisposed(runtime, {
|
||||
@@ -146,27 +146,74 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
|
||||
let resources = cx.pending_resources();
|
||||
let pending_resources = serde_json::to_string(&resources).unwrap();
|
||||
let prefix = prefix(cx);
|
||||
|
||||
(
|
||||
shell,
|
||||
prefix,
|
||||
pending_resources,
|
||||
cx.pending_fragments(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
}
|
||||
});
|
||||
let cx = Scope { runtime, id: scope };
|
||||
|
||||
let blocking_fragments = FuturesUnordered::new();
|
||||
let fragments = FuturesUnordered::new();
|
||||
for (fragment_id, (fut, _)) in pending_fragments {
|
||||
fragments.push(async move { (fragment_id, fut.await) })
|
||||
|
||||
for (fragment_id, data) in pending_fragments {
|
||||
if data.should_block {
|
||||
blocking_fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.await) });
|
||||
} else {
|
||||
fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.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_to_chunks(fragments);
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(serializers);
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments = fragments_to_chunks(blocking_fragments);
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
{blocking}
|
||||
"#
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources)
|
||||
// dispose of the root scope
|
||||
.chain(futures::stream::once(async move {
|
||||
disposer.dispose();
|
||||
Default::default()
|
||||
}));
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
fn fragments_to_chunks(
|
||||
fragments: impl Stream<Item = (String, String)>,
|
||||
) -> impl Stream<Item = String> {
|
||||
fragments.map(|(fragment_id, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
@@ -191,35 +238,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
});
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(serializers);
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources)
|
||||
// dispose of the root scope
|
||||
.chain(futures::stream::once(async move {
|
||||
disposer.dispose();
|
||||
Default::default()
|
||||
}));
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
impl View {
|
||||
@@ -509,12 +528,14 @@ pub(crate) fn render_serializers(
|
||||
) -> impl Stream<Item = String> {
|
||||
serializers.map(|(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
let json = json.replace('<', "\\u003c");
|
||||
format!(
|
||||
r#"<script>
|
||||
var val = {json:?};
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
|
||||
}} else {{
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, val);
|
||||
}}
|
||||
</script>"#,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ use leptos_reactive::{
|
||||
create_runtime, run_scope_undisposed, suspense::StreamChunk, RuntimeId,
|
||||
Scope, ScopeId,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, collections::VecDeque};
|
||||
|
||||
/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s
|
||||
/// loaded in `<Suspense/>` elements have finished loading.
|
||||
@@ -80,29 +80,48 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
|
||||
let ((chunks, prefix, pending_resources, serializers), scope_id, disposer) =
|
||||
run_scope_undisposed(runtime, |cx| {
|
||||
// add additional context
|
||||
additional_context(cx);
|
||||
let (
|
||||
(
|
||||
blocking_fragments_ready,
|
||||
chunks,
|
||||
prefix,
|
||||
pending_resources,
|
||||
serializers,
|
||||
),
|
||||
scope_id,
|
||||
disposer,
|
||||
) = run_scope_undisposed(runtime, |cx| {
|
||||
// add additional context
|
||||
additional_context(cx);
|
||||
|
||||
// render view and return chunks
|
||||
let view = view(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(),
|
||||
)
|
||||
});
|
||||
(
|
||||
cx.blocking_fragments_ready(),
|
||||
view.into_stream_chunks(cx),
|
||||
prefix,
|
||||
serde_json::to_string(&cx.pending_resources()).unwrap(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
});
|
||||
let cx = Scope {
|
||||
runtime,
|
||||
id: scope_id,
|
||||
};
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
let (prefix_tx, prefix_rx) = futures::channel::oneshot::channel();
|
||||
leptos_reactive::spawn_local(async move {
|
||||
handle_chunks(tx, chunks).await;
|
||||
blocking_fragments_ready.await;
|
||||
let remaining_chunks = handle_blocking_chunks(tx.clone(), chunks).await;
|
||||
let prefix = prefix(cx);
|
||||
prefix_tx.send(prefix).expect("to send prefix");
|
||||
handle_chunks(tx, remaining_chunks).await;
|
||||
});
|
||||
|
||||
let stream = futures::stream::once(async move {
|
||||
let prefix = prefix_rx.await.expect("to receive prefix");
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
@@ -126,18 +145,61 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_chunks(tx: UnboundedSender<String>, chunks: Vec<StreamChunk>) {
|
||||
async fn handle_blocking_chunks(
|
||||
tx: UnboundedSender<String>,
|
||||
mut queued_chunks: VecDeque<StreamChunk>,
|
||||
) -> VecDeque<StreamChunk> {
|
||||
let mut buffer = String::new();
|
||||
while let Some(chunk) = queued_chunks.pop_front() {
|
||||
match chunk {
|
||||
StreamChunk::Sync(sync) => buffer.push_str(&sync),
|
||||
StreamChunk::Async {
|
||||
chunks,
|
||||
should_block,
|
||||
} => {
|
||||
if should_block {
|
||||
// add static HTML before the Suspense and stream it down
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send async HTML chunk");
|
||||
|
||||
// send the inner stream
|
||||
let suspended = chunks.await;
|
||||
handle_blocking_chunks(tx.clone(), suspended).await;
|
||||
} else {
|
||||
// TODO: should probably first check if there are any *other* blocking chunks
|
||||
queued_chunks.push_front(StreamChunk::Async {
|
||||
chunks,
|
||||
should_block: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send final sync chunk
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send final HTML chunk");
|
||||
|
||||
queued_chunks
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_chunks(
|
||||
tx: UnboundedSender<String>,
|
||||
chunks: VecDeque<StreamChunk>,
|
||||
) {
|
||||
let mut buffer = String::new();
|
||||
for chunk in chunks {
|
||||
match chunk {
|
||||
StreamChunk::Sync(sync) => buffer.push_str(&sync),
|
||||
StreamChunk::Async(suspended) => {
|
||||
StreamChunk::Async { chunks, .. } => {
|
||||
// add static HTML before the Suspense and stream it down
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send async HTML chunk");
|
||||
|
||||
// send the inner stream
|
||||
let suspended = suspended.await;
|
||||
let suspended = chunks.await;
|
||||
handle_chunks(tx.clone(), suspended).await;
|
||||
}
|
||||
}
|
||||
@@ -149,8 +211,8 @@ async fn handle_chunks(tx: UnboundedSender<String>, chunks: Vec<StreamChunk>) {
|
||||
|
||||
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();
|
||||
pub fn into_stream_chunks(self, cx: Scope) -> VecDeque<StreamChunk> {
|
||||
let mut chunks = VecDeque::new();
|
||||
self.into_stream_chunks_helper(cx, &mut chunks);
|
||||
chunks
|
||||
}
|
||||
@@ -158,37 +220,42 @@ impl View {
|
||||
fn into_stream_chunks_helper(
|
||||
self,
|
||||
cx: Scope,
|
||||
chunks: &mut Vec<StreamChunk>,
|
||||
chunks: &mut VecDeque<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));
|
||||
if let Some(data) = cx.take_pending_fragment(&id) {
|
||||
chunks.push_back(StreamChunk::Async {
|
||||
chunks: data.in_order,
|
||||
should_block: data.should_block,
|
||||
});
|
||||
}
|
||||
}
|
||||
View::Text(node) => chunks.push(StreamChunk::Sync(node.content)),
|
||||
View::Text(node) => {
|
||||
chunks.push_back(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()));
|
||||
chunks.push_back(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()));
|
||||
chunks.push_back(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()))
|
||||
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}-->"#, HydrationCtx::to_string(&node.id, true)).into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(id) = &el.view_marker {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<!--leptos-view|{id}|open-->").into(),
|
||||
));
|
||||
}
|
||||
@@ -196,7 +263,7 @@ impl View {
|
||||
for chunk in el_chunks {
|
||||
match chunk {
|
||||
StringOrView::String(string) => {
|
||||
chunks.push(StreamChunk::Sync(string))
|
||||
chunks.push_back(StreamChunk::Sync(string))
|
||||
}
|
||||
StringOrView::View(view) => {
|
||||
view().into_stream_chunks_helper(cx, chunks);
|
||||
@@ -232,18 +299,18 @@ impl View {
|
||||
.join("");
|
||||
|
||||
if el.is_void {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<{tag_name}{attrs}/>").into(),
|
||||
));
|
||||
} else if let Some(inner_html) = inner_html {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!(
|
||||
"<{tag_name}{attrs}>{inner_html}</{tag_name}>"
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
} else {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<{tag_name}{attrs}>").into(),
|
||||
));
|
||||
|
||||
@@ -255,20 +322,20 @@ impl View {
|
||||
}
|
||||
}
|
||||
ElementChildren::InnerHtml(inner_html) => {
|
||||
chunks.push(StreamChunk::Sync(inner_html));
|
||||
chunks.push_back(StreamChunk::Sync(inner_html));
|
||||
}
|
||||
// handled above
|
||||
ElementChildren::Chunks(_) => unreachable!(),
|
||||
}
|
||||
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("</{tag_name}>").into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(id) = &el.view_marker {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<!--leptos-view|{id}|close-->").into(),
|
||||
));
|
||||
}
|
||||
@@ -280,10 +347,10 @@ impl View {
|
||||
u.id.clone(),
|
||||
"",
|
||||
false,
|
||||
Box::new(move |chunks: &mut Vec<StreamChunk>| {
|
||||
Box::new(move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-unit-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
@@ -293,7 +360,7 @@ impl View {
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
@@ -301,7 +368,7 @@ impl View {
|
||||
.into(),
|
||||
));
|
||||
})
|
||||
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
@@ -309,34 +376,39 @@ impl View {
|
||||
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)
|
||||
Box::new(
|
||||
move |chunks: &mut VecDeque<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_back(
|
||||
if !cfg!(debug_assertions) {
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!>{}",
|
||||
t.content
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(t.content)
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
);
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(t.content)
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
|
||||
},
|
||||
)
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
@@ -345,33 +417,40 @@ impl View {
|
||||
node.id,
|
||||
"each",
|
||||
true,
|
||||
Box::new(move |chunks: &mut Vec<StreamChunk>| {
|
||||
for node in children.into_iter().flatten() {
|
||||
let id = node.id;
|
||||
Box::new(
|
||||
move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
for node in children.into_iter().flatten() {
|
||||
let id = node.id;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!(
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push_back(
|
||||
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!(
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
node.child
|
||||
.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
);
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-end-->",
|
||||
HydrationCtx::to_string(&id, true)
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
|
||||
},
|
||||
)
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -379,13 +458,13 @@ impl View {
|
||||
if wrap {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
chunks.push(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-start-->", HydrationCtx::to_string(&id, false)).into()));
|
||||
chunks.push_back(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()));
|
||||
chunks.push_back(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()))
|
||||
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}-->", HydrationCtx::to_string(&id, true)).into()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -43,7 +43,7 @@ impl Parse for Model {
|
||||
"this method requires a `Scope` parameter";
|
||||
help = "try `fn {}(cx: Scope, /* ... */)`", item.sig.ident
|
||||
);
|
||||
} else if props[0].ty != parse_quote!(Scope) {
|
||||
} else if !is_valid_scope_type(&props[0].ty) {
|
||||
abort!(
|
||||
item.sig.inputs,
|
||||
"this method requires a `Scope` parameter";
|
||||
@@ -68,7 +68,7 @@ impl Parse for Model {
|
||||
});
|
||||
|
||||
// Make sure return type is correct
|
||||
if item.sig.output != parse_quote!(-> impl IntoView) {
|
||||
if !is_valid_into_view_return_type(&item.sig.output) {
|
||||
abort!(
|
||||
item.sig,
|
||||
"return type is incorrect";
|
||||
@@ -130,6 +130,7 @@ impl ToTokens for Model {
|
||||
let mut body = body.to_owned();
|
||||
|
||||
body.sig.ident = format_ident!("__{}", body.sig.ident);
|
||||
#[allow(clippy::redundant_clone)] // false positive
|
||||
let body_name = body.sig.ident.clone();
|
||||
|
||||
let (_, generics, where_clause) = body.sig.generics.split_for_impl();
|
||||
@@ -153,6 +154,7 @@ impl ToTokens for Model {
|
||||
if cfg!(feature = "tracing") {
|
||||
(
|
||||
quote! {
|
||||
#[allow(clippy::let_with_type_underscore)]
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
|
||||
@@ -204,7 +206,7 @@ impl ToTokens for Model {
|
||||
#tracing_instrument_attr
|
||||
#vis fn #name #generics (
|
||||
#[allow(unused_variables)]
|
||||
#scope_name: Scope,
|
||||
#scope_name: ::leptos::Scope,
|
||||
props: #props_name #generics
|
||||
) #ret #(+ #lifetimes)*
|
||||
#where_clause
|
||||
@@ -434,7 +436,7 @@ impl ToTokens for TypedBuilderOpts {
|
||||
fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
props
|
||||
.iter()
|
||||
.filter(|Prop { ty, .. }| *ty != parse_quote!(Scope))
|
||||
.filter(|Prop { ty, .. }| !is_valid_scope_type(ty))
|
||||
.map(|prop| {
|
||||
let Prop {
|
||||
docs,
|
||||
@@ -461,7 +463,7 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
fn prop_names(props: &[Prop]) -> TokenStream {
|
||||
props
|
||||
.iter()
|
||||
.filter(|Prop { ty, .. }| *ty != parse_quote!(Scope))
|
||||
.filter(|Prop { ty, .. }| !is_valid_scope_type(ty))
|
||||
.map(|Prop { name, .. }| quote! { #name, })
|
||||
.collect()
|
||||
}
|
||||
@@ -640,3 +642,23 @@ fn prop_to_doc(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_scope_type(ty: &Type) -> bool {
|
||||
[
|
||||
parse_quote!(Scope),
|
||||
parse_quote!(leptos::Scope),
|
||||
parse_quote!(::leptos::Scope),
|
||||
]
|
||||
.iter()
|
||||
.any(|test| ty == test)
|
||||
}
|
||||
|
||||
fn is_valid_into_view_return_type(ty: &ReturnType) -> bool {
|
||||
[
|
||||
parse_quote!(-> impl IntoView),
|
||||
parse_quote!(-> impl leptos::IntoView),
|
||||
parse_quote!(-> impl ::leptos::IntoView),
|
||||
]
|
||||
.iter()
|
||||
.any(|test| ty == test)
|
||||
}
|
||||
|
||||
@@ -201,13 +201,15 @@ mod template;
|
||||
/// ```
|
||||
///
|
||||
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
|
||||
/// [NodeRef](leptos_dom::NodeRef) to use later.
|
||||
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// use leptos::html::Input;
|
||||
///
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
/// let my_input = NodeRef::new(cx);
|
||||
/// let my_input = create_node_ref::<Input>(cx);
|
||||
/// view! { cx, <input type="text" _ref=my_input/> }
|
||||
/// // `my_input` now contains an `Element` that we can use anywhere
|
||||
/// # ;
|
||||
@@ -399,9 +401,9 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// The `#[component]` macro allows you to annotate plain Rust functions as components
|
||||
/// and use them within your Leptos [view](crate::view!) as if they were custom HTML elements. The
|
||||
/// component function takes a [Scope](leptos_reactive::Scope) and any number of other arguments.
|
||||
/// When you use the component somewhere else, the names of its arguments are the names
|
||||
/// of the properties you use in the [view](crate::view!) macro.
|
||||
/// component function takes a [Scope](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||
/// and any number of other arguments. When you use the component somewhere else,
|
||||
/// the names of its arguments are the names of the properties you use in the [view](crate::view!) macro.
|
||||
///
|
||||
/// Every component function should have the return type `-> impl IntoView`.
|
||||
///
|
||||
@@ -576,8 +578,10 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
/// You can use the `#[prop]` attribute on individual component properties (function arguments) to
|
||||
/// customize the types that component property can receive. You can use the following attributes:
|
||||
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
|
||||
/// you could apply `#[prop(into)]` to a prop that takes [Signal](leptos_reactive::Signal), which would
|
||||
/// allow users to pass a [ReadSignal](leptos_reactive::ReadSignal) or [RwSignal](leptos_reactive::RwSignal)
|
||||
/// you could apply `#[prop(into)]` to a prop that takes
|
||||
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
|
||||
/// allow users to pass a [ReadSignal](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) or
|
||||
/// [RwSignal](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html)
|
||||
/// and automatically convert it.)
|
||||
/// * `#[prop(optional)]`: If the user does not specify this property when they use the component,
|
||||
/// it will be set to its default value. If the property type is `Option<T>`, values should be passed
|
||||
@@ -640,8 +644,8 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Declares that a function is a [server function](leptos_server). This means that
|
||||
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
/// Declares that a function is a [server function](https://docs.rs/server_fn/latest/server_fn/index.html).
|
||||
/// This means that its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
///
|
||||
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
|
||||
/// are enabled), it will instead make a network request to the server.
|
||||
@@ -657,7 +661,8 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// work without WebAssembly, the encoding must be `"Url"`.
|
||||
///
|
||||
/// The server function itself can take any number of arguments, each of which should be serializable
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope),
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos
|
||||
/// [Scope](https://docs.rs/leptos/latest/leptos/struct.Scope.html),
|
||||
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
|
||||
/// server-side context into the server function.
|
||||
///
|
||||
@@ -680,7 +685,7 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must be [Serializable](leptos_reactive::Serializable).**
|
||||
/// - **Return types must be [Serializable](https://docs.rs/leptos/latest/leptos/trait.Serializable.html).**
|
||||
/// 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 [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
@@ -688,8 +693,8 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// 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
|
||||
/// can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
|
||||
/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a Leptos `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.
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
|
||||
@@ -365,6 +365,7 @@ enum SsrElementChunks {
|
||||
View(TokenStream),
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn element_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
@@ -817,7 +818,22 @@ fn element_to_tokens(
|
||||
};
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
Some(attribute_to_tokens(cx, node))
|
||||
if node.key.to_string().trim().starts_with("class:") {
|
||||
None
|
||||
} else {
|
||||
Some(attribute_to_tokens(cx, node))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let class_attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
if node.key.to_string().trim().starts_with("class:") {
|
||||
Some(attribute_to_tokens(cx, node))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -875,6 +891,7 @@ fn element_to_tokens(
|
||||
quote! {
|
||||
#name
|
||||
#(#attrs)*
|
||||
#(#class_attrs)*
|
||||
#global_class_expr
|
||||
#(#children)*
|
||||
#view_marker
|
||||
@@ -1375,7 +1392,7 @@ fn is_math_ml_element(tag: &str) -> bool {
|
||||
}
|
||||
|
||||
fn is_ambiguous_element(tag: &str) -> bool {
|
||||
tag == "a" || tag == "script"
|
||||
tag == "a" || tag == "script" || tag == "title"
|
||||
}
|
||||
|
||||
fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
|
||||
@@ -20,7 +20,7 @@ fn Component(
|
||||
#[test]
|
||||
fn component() {
|
||||
let cp = ComponentProps::builder().into("").strip_option(9).build();
|
||||
assert_eq!(cp.optional, false);
|
||||
assert!(!cp.optional);
|
||||
assert_eq!(cp.optional_no_strip, None);
|
||||
assert_eq!(cp.strip_option, Some(9));
|
||||
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/ui/*.rs");
|
||||
t.compile_fail("tests/ui/component.rs");
|
||||
t.compile_fail("tests/ui/component_absolute.rs");
|
||||
}
|
||||
|
||||
52
leptos_macro/tests/ui/component_absolute.rs
Normal file
52
leptos_macro/tests/ui/component_absolute.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
#[::leptos::component]
|
||||
fn missing_scope() {}
|
||||
|
||||
#[::leptos::component]
|
||||
fn missing_return_type(cx: ::leptos::Scope) {}
|
||||
|
||||
#[::leptos::component]
|
||||
fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
|
||||
|
||||
#[::leptos::component]
|
||||
fn optional_and_optional_no_strip(
|
||||
cx: Scope,
|
||||
#[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn optional_and_strip_option(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(optional, strip_option)] conflicting: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn optional_no_strip_and_strip_option(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn default_without_value(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(default)] default: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn default_with_invalid_value(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(default= |)] default: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
pub fn using_the_view_macro(cx: ::leptos::Scope) -> impl ::leptos::IntoView {
|
||||
::leptos::view! { cx,
|
||||
"ok"
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
53
leptos_macro/tests/ui/component_absolute.stderr
Normal file
53
leptos_macro/tests/ui/component_absolute.stderr
Normal file
@@ -0,0 +1,53 @@
|
||||
error: this method requires a `Scope` parameter
|
||||
--> tests/ui/component_absolute.rs:2:1
|
||||
|
|
||||
2 | fn missing_scope() {}
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: try `fn missing_scope(cx: Scope, /* ... */)`
|
||||
|
||||
error: return type is incorrect
|
||||
--> tests/ui/component_absolute.rs:5:1
|
||||
|
|
||||
5 | fn missing_return_type(cx: ::leptos::Scope) {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: return signature must be `-> impl IntoView`
|
||||
|
||||
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default` and `into`
|
||||
--> tests/ui/component_absolute.rs:8:52
|
||||
|
|
||||
8 | fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
|
||||
| ^^^^^
|
||||
|
||||
error: `optional` conflicts with mutually exclusive `optional_no_strip`
|
||||
--> tests/ui/component_absolute.rs:13:12
|
||||
|
|
||||
13 | #[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: `optional` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component_absolute.rs:20:12
|
||||
|
|
||||
20 | #[prop(optional, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component_absolute.rs:27:12
|
||||
|
|
||||
27 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: unexpected end of input, expected assignment `=`
|
||||
--> tests/ui/component_absolute.rs:34:19
|
||||
|
|
||||
34 | #[prop(default)] default: bool,
|
||||
| ^
|
||||
|
||||
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
|
||||
= help: try `#[prop(default=5 * 10)]`
|
||||
--> tests/ui/component_absolute.rs:41:22
|
||||
|
|
||||
41 | #[prop(default= |)] default: bool,
|
||||
| ^
|
||||
@@ -10,9 +10,9 @@ description = "Reactive system for the Leptos web framework."
|
||||
[dependencies]
|
||||
slotmap = { version = "1", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde-lite = { version = "0.3", optional = true }
|
||||
serde-lite = { version = "0.4", optional = true }
|
||||
futures = { version = "0.3" }
|
||||
js-sys = "0.3"
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
miniserde = { version = "0.1", optional = true }
|
||||
rkyv = { version = "0.7.39", features = [
|
||||
"validation",
|
||||
@@ -31,17 +31,17 @@ base64 = "0.21"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["rt"], optional = true }
|
||||
tracing = "0.1"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = [
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4", optional = true }
|
||||
web-sys = { version = "0.3", optional = true, features = [
|
||||
"DocumentFragment",
|
||||
"Element",
|
||||
"HtmlTemplateElement",
|
||||
"NodeList",
|
||||
"Window",
|
||||
] }
|
||||
cfg-if = "1.0.0"
|
||||
indexmap = "1.9.2"
|
||||
cfg-if = "1"
|
||||
indexmap = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4"
|
||||
@@ -50,8 +50,18 @@ leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
csr = []
|
||||
hydrate = []
|
||||
csr = [
|
||||
"dep:js-sys",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:wasm-bindgen-futures",
|
||||
"dep:web-sys",
|
||||
]
|
||||
hydrate = [
|
||||
"dep:js-sys",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:wasm-bindgen-futures",
|
||||
"dep:web-sys",
|
||||
]
|
||||
ssr = ["dep:tokio"]
|
||||
stable = []
|
||||
serde = []
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{runtime::PinnedFuture, suspense::StreamChunk, ResourceId};
|
||||
use cfg_if::cfg_if;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
pub struct SharedContext {
|
||||
pub events: Vec<()>,
|
||||
pub pending_resources: HashSet<ResourceId>,
|
||||
pub resolved_resources: HashMap<ResourceId, String>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
// 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>>)>,
|
||||
pub pending_fragments: HashMap<String, FragmentData>,
|
||||
}
|
||||
|
||||
/// Represents its pending `<Suspense/>` fragment.
|
||||
pub struct FragmentData {
|
||||
/// Future that represents how it should be render for an out-of-order stream.
|
||||
pub out_of_order: PinnedFuture<String>,
|
||||
/// Future that represents how it should be render for an in-order stream.
|
||||
pub in_order: PinnedFuture<VecDeque<StreamChunk>>,
|
||||
/// Whether the stream should wait for this fragment before sending any data.
|
||||
pub should_block: bool,
|
||||
/// Future that will resolve when the fragment is ready.
|
||||
pub is_ready: Option<PinnedFuture<()>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SharedContext {
|
||||
|
||||
@@ -191,7 +191,8 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
)]
|
||||
fn get_untracked(&self) -> T {
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
match self.id.try_with_no_subscription(runtime, T::clone) {
|
||||
let f = move |maybe_value: &Option<T>| maybe_value.clone().unwrap();
|
||||
match self.id.try_with_no_subscription(runtime, f) {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
@@ -105,6 +106,73 @@ pub fn create_resource_with_initial_value<S, T, Fu>(
|
||||
fetcher: impl Fn(S) -> Fu + 'static,
|
||||
initial_value: Option<T>,
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
create_resource_helper(
|
||||
cx,
|
||||
source,
|
||||
fetcher,
|
||||
initial_value,
|
||||
ResourceSerialization::Serializable,
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a “blocking” [Resource](crate::Resource). When server-side rendering is used,
|
||||
/// this resource will cause any `<Suspense/>` you read it under to block the initial
|
||||
/// chunk of HTML from being sent to the client. This means that if you set things like
|
||||
/// HTTP headers or `<head>` metadata in that `<Suspense/>`, that header material will
|
||||
/// be included in the server’s original response.
|
||||
///
|
||||
/// This causes a slow time to first byte (TTFB) but is very useful for loading data that
|
||||
/// is essential to the first load. For example, a blog post page that needs to include
|
||||
/// the title of the blog post in the page’s initial HTML `<title>` tag for SEO reasons
|
||||
/// might use a blocking resource to load blog post metadata, which will prevent the page from
|
||||
/// returning until that data has loaded.
|
||||
///
|
||||
/// **Note**: This is not “blocking” in the sense that it blocks the current thread. Rather,
|
||||
/// it is blocking in the sense that it blocks the server from sending a response.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>(),
|
||||
signal_ty = %std::any::type_name::<S>(),
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_blocking_resource<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: impl Fn() -> S + 'static,
|
||||
fetcher: impl Fn(S) -> Fu + 'static,
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
create_resource_helper(
|
||||
cx,
|
||||
source,
|
||||
fetcher,
|
||||
None,
|
||||
ResourceSerialization::Blocking,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_resource_helper<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: impl Fn() -> S + 'static,
|
||||
fetcher: impl Fn(S) -> Fu + 'static,
|
||||
initial_value: Option<T>,
|
||||
serializable: ResourceSerialization,
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
@@ -131,7 +199,7 @@ where
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable: true,
|
||||
serializable,
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
@@ -255,7 +323,7 @@ where
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable: false,
|
||||
serializable: ResourceSerialization::Local,
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
@@ -377,13 +445,15 @@ where
|
||||
///
|
||||
/// If you want to get the value without cloning it, use [Resource::with].
|
||||
/// (`value.read(cx)` is equivalent to `value.with(cx, T::clone)`.)
|
||||
#[track_caller]
|
||||
pub fn read(&self, cx: Scope) -> Option<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.read(cx)
|
||||
resource.read(cx, location)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
@@ -397,10 +467,12 @@ where
|
||||
///
|
||||
/// If you want to get the value by cloning it, you can use
|
||||
/// [Resource::read].
|
||||
#[track_caller]
|
||||
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
let location = std::panic::Location::caller();
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.with(cx, f)
|
||||
resource.with(cx, f, location)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
@@ -555,7 +627,19 @@ where
|
||||
resolved: Rc<Cell<bool>>,
|
||||
scheduled: Rc<Cell<bool>>,
|
||||
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
|
||||
serializable: bool,
|
||||
serializable: ResourceSerialization,
|
||||
}
|
||||
|
||||
/// Whether and how the resource can be serialized.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum ResourceSerialization {
|
||||
/// Not serializable.
|
||||
Local,
|
||||
/// Can be serialized.
|
||||
Serializable,
|
||||
/// Can be serialized, and cause the first chunk to be blocked until
|
||||
/// their suspense has resolved.
|
||||
Blocking,
|
||||
}
|
||||
|
||||
impl<S, T> ResourceState<S, T>
|
||||
@@ -563,14 +647,25 @@ where
|
||||
S: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
pub fn read(&self, cx: Scope) -> Option<T>
|
||||
#[track_caller]
|
||||
pub fn read(
|
||||
&self,
|
||||
cx: Scope,
|
||||
location: &'static Location<'static>,
|
||||
) -> Option<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.with(cx, T::clone)
|
||||
self.with(cx, T::clone, location)
|
||||
}
|
||||
|
||||
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
#[track_caller]
|
||||
pub fn with<U>(
|
||||
&self,
|
||||
cx: Scope,
|
||||
f: impl FnOnce(&T) -> U,
|
||||
location: &'static Location<'static>,
|
||||
) -> Option<U> {
|
||||
let suspense_cx = use_context::<SuspenseContext>(cx);
|
||||
|
||||
let v = self
|
||||
@@ -584,9 +679,26 @@ where
|
||||
|
||||
let serializable = self.serializable;
|
||||
if let Some(suspense_cx) = &suspense_cx {
|
||||
if serializable {
|
||||
if serializable != ResourceSerialization::Local {
|
||||
suspense_cx.has_local_only.set_value(false);
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(all(feature = "hydrate", debug_assertions)))]
|
||||
{
|
||||
_ = location;
|
||||
}
|
||||
#[cfg(all(feature = "hydrate", debug_assertions))]
|
||||
crate::macros::debug_warn!(
|
||||
"At {location}, you are reading a resource in `hydrate` mode \
|
||||
outside a <Suspense/> or <Transition/>. This can cause \
|
||||
hydration mismatch errors and loses out on a significant \
|
||||
performance optimization. To fix this issue, you can either: \
|
||||
\n1. Wrap the place where you read the resource in a \
|
||||
<Suspense/> or <Transition/> component, or \n2. Switch to \
|
||||
using create_local_resource(), which will wait to load the \
|
||||
resource until the app is hydrated on the client side. (This \
|
||||
will have worse performance in most cases.)",
|
||||
);
|
||||
}
|
||||
|
||||
let increment = move |_: Option<()>| {
|
||||
@@ -600,7 +712,12 @@ where
|
||||
// because the context has been tracked here
|
||||
// on the first read, resource is already loading without having incremented
|
||||
if !has_value {
|
||||
s.increment(serializable);
|
||||
s.increment(
|
||||
serializable != ResourceSerialization::Local,
|
||||
);
|
||||
if serializable == ResourceSerialization::Blocking {
|
||||
s.should_block.set_value(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -641,7 +758,12 @@ where
|
||||
let suspense_contexts = self.suspense_contexts.clone();
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
suspense_context.increment(self.serializable);
|
||||
suspense_context.increment(
|
||||
self.serializable != ResourceSerialization::Local,
|
||||
);
|
||||
if self.serializable == ResourceSerialization::Blocking {
|
||||
suspense_context.should_block.set_value(true);
|
||||
}
|
||||
}
|
||||
|
||||
// run the Future
|
||||
@@ -659,7 +781,9 @@ where
|
||||
set_loading.update(|n| *n = false);
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
suspense_context.decrement(serializable);
|
||||
suspense_context.decrement(
|
||||
serializable != ResourceSerialization::Local,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
console_warn,
|
||||
hydration::FragmentData,
|
||||
node::NodeId,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
suspense::StreamChunk,
|
||||
PinnedFuture, ResourceId, StoredValueId, SuspenseContext,
|
||||
};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use std::{collections::HashMap, fmt};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fmt,
|
||||
};
|
||||
|
||||
#[doc(hidden)]
|
||||
#[must_use = "Scope will leak memory if the disposer function is never called"]
|
||||
@@ -374,7 +378,7 @@ impl Scope {
|
||||
context: SuspenseContext,
|
||||
key: &str,
|
||||
out_of_order_resolver: impl FnOnce() -> String + 'static,
|
||||
in_order_resolver: impl FnOnce() -> Vec<StreamChunk> + 'static,
|
||||
in_order_resolver: impl FnOnce() -> VecDeque<StreamChunk> + 'static,
|
||||
) {
|
||||
use crate::create_isomorphic_effect;
|
||||
use futures::StreamExt;
|
||||
@@ -383,6 +387,7 @@ impl Scope {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
let (tx1, mut rx1) = futures::channel::mpsc::unbounded();
|
||||
let (tx2, mut rx2) = futures::channel::mpsc::unbounded();
|
||||
let (tx3, mut rx3) = futures::channel::mpsc::unbounded();
|
||||
|
||||
create_isomorphic_effect(*self, move |_| {
|
||||
let pending = context
|
||||
@@ -393,33 +398,35 @@ impl Scope {
|
||||
if pending == 0 {
|
||||
_ = tx1.unbounded_send(());
|
||||
_ = tx2.unbounded_send(());
|
||||
_ = tx3.unbounded_send(());
|
||||
}
|
||||
});
|
||||
|
||||
shared_context.pending_fragments.insert(
|
||||
key.to_string(),
|
||||
(
|
||||
Box::pin(async move {
|
||||
FragmentData {
|
||||
out_of_order: Box::pin(async move {
|
||||
rx1.next().await;
|
||||
out_of_order_resolver()
|
||||
}),
|
||||
Box::pin(async move {
|
||||
in_order: Box::pin(async move {
|
||||
rx2.next().await;
|
||||
in_order_resolver()
|
||||
}),
|
||||
),
|
||||
should_block: context.should_block(),
|
||||
is_ready: Some(Box::pin(async move {
|
||||
rx3.next().await;
|
||||
})),
|
||||
},
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
/// The set of all HTML fragments currently pending.
|
||||
///
|
||||
/// The keys are hydration IDs. Valeus are tuples of two pinned
|
||||
/// The keys are hydration IDs. Values 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, (PinnedFuture<String>, PinnedFuture<Vec<StreamChunk>>)>
|
||||
{
|
||||
pub fn pending_fragments(&self) -> HashMap<String, FragmentData> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
std::mem::take(&mut shared_context.pending_fragments)
|
||||
@@ -427,14 +434,31 @@ impl Scope {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A future that will resolve when all blocking fragments are ready.
|
||||
pub fn blocking_fragments_ready(self) -> PinnedFuture<()> {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut ready = with_runtime(self.runtime, |runtime| {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
let ready = FuturesUnordered::new();
|
||||
for (_, data) in shared_context.pending_fragments.iter_mut() {
|
||||
if data.should_block {
|
||||
if let Some(is_ready) = data.is_ready.take() {
|
||||
ready.push(is_ready);
|
||||
}
|
||||
}
|
||||
}
|
||||
ready
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Box::pin(async move { while ready.next().await.is_some() {} })
|
||||
}
|
||||
|
||||
/// 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>>)> {
|
||||
pub fn take_pending_fragment(&self, id: &str) -> Option<FragmentData> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
shared_context.pending_fragments.remove(id)
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
RwSignal, Scope, SignalUpdate, StoredValue, WriteSignal,
|
||||
};
|
||||
use futures::Future;
|
||||
use std::{borrow::Cow, pin::Pin};
|
||||
use std::{borrow::Cow, collections::VecDeque, pin::Pin};
|
||||
|
||||
/// Tracks [Resource](crate::Resource)s that are read under a suspense context,
|
||||
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
|
||||
@@ -17,13 +17,21 @@ pub struct SuspenseContext {
|
||||
set_pending_resources: WriteSignal<usize>,
|
||||
pub(crate) pending_serializable_resources: RwSignal<usize>,
|
||||
pub(crate) has_local_only: StoredValue<bool>,
|
||||
pub(crate) should_block: StoredValue<bool>,
|
||||
}
|
||||
|
||||
impl SuspenseContext {
|
||||
/// Whether the suspense contains local resources at this moment, and therefore can't be
|
||||
/// Whether the suspense contains local resources at this moment,
|
||||
/// and therefore can't be serialized
|
||||
pub fn has_local_only(&self) -> bool {
|
||||
self.has_local_only.get_value()
|
||||
}
|
||||
|
||||
/// Whether any blocking resources are read under this suspense context,
|
||||
/// meaning the HTML stream should not begin until it has resolved.
|
||||
pub fn should_block(&self) -> bool {
|
||||
self.should_block.get_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for SuspenseContext {
|
||||
@@ -46,11 +54,13 @@ impl SuspenseContext {
|
||||
let (pending_resources, set_pending_resources) = create_signal(cx, 0);
|
||||
let pending_serializable_resources = create_rw_signal(cx, 0);
|
||||
let has_local_only = store_value(cx, true);
|
||||
let should_block = store_value(cx, false);
|
||||
Self {
|
||||
pending_resources,
|
||||
set_pending_resources,
|
||||
pending_serializable_resources,
|
||||
has_local_only,
|
||||
should_block,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,14 +111,19 @@ 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>>>>),
|
||||
Async {
|
||||
/// The HTML chunks this contains.
|
||||
chunks: Pin<Box<dyn Future<Output = VecDeque<StreamChunk>>>>,
|
||||
/// Whether this should block the stream.
|
||||
should_block: bool,
|
||||
},
|
||||
}
|
||||
|
||||
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(_)"),
|
||||
StreamChunk::Async { .. } => write!(f, "StreamChunk::Async(_)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
//! # run_scope(create_runtime(), |cx| {
|
||||
//! spawn_local(async {
|
||||
//! let posts = read_posts(3, "my search".to_string()).await;
|
||||
//! log::debug!("posts = {posts{:#?}");
|
||||
//! log::debug!("posts = {posts:#?}");
|
||||
//! })
|
||||
//! # });
|
||||
//!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.2.3"
|
||||
version = "0.2.5"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
44
meta/src/additional_attributes.rs
Normal file
44
meta/src/additional_attributes.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::TextProp;
|
||||
|
||||
/// A collection of additional HTML attributes to be applied to an element,
|
||||
/// each of which may or may not be reactive.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct AdditionalAttributes(pub(crate) Vec<(String, TextProp)>);
|
||||
|
||||
impl<I, T, U> From<I> for AdditionalAttributes
|
||||
where
|
||||
I: IntoIterator<Item = (T, U)>,
|
||||
T: Into<String>,
|
||||
U: Into<TextProp>,
|
||||
{
|
||||
fn from(value: I) -> Self {
|
||||
Self(
|
||||
value
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.into(), v.into()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over additional HTML attributes.
|
||||
pub struct AdditionalAttributesIter<'a>(
|
||||
std::slice::Iter<'a, (String, TextProp)>,
|
||||
);
|
||||
|
||||
impl<'a> Iterator for AdditionalAttributesIter<'a> {
|
||||
type Item = &'a (String, TextProp);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a AdditionalAttributes {
|
||||
type Item = &'a (String, TextProp);
|
||||
type IntoIter = AdditionalAttributesIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::TextProp;
|
||||
use crate::{additional_attributes::AdditionalAttributes, TextProp};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
@@ -7,15 +7,37 @@ use std::{cell::RefCell, rc::Rc};
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BodyContext {
|
||||
class: Rc<RefCell<Option<TextProp>>>,
|
||||
attributes: Rc<RefCell<Option<MaybeSignal<AdditionalAttributes>>>>,
|
||||
}
|
||||
|
||||
impl BodyContext {
|
||||
/// Converts the `<body>` metadata into an HTML string.
|
||||
pub fn as_string(&self) -> Option<String> {
|
||||
self.class
|
||||
let class = self
|
||||
.class
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|class| format!(" class=\"{}\"", class.get()))
|
||||
.map(|val| format!("class=\"{}\"", val.get()));
|
||||
let attributes = self.attributes.borrow().as_ref().map(|val| {
|
||||
val.with(|val| {
|
||||
val.0
|
||||
.iter()
|
||||
.map(|(n, v)| format!("{}=\"{}\"", n, v.get()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
});
|
||||
let mut val = [class, attributes]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if val.is_empty() {
|
||||
None
|
||||
} else {
|
||||
val.insert(0, ' ');
|
||||
Some(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,20 +79,38 @@ pub fn Body(
|
||||
/// The `class` attribute on the `<body>`.
|
||||
#[prop(optional, into)]
|
||||
class: Option<TextProp>,
|
||||
/// Arbitrary attributes to add to the `<html>`
|
||||
#[prop(optional, into)]
|
||||
attributes: Option<MaybeSignal<AdditionalAttributes>>,
|
||||
) -> impl IntoView {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
let el = document().body().expect("there to be a <body> element");
|
||||
|
||||
if let Some(class) = class {
|
||||
create_render_effect(cx, move |_| {
|
||||
let value = class.get();
|
||||
_ = el.set_attribute("class", &value);
|
||||
create_render_effect(cx, {
|
||||
let el = el.clone();
|
||||
move |_| {
|
||||
let value = class.get();
|
||||
_ = el.set_attribute("class", &value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(attributes) = attributes {
|
||||
let attributes = attributes.get();
|
||||
for (attr_name, attr_value) in attributes.0.into_iter() {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |_|{
|
||||
let value = attr_value.get();
|
||||
_ = el.set_attribute(&attr_name, &value);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let meta = crate::use_head(cx);
|
||||
*meta.body.class.borrow_mut() = class;
|
||||
*meta.body.attributes.borrow_mut() = attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::TextProp;
|
||||
use crate::{additional_attributes::AdditionalAttributes, TextProp};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
@@ -9,6 +9,7 @@ pub struct HtmlContext {
|
||||
lang: Rc<RefCell<Option<TextProp>>>,
|
||||
dir: Rc<RefCell<Option<TextProp>>>,
|
||||
class: Rc<RefCell<Option<TextProp>>>,
|
||||
attributes: Rc<RefCell<Option<MaybeSignal<AdditionalAttributes>>>>,
|
||||
}
|
||||
|
||||
impl HtmlContext {
|
||||
@@ -29,7 +30,16 @@ impl HtmlContext {
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|val| format!("class=\"{}\"", val.get()));
|
||||
let mut val = [lang, dir, class]
|
||||
let attributes = self.attributes.borrow().as_ref().map(|val| {
|
||||
val.with(|val| {
|
||||
val.0
|
||||
.iter()
|
||||
.map(|(n, v)| format!("{}=\"{}\"", n, v.get()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
});
|
||||
let mut val = [lang, dir, class, attributes]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
@@ -62,7 +72,12 @@ impl std::fmt::Debug for HtmlContext {
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Html lang="he" dir="rtl"/>
|
||||
/// <Html
|
||||
/// lang="he"
|
||||
/// dir="rtl"
|
||||
/// // arbitrary additional attributes can be passed via `attributes`
|
||||
/// attributes=AdditionalAttributes::from(vec![("data-theme", "dark")])
|
||||
/// />
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
@@ -79,6 +94,9 @@ pub fn Html(
|
||||
/// The `class` attribute on the `<html>`.
|
||||
#[prop(optional, into)]
|
||||
class: Option<TextProp>,
|
||||
/// Arbitrary attributes to add to the `<html>`
|
||||
#[prop(optional, into)]
|
||||
attributes: Option<MaybeSignal<AdditionalAttributes>>,
|
||||
) -> impl IntoView {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
@@ -101,16 +119,29 @@ pub fn Html(
|
||||
}
|
||||
|
||||
if let Some(class) = class {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |_| {
|
||||
let value = class.get();
|
||||
_ = el.set_attribute("class", &value);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(attributes) = attributes {
|
||||
let attributes = attributes.get();
|
||||
for (attr_name, attr_value) in attributes.0.into_iter() {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |_|{
|
||||
let value = attr_value.get();
|
||||
_ = el.set_attribute(&attr_name, &value);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let meta = crate::use_head(cx);
|
||||
*meta.html.lang.borrow_mut() = lang;
|
||||
*meta.html.dir.borrow_mut() = dir;
|
||||
*meta.html.class.borrow_mut() = class;
|
||||
*meta.html.attributes.borrow_mut() = attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ use std::{
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
mod additional_attributes;
|
||||
mod body;
|
||||
mod html;
|
||||
mod link;
|
||||
@@ -66,6 +67,7 @@ mod script;
|
||||
mod style;
|
||||
mod stylesheet;
|
||||
mod title;
|
||||
pub use additional_attributes::*;
|
||||
pub use body::*;
|
||||
pub use html::*;
|
||||
pub use link::*;
|
||||
@@ -282,6 +284,15 @@ impl MetaContext {
|
||||
/// server-side HTML rendering across crates.
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn generate_head_metadata(cx: Scope) -> String {
|
||||
let (head, body) = generate_head_metadata_separated(cx);
|
||||
format!("{head}</head><{body}>")
|
||||
}
|
||||
|
||||
/// Extracts the metadata that should be inserted at the beginning of the `<head>` tag
|
||||
/// and on the opening `<body>` tag. This is a helper function used in implementing
|
||||
/// server-side HTML rendering across crates.
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn generate_head_metadata_separated(cx: Scope) -> (String, String) {
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let head = meta
|
||||
.as_ref()
|
||||
@@ -291,7 +302,7 @@ pub fn generate_head_metadata(cx: Scope) -> String {
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.body.as_string())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body{body_meta}>")
|
||||
(head, format!("<body{body_meta}>"))
|
||||
}
|
||||
|
||||
/// Describes a value that is either a static or a reactive string, i.e.,
|
||||
|
||||
@@ -30,6 +30,9 @@ pub fn Meta(
|
||||
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
|
||||
#[prop(optional, into)]
|
||||
name: Option<TextProp>,
|
||||
/// The [`property`](https://ogp.me/) attribute.
|
||||
#[prop(optional, into)]
|
||||
property: Option<TextProp>,
|
||||
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
|
||||
#[prop(optional, into)]
|
||||
http_equiv: Option<TextProp>,
|
||||
@@ -45,6 +48,7 @@ pub fn Meta(
|
||||
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("property", move || property.as_ref().map(|v| v.get()))
|
||||
.attr("http-equiv", move || http_equiv.as_ref().map(|v| v.get()))
|
||||
.attr("content", move || content.as_ref().map(|v| v.get()))
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.2.3"
|
||||
version = "0.2.5"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -85,6 +85,7 @@ where
|
||||
// POST
|
||||
if method == "post" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
spawn_local(async move {
|
||||
@@ -120,7 +121,10 @@ where
|
||||
Ok(url) => {
|
||||
request_animation_frame(move || {
|
||||
if let Err(e) = navigate(
|
||||
&url.pathname,
|
||||
&format!(
|
||||
"{}{}",
|
||||
url.pathname, url.search,
|
||||
),
|
||||
Default::default(),
|
||||
) {
|
||||
warn!("{}", e);
|
||||
@@ -141,6 +145,7 @@ where
|
||||
.is_ok()
|
||||
{
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -345,6 +350,7 @@ where
|
||||
}
|
||||
Ok(input) => {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
multi_action.dispatch(input);
|
||||
if let Some(error) = error {
|
||||
error.set(None);
|
||||
|
||||
@@ -62,6 +62,9 @@ pub fn A<H>(
|
||||
/// Sets the `class` attribute on the underlying `<a>` tag, making it easier to style.
|
||||
#[prop(optional, into)]
|
||||
class: Option<AttributeValue>,
|
||||
/// Sets the `id` attribute on the underlying `<a>` tag, making it easier to target.
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
/// The nodes or elements to be shown inside the link.
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
@@ -75,6 +78,7 @@ where
|
||||
state: Option<State>,
|
||||
replace: bool,
|
||||
class: Option<AttributeValue>,
|
||||
id: Option<String>,
|
||||
children: Children,
|
||||
) -> HtmlElement<leptos::html::A> {
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
@@ -109,6 +113,7 @@ where
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
@@ -116,5 +121,5 @@ where
|
||||
}
|
||||
|
||||
let href = use_resolved_path(cx, move || href.to_href()());
|
||||
inner(cx, href, exact, state, replace, class, children)
|
||||
inner(cx, href, exact, state, replace, class, id, children)
|
||||
}
|
||||
|
||||
@@ -34,10 +34,15 @@ where
|
||||
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>(cx) {
|
||||
(redirect_fn.f)(&path);
|
||||
}
|
||||
|
||||
// redirect on the client
|
||||
let navigate = use_navigate(cx);
|
||||
navigate(&path, options.unwrap_or_default())
|
||||
else {
|
||||
let navigate = use_navigate(cx);
|
||||
leptos::request_animation_frame(move || {
|
||||
if let Err(e) = navigate(&path, options.unwrap_or_default()) {
|
||||
leptos::error!("<Redirect/> error: {e:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapping type for a function provided as context to allow for
|
||||
|
||||
@@ -3,10 +3,7 @@ use crate::{
|
||||
ParamsMap, RouterContext, SsrMode,
|
||||
};
|
||||
use leptos::{leptos_dom::Transparent, *};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
thread_local! {
|
||||
static ROUTE_ID: Cell<usize> = Cell::new(0);
|
||||
@@ -37,44 +34,7 @@ where
|
||||
F: Fn(Scope) -> E + 'static,
|
||||
P: std::fmt::Display,
|
||||
{
|
||||
fn inner(
|
||||
cx: Scope,
|
||||
children: Option<Children>,
|
||||
path: String,
|
||||
view: Rc<dyn Fn(Scope) -> View>,
|
||||
ssr_mode: SsrMode,
|
||||
) -> RouteDefinition {
|
||||
let children = children
|
||||
.map(|children| {
|
||||
children(cx)
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let id = ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
});
|
||||
|
||||
RouteDefinition {
|
||||
id,
|
||||
path,
|
||||
children,
|
||||
view,
|
||||
ssr_mode,
|
||||
}
|
||||
}
|
||||
|
||||
inner(
|
||||
define_route(
|
||||
cx,
|
||||
children,
|
||||
path.to_string(),
|
||||
@@ -83,6 +43,91 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Describes a route that is guarded by a certain condition. This works the same way as
|
||||
/// [`<Route/>`](Route), except that if the `condition` function evaluates to `false`, it
|
||||
/// redirects to `redirect_path` instead of displaying its `view`.
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedRoute<P, E, F, C>(
|
||||
cx: Scope,
|
||||
/// The path fragment that this route should match. This can be static (`users`),
|
||||
/// include a parameter (`:id`) or an optional parameter (`:id?`), or match a
|
||||
/// wildcard (`user/*any`).
|
||||
path: P,
|
||||
/// The path that will be redirected to if the condition is `false`.
|
||||
redirect_path: P,
|
||||
/// Condition function that returns a boolean.
|
||||
condition: C,
|
||||
/// View that will be exposed if the condition is `true`.
|
||||
view: F,
|
||||
/// The mode that this route prefers during server-side rendering. Defaults to out-of-order streaming.
|
||||
#[prop(optional)]
|
||||
ssr: SsrMode,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
E: IntoView,
|
||||
F: Fn(Scope) -> E + 'static,
|
||||
P: std::fmt::Display + 'static,
|
||||
C: Fn(Scope) -> bool + 'static,
|
||||
{
|
||||
use crate::{Redirect, RedirectProps};
|
||||
let redirect_path = redirect_path.to_string();
|
||||
|
||||
define_route(
|
||||
cx,
|
||||
children,
|
||||
path.to_string(),
|
||||
Rc::new(move |cx| {
|
||||
if condition(cx) {
|
||||
view(cx).into_view(cx)
|
||||
} else {
|
||||
view! { cx, <Redirect path=redirect_path.clone()/> }
|
||||
.into_view(cx)
|
||||
}
|
||||
}),
|
||||
ssr,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn define_route(
|
||||
cx: Scope,
|
||||
children: Option<Children>,
|
||||
path: String,
|
||||
view: Rc<dyn Fn(Scope) -> View>,
|
||||
ssr_mode: SsrMode,
|
||||
) -> RouteDefinition {
|
||||
let children = children
|
||||
.map(|children| {
|
||||
children(cx)
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let id = ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
});
|
||||
|
||||
RouteDefinition {
|
||||
id,
|
||||
path,
|
||||
children,
|
||||
view,
|
||||
ssr_mode,
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for RouteDefinition {
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
Transparent::new(self).into_view(cx)
|
||||
@@ -121,7 +166,7 @@ impl RouteContext {
|
||||
id,
|
||||
base_path: base,
|
||||
child: Box::new(child),
|
||||
path: RefCell::new(path),
|
||||
path: create_rw_signal(cx, path),
|
||||
original_path: route.original_path.to_string(),
|
||||
params,
|
||||
outlet: Box::new(move |cx| Some(element(cx))),
|
||||
@@ -144,11 +189,11 @@ impl RouteContext {
|
||||
/// e.g., this will return `/article/0` rather than `/article/:id`.
|
||||
/// For the opposite behavior, see [RouteContext::original_path].
|
||||
pub fn path(&self) -> String {
|
||||
self.inner.path.borrow().to_string()
|
||||
self.inner.path.get_untracked()
|
||||
}
|
||||
|
||||
pub(crate) fn set_path(&mut self, path: String) {
|
||||
*self.inner.path.borrow_mut() = path;
|
||||
pub(crate) fn set_path(&self, path: String) {
|
||||
self.inner.path.set(path);
|
||||
}
|
||||
|
||||
/// Returns the original URL path of the current route,
|
||||
@@ -176,7 +221,7 @@ impl RouteContext {
|
||||
id: 0,
|
||||
base_path: path.to_string(),
|
||||
child: Box::new(|_| None),
|
||||
path: RefCell::new(path.to_string()),
|
||||
path: create_rw_signal(cx, path.to_string()),
|
||||
original_path: path.to_string(),
|
||||
params: create_memo(cx, |_| ParamsMap::new()),
|
||||
outlet: Box::new(move |cx| {
|
||||
@@ -188,7 +233,16 @@ impl RouteContext {
|
||||
|
||||
/// Resolves a relative route, relative to the current route's path.
|
||||
pub fn resolve_path(&self, to: &str) -> Option<String> {
|
||||
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.borrow()))
|
||||
resolve_path(
|
||||
&self.inner.base_path,
|
||||
to,
|
||||
Some(&self.inner.path.get_untracked()),
|
||||
)
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_path_tracked(&self, to: &str) -> Option<String> {
|
||||
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.get()))
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
@@ -208,7 +262,7 @@ pub(crate) struct RouteContextInner {
|
||||
base_path: String,
|
||||
pub(crate) id: usize,
|
||||
pub(crate) child: Box<dyn Fn(Scope) -> Option<RouteContext>>,
|
||||
pub(crate) path: RefCell<String>,
|
||||
pub(crate) path: RwSignal<String>,
|
||||
pub(crate) original_path: String,
|
||||
pub(crate) params: Memo<ParamsMap>,
|
||||
pub(crate) outlet: Box<dyn Fn(Scope) -> Option<View>>,
|
||||
|
||||
@@ -221,55 +221,36 @@ impl RouterContextInner {
|
||||
if resolved_to != this.reference.get()
|
||||
|| options.state != (this.state).get()
|
||||
{
|
||||
if cfg!(feature = "server") {
|
||||
self.history.navigate(&LocationChange {
|
||||
value: resolved_to,
|
||||
{
|
||||
self.referrers.borrow_mut().push(LocationChange {
|
||||
value: self.reference.get(),
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: options.state.clone(),
|
||||
state: self.state.get(),
|
||||
});
|
||||
} else {
|
||||
{
|
||||
self.referrers.borrow_mut().push(
|
||||
LocationChange {
|
||||
value: self.reference.get(),
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: self.state.get(),
|
||||
},
|
||||
);
|
||||
}
|
||||
let len = self.referrers.borrow().len();
|
||||
}
|
||||
let len = self.referrers.borrow().len();
|
||||
|
||||
#[cfg(feature = "transition")]
|
||||
let transition = use_transition(self.cx);
|
||||
//transition.start({
|
||||
let set_reference = self.set_reference;
|
||||
let set_state = self.set_state;
|
||||
let referrers = self.referrers.clone();
|
||||
let this = Rc::clone(&self);
|
||||
//move || {
|
||||
let set_reference = self.set_reference;
|
||||
let set_state = self.set_state;
|
||||
let referrers = self.referrers.clone();
|
||||
let this = Rc::clone(&self);
|
||||
|
||||
let resolved = resolved_to.to_string();
|
||||
let state = options.state.clone();
|
||||
queue_microtask(move || {
|
||||
set_reference.update(move |r| *r = resolved);
|
||||
let resolved = resolved_to.to_string();
|
||||
let state = options.state.clone();
|
||||
set_reference.update(move |r| *r = resolved);
|
||||
|
||||
set_state.update({
|
||||
let next_state = state.clone();
|
||||
move |state| *state = next_state
|
||||
});
|
||||
if referrers.borrow().len() == len {
|
||||
this.navigate_end(LocationChange {
|
||||
value: resolved_to.to_string(),
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state,
|
||||
})
|
||||
//}
|
||||
}
|
||||
});
|
||||
//});
|
||||
set_state.update({
|
||||
let next_state = state.clone();
|
||||
move |state| *state = next_state
|
||||
});
|
||||
if referrers.borrow().len() == len {
|
||||
this.navigate_end(LocationChange {
|
||||
value: resolved_to,
|
||||
replace: false,
|
||||
scroll: true,
|
||||
state,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,17 @@ pub fn Routes(
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
child
|
||||
let def = child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>());
|
||||
if def.is_none() {
|
||||
warn!(
|
||||
"[NOTE] The <Routes/> component should include *only* \
|
||||
<Route/>or <ProtectedRoute/> components, or some \
|
||||
#[component(transparent)] that returns a RouteDefinition."
|
||||
);
|
||||
}
|
||||
def
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
@@ -88,7 +96,7 @@ pub fn Routes(
|
||||
if next_match.route.key == prev_match.route.key
|
||||
&& next_match.route.id == prev_match.route.id =>
|
||||
{
|
||||
let mut prev_one = { prev.borrow()[i].clone() };
|
||||
let prev_one = { prev.borrow()[i].clone() };
|
||||
if next_match.path_match.path != prev_one.path() {
|
||||
prev_one
|
||||
.set_path(next_match.path_match.path.clone());
|
||||
|
||||
@@ -70,7 +70,7 @@ pub fn use_resolved_path(
|
||||
if path.starts_with('/') {
|
||||
Some(path)
|
||||
} else {
|
||||
route.resolve_path(&path).map(String::from)
|
||||
route.resolve_path_tracked(&path).map(String::from)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ cfg_if! {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complext_query_string() {
|
||||
fn test_complex_query_string() {
|
||||
let url = Url::try_from("http://leptos.com?data=Data%3A+%24+%26+%2B%2B+7").unwrap();
|
||||
assert_params_map!{
|
||||
["data" => "Data: $ & ++ 7"],
|
||||
|
||||
@@ -6,8 +6,9 @@ use proc_macro::TokenStream;
|
||||
use server_fn_macro::server_macro_impl;
|
||||
use syn::__private::ToTokens;
|
||||
|
||||
/// Declares that a function is a [server function](server_fn). This means that
|
||||
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
/// Declares that a function is a [server function](https://docs.rs/server_fn/).
|
||||
/// This means that its body will only run on the server, i.e., when the `ssr`
|
||||
/// feature is enabled.
|
||||
///
|
||||
/// You can specify one, two, or three arguments to the server function:
|
||||
/// 1. **Required**: A type name that will be used to identify and register the server function
|
||||
@@ -41,7 +42,7 @@ use syn::__private::ToTokens;
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must implement [Serialize](serde::Serialize).**
|
||||
/// - **Return types must implement [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
|
||||
/// 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 [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
//! # async fn main() {
|
||||
//! async {
|
||||
//! let posts = read_posts(3, "my search".to_string()).await;
|
||||
//! log::debug!("posts = {posts{:#?}");
|
||||
//! log::debug!("posts = {posts:#?}");
|
||||
//! }
|
||||
//! # }
|
||||
//!
|
||||
|
||||
@@ -14,7 +14,7 @@ use syn::{
|
||||
*,
|
||||
};
|
||||
|
||||
/// Discribes the custom context from the server that passed to the server function. Optionally, the first argument of a server function
|
||||
/// Describes the custom context from the server that passed to the server function. Optionally, the first argument of a server function
|
||||
/// can be a custom context of this type. This context can be used to access the server's state within the server function.
|
||||
pub struct ServerContext {
|
||||
/// The type of the context.
|
||||
|
||||
Reference in New Issue
Block a user