mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
18 Commits
fix-resour
...
v0.2.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e78ce7e6b9 | ||
|
|
a3327f8841 | ||
|
|
f727dd773b | ||
|
|
952646f066 | ||
|
|
1e037ecb60 | ||
|
|
c9f75d82d6 | ||
|
|
de3849c20c | ||
|
|
c391c2e938 | ||
|
|
1cde4b1f8a | ||
|
|
42360d109b | ||
|
|
7aa4d9e6db | ||
|
|
9ed3390b81 | ||
|
|
1ff56f7bfd | ||
|
|
16917997cd | ||
|
|
f42568d262 | ||
|
|
97bbdf561a | ||
|
|
f4043cbd9f | ||
|
|
e9ff26abb4 |
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.4" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.4" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.4" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.4" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.4" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.4" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.4" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.4" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.4" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.4" }
|
||||
leptos_router = { path = "./router", version = "0.2.4" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.4" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.4" }
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[[proxy]]
|
||||
rewrite = "/api/"
|
||||
backend = "http://0.0.0.0:3000/"
|
||||
backend = "http://127.0.0.1:3000/"
|
||||
|
||||
@@ -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/>");
|
||||
|
||||
@@ -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;
|
||||
@@ -346,8 +346,9 @@ where
|
||||
/// 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};
|
||||
@@ -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
|
||||
/// Required 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,
|
||||
@@ -199,7 +200,8 @@ impl<B> LeptosRequest<B> {
|
||||
}
|
||||
/// Generate a wrapper for the http::Request::Request type that allows one to
|
||||
/// process it, access the body, and use axum Extractors on it.
|
||||
/// Required by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
|
||||
/// 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,
|
||||
@@ -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);
|
||||
@@ -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 maintain 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};
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -781,9 +781,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let mut this = self;
|
||||
|
||||
let this = classes_signal()
|
||||
classes_signal()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.flat_map(|classes| {
|
||||
@@ -792,9 +790,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
.map(ToString::to_string)
|
||||
.collect::<SmallVec<[_; 4]>>()
|
||||
})
|
||||
.fold(this, |this, class| this.class(class, true));
|
||||
|
||||
this
|
||||
.fold(self, |this, class| this.class(class, true))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -206,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
|
||||
@@ -436,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,
|
||||
@@ -463,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()
|
||||
}
|
||||
@@ -642,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 {
|
||||
|
||||
@@ -1392,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) {
|
||||
|
||||
@@ -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,
|
||||
| ^
|
||||
@@ -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 {
|
||||
|
||||
@@ -106,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,
|
||||
@@ -132,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| {
|
||||
@@ -256,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| {
|
||||
@@ -560,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>
|
||||
@@ -600,10 +679,14 @@ 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 \
|
||||
@@ -629,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,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
|
||||
@@ -688,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,21 +398,26 @@ 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;
|
||||
})),
|
||||
},
|
||||
);
|
||||
})
|
||||
}
|
||||
@@ -416,10 +426,7 @@ impl Scope {
|
||||
///
|
||||
/// 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(_)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.2.4"
|
||||
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.,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.2.4"
|
||||
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 {
|
||||
@@ -144,6 +145,7 @@ where
|
||||
.is_ok()
|
||||
{
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -348,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
|
||||
|
||||
@@ -34,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(),
|
||||
@@ -80,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)
|
||||
|
||||
@@ -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<_>>();
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user