Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
b0f745f4a9 fix: escape </script> and other HTML tags in serialized resources 2023-03-28 20:34:54 -04:00
62 changed files with 435 additions and 1989 deletions

View File

@@ -25,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.2.5"
version = "0.2.4"
[workspace.dependencies]
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" }
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" }
[profile.release]
codegen-units = 1

View File

@@ -122,7 +122,6 @@ fn App(cx: Scope) -> impl IntoView {
provide_context(cx, state);
// ...
}
```
Then child components can access “slices” of that state with fine-grained

View File

@@ -26,7 +26,7 @@
- [Nested Routing](./router/17_nested_routing.md)
- [Params and Queries](./router/18_params_and_queries.md)
- [`<A/>`](./router/19_a.md)
- [`<Form/>`](./router/20_form.md)
- [`<Form/>`]()
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [Metadata]()
- [SSR]()

View File

@@ -1,67 +0,0 @@
# The `<Form/>` Component
Links and forms sometimes seem completely unrelated. But in fact, they work in very similar ways.
In plain HTML, there are three ways to navigate to another page:
1. An `<a>` element that links to another page. Navigates to the URL in its `href` attribute with the `GET` HTTP method.
2. A `<form method="GET">`. Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
3. A `<form method="POST">`. Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way.
The router provides a [`<Form>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Form.html) component, which works like the HTML `<form>` element, but uses client-side navigations instead of full page reloads. `<Form/>` works with both `GET` and `POST` requests. With `method="GET"`, it will navigate to the URL encoded in the form data. With `method="POST"` it will make a `POST` request and handle the servers response.
`<Form/>` provides the basis for some components like `<ActionForm/>` and `<MultiActionForm/>` that well see in later chapters. But it also enables some powerful patterns of its own.
For example, imagine that you want to create a search field that updates search results in real time as the user searches, without a page reload, but that also stores the search in the URL so a user can copy and paste it to share results with someone else.
It turns out that the patterns weve learned so far make this easy to implement.
```rust
async fn fetch_results() {
// some async function to fetch our search results
}
#[component]
pub fn Search(cx: Scope) -> impl IntoView {
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
// reactive access to URL query strings
let query = use_query_map(cx);
// search stored as ?q=
let search = move || query().get("q").cloned().unwrap_or_default();
// a resource driven by the search string
let search_results = create_resource(cx, search, fetch_results);
view! { cx,
<Form method="GET" action="">
<input type="search" name="search" value=search/>
<input type="submit"/>
</Form>
<Transition fallback=move || ()>
/* render search results */
</Transition>
}
}
```
Whenever you click `Submit`, the `<Form/>` will “navigate” to `?q={search}`. But because this navigation is done on the client side, theres no page flicker or reload. The URL query string changes, which triggers `search` to update. Because `search` is the source signal for the `search_results` resource, this triggers `search_results` to reload its resource. The `<Transition/>` continues displaying the current search results until the new ones have loaded. When they are complete, it switches to displaying the new result.
This is a great pattern. The data flow is extremely clear: all data flows from the URL to the resource into the UI. The current state of the application is stored in the URL, which means you can refresh the page or text the link to a friend and it will show exactly what youre expecting. And once we introduce server rendering, this pattern will prove to be really fault-tolerant, too: because it uses a `<form>` element and URLs under the hood, it actually works really well without even loading your WASM on the client.
We can actually take it a step further and do something kind of clever:
```rust
view! { cx,
<Form method="GET" action="">
<input type="search" name="search" value=search
oninput="this.form.requestSubmit()"
/>
</Form>
}
```
Youll notice that this version drops the `Submit` button. Instead, we add an `oninput` attribute to the input. Note that this is _not_ `on:input`, which would listen for the `input` event and run some Rust code. Without the colon, `oninput` is the plain HTML attribute. So the string is actually a JavaScript string. `this.form` gives us the form the input is attached to. `requestSubmit()` fires the `submit` event on the `<form>`, which is caught by `<Form/>` just as if we had clicked a `Submit` button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the users input as they type.
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>

View File

@@ -107,28 +107,27 @@ fn clear() {
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
);
}
```
Well use some manual DOM operations to grab the `<div>` that wraps
the whole component, as well as the `clear` button.
```rust
// now we extract the buttons by iterating over the DOM
// this would be easier if they had IDs
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
// now we 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
@@ -136,27 +135,27 @@ I like to test the whole view at once. We can do this by testing the elements
`outerHTML` against our expectations.
```rust
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system to render the test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
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` thats inside the component.
@@ -165,14 +164,15 @@ with the initial value `0`. This is where our wrapping element comes in: Ill
the wrappers `innerHTML` against another comparison case.
```rust
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
}
```
This is only a very limited introduction to testing. But I hope its useful as you begin to build applications.

View File

@@ -20,12 +20,6 @@ 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.

View File

@@ -24,7 +24,6 @@ view! {
max="50"
value=double_count
/>
}
```
But of course, this doesnt scale very well. If you want to add a third progress

View File

@@ -38,7 +38,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link =
move |cx| pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
@@ -65,20 +65,16 @@ pub fn Stories(cx: Scope) -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<span class="page-link"
class:disabled=move || hide_more_link(cx)
aria-hidden=move || hide_more_link(cx)
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Transition>
"more >"
</a>
</span>
</div>
<main class="news-list">
<div>

View File

@@ -1,3 +1,3 @@
[[proxy]]
rewrite = "/api/"
backend = "http://127.0.0.1:3000/"
backend = "http://0.0.0.0:3000/"

View File

@@ -28,7 +28,19 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
</nav>
<main>
<Routes>
<ContactRoutes/>
<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>
<Route
path="about"
view=move |cx| view! { cx, <About/> }
@@ -47,27 +59,6 @@ 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/>");

View File

@@ -20,7 +20,7 @@ use leptos::{
leptos_server::{server_fn_by_path, Payload},
*,
};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
@@ -346,9 +346,8 @@ where
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using
/// [render_to_stream_in_order](leptos::ssr::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], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -410,8 +409,8 @@ where
/// The provides a [MetaContext] and a [RouterIntegrationContext] to the apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_string_async](leptos::ssr::render_to_string_async), and
/// includes everything described in the documentation for that function.
/// The HTML stream is rendered using [render_to_string_async], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -729,7 +728,7 @@ async fn stream_app(
let (stream, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| generate_head_metadata_separated(cx).1.into(),
move |cx| generate_head_metadata(cx).into(),
additional_context,
);
@@ -746,7 +745,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_separated(cx).1.into()
generate_head_metadata(cx).into()
},
additional_context,
);
@@ -763,7 +762,7 @@ async fn build_stream_response(
) -> HttpResponse {
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
html_parts(options, use_context::<MetaContext>(cx).as_ref());
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })

View File

@@ -27,8 +27,8 @@ use leptos::{
ssr::*,
*,
};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::{generate_head_metadata, MetaContext};
use leptos_router::*;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc};
@@ -147,9 +147,8 @@ 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](https://github.com/hyperium/http/pull/574) for eventual resolution:
/// 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
#[derive(Debug, Default)]
pub struct LeptosRequest<B>(Arc<RwLock<Option<Request<B>>>>);
@@ -159,12 +158,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,
@@ -200,8 +199,7 @@ 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](https://github.com/hyperium/http/pull/574) for eventual resolution:
/// Required by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
pub async fn generate_leptos_request<B>(req: Request<B>) -> LeptosRequest<B>
where
B: Default + std::fmt::Debug,
@@ -655,7 +653,7 @@ where
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
|cx| generate_head_metadata(cx).into(),
add_context,
);
@@ -713,7 +711,7 @@ async fn forward_stream(
) {
let cx = Scope { runtime, id: scope };
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
html_parts(options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
@@ -824,7 +822,7 @@ where
let (bundle, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
|cx| generate_head_metadata(cx).into(),
add_context,
);

View File

@@ -3,10 +3,25 @@ use leptos::{use_context, RuntimeId, ScopeId};
use leptos_config::LeptosOptions;
use leptos_meta::MetaContext;
fn autoreload(options: &LeptosOptions) -> 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 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");
}
let site_ip = &options.site_addr.ip().to_string();
let reload_port = options.reload_port;
match std::env::var("LEPTOS_WATCH").is_ok() {
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin="">(function () {{
@@ -37,25 +52,7 @@ fn autoreload(options: &LeptosOptions) -> String {
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();
@@ -75,46 +72,6 @@ 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,
@@ -129,7 +86,7 @@ pub async fn build_async_response(
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
html_parts(options, use_context::<MetaContext>(cx).as_ref());
// in async, we load the meta content *now*, after the suspenses have resolved
let meta = use_context::<MetaContext>(cx);

View File

@@ -17,8 +17,8 @@ use leptos::{
ssr::*,
*,
};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::{generate_head_metadata, MetaContext};
use leptos_router::*;
use parking_lot::RwLock;
use std::{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_separated(cx).1.into(),
|cx| generate_head_metadata(cx).into(),
add_context,
);
@@ -593,7 +593,7 @@ async fn forward_stream(
) {
let cx = Scope { runtime, id: scope };
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
html_parts(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_separated(cx).1.into(),
|cx| generate_head_metadata(cx).into(),
add_context,
);

View File

@@ -79,9 +79,9 @@ where
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
if context.ready() {
Fragment::lazy(Box::new(|| vec![orig_child(cx).into_view(cx)])).into_view(cx)
orig_child(cx).into_view(cx)
} else {
Fragment::lazy(Box::new(|| vec![fallback().into_view(cx)])).into_view(cx)
fallback().into_view(cx)
}
} else {
use leptos_reactive::signal_prelude::*;
@@ -108,12 +108,10 @@ where
let orig_child = Rc::clone(&orig_child);
move || {
HydrationCtx::continue_from(current_id.clone());
Fragment::lazy(Box::new(move || {
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
}))
.into_view(cx)
.render_to_string(cx)
.to_string()
DynChild::new(move || orig_child(cx))
.into_view(cx)
.render_to_string(cx)
.to_string()
}
},
// in-order streaming
@@ -121,13 +119,11 @@ where
let current_id = current_id.clone();
move || {
HydrationCtx::continue_from(current_id.clone());
Fragment::lazy(Box::new(move || {
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
}))
.into_view(cx)
.into_stream_chunks(cx)
DynChild::new(move || orig_child(cx))
.into_view(cx)
.into_stream_chunks(cx)
}
},
}
);
// return the fallback for now, wrapped in fragment identifier

View File

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

View File

@@ -1,78 +0,0 @@
[package]
name = "leptos_start"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "1"
cfg-if = "1"
leptos = { path = "../../..", default-features = false, features = ["serde"] }
leptos_actix = { path = "../../../../integrations/actix", optional = true }
leptos_router = { path = "../../../../router", default-features = false }
log = "0.4"
simple_logger = "4"
wasm-bindgen = "0.2"
serde = "1.0.159"
tokio = { version = "1.27.0", features = ["time"], optional = true }
[features]
hydrate = ["leptos/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_router/ssr",
"dep:tokio",
]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos_start"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
[workspace]

View File

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

View File

@@ -1,61 +0,0 @@
<picture>
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
</picture>
# Leptos Starter Template
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
## Creating your template repo
If you don't have `cargo-leptos` installed you can install it with
`cargo install cargo-leptos`
Then run
`cargo leptos new --git leptos-rs/start`
to generate a new project template.
`cd {projectname}`
to go to your newly created project.
Of course you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
## Running your project
`cargo leptos watch`
## Installing Additional Tools
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on
3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
5. `npm install -g sass` - install `dart-sass` (should be optional in future)
## Executing a Server on a Remote Machine Without the Toolchain
After running a `cargo leptos build --release` the minimum files needed are:
1. The server binary located in `target/server/release`
2. The `site` directory and all files within located in `target/site`
Copy these files to your remote server. The directory structure should be:
```text
leptos_start
site/
```
Set the following enviornment variables (updating for your project as needed):
```text
LEPTOS_OUTPUT_NAME="leptos_start"
LEPTOS_SITE_ROOT="site"
LEPTOS_SITE_PKG_DIR="pkg"
LEPTOS_SITE_ADDR="127.0.0.1:3000"
LEPTOS_RELOAD_PORT="3001"
```
Finally, run the server binary.

View File

@@ -1,74 +0,0 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
},
"node_modules/@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.28.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"node_modules/playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true,
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
}
},
"dependencies": {
"@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.28.0"
}
},
"@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true
}
}
}

View File

@@ -1,13 +0,0 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
}

View File

@@ -1,107 +0,0 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View File

@@ -1,9 +0,0 @@
import { test, expect } from "@playwright/test";
test("homepage has title and links to intro page", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Welcome to Leptos");
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
});

View File

@@ -1,219 +0,0 @@
use leptos::*;
use leptos_router::*;
#[server(OneSecondFn "/api")]
async fn one_second_fn(query: ()) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
Ok(())
}
#[server(TwoSecondFn "/api")]
async fn two_second_fn(query: ()) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
Ok(())
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let style = r#"
nav {
display: flex;
width: 100%;
justify-content: space-around;
}
[aria-current] {
font-weight: bold;
}
"#;
view! {
cx,
<style>{style}</style>
<Router>
<nav>
<A href="/out-of-order">"Out-of-Order"</A>
<A href="/in-order">"In-Order"</A>
<A href="/async">"Async"</A>
</nav>
<main>
<Routes>
<Route
path=""
view=|cx| view! { cx, <Redirect path="/out-of-order"/> }
/>
// out-of-order
<Route
path="out-of-order"
view=|cx| view! { cx,
<SecondaryNav/>
<h1>"Out-of-Order"</h1>
<Outlet/>
}
>
<Route path="" view=|cx| view! { cx, <Nested/> }/>
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
</Route>
// in-order
<Route
path="in-order"
ssr=SsrMode::InOrder
view=|cx| view! { cx,
<SecondaryNav/>
<h1>"In-Order"</h1>
<Outlet/>
}
>
<Route path="" view=|cx| view! { cx, <Nested/> }/>
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
</Route>
// async
<Route
path="async"
ssr=SsrMode::Async
view=|cx| view! { cx,
<SecondaryNav/>
<h1>"Async"</h1>
<Outlet/>
}
>
<Route path="" view=|cx| view! { cx, <Nested/> }/>
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn SecondaryNav(cx: Scope) -> impl IntoView {
view! { cx,
<nav>
<A href="" exact=true>"Nested"</A>
<A href="single">"Single"</A>
<A href="parallel">"Parallel"</A>
<A href="inside-component">"Inside Component"</A>
</nav>
}
}
#[component]
fn Nested(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);
let two_second = create_resource(cx, || (), two_second_fn);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
"One Second: "
{move || {
one_second.read(cx).map(|_| "Loaded 1!")
}}
<br/><br/>
<Suspense fallback=|| "Loading 2...">
"Two Second: "
{move || {
two_second.read(cx).map(|_| "Loaded 2!")
}}
</Suspense>
</Suspense>
</div>
}
}
#[component]
fn Parallel(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);
let two_second = create_resource(cx, || (), two_second_fn);
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
"One Second: "
{move || {
one_second.read(cx).map(move |_| view! { cx,
"Loaded 1"
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
})
}}
</Suspense>
<br/><br/>
<Suspense fallback=|| "Loading 2...">
"Two Second: "
{move || {
two_second.read(cx).map(move |_| view! { cx,
"Loaded 2"
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
})
}}
</Suspense>
</div>
}
}
#[component]
fn Single(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
"One Second: "
{move || {
one_second.read(cx).map(|_| "Loaded 1!")
}}
</Suspense>
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
<div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</div>
</div>
}
}
#[component]
fn InsideComponent(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<p><code>"<Suspense/>"</code> " inside another component should work."</p>
<InsideComponentChild/>
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
<div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</div>
</div>
}
}
#[component]
fn InsideComponentChild(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);
view! { cx,
<Suspense fallback=|| "Loading 1...">
"One Second: "
{move || {
one_second.read(cx).map(|_| "Loaded 1!")
}}
</Suspense>
}
}

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ readme = "../README.md"
[dependencies]
config = "0.13.3"
fs = "0.0.5"
regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"

View File

@@ -50,10 +50,8 @@ pub fn add_event_listener<E>(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -76,13 +74,11 @@ pub(crate) fn add_event_listener_undelegated<E>(
{
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
leptos_reactive::SpecialNonReactiveZone::enter();
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
leptos_reactive::SpecialNonReactiveZone::exit();
}
}

View File

@@ -203,10 +203,8 @@ pub fn set_timeout_with_handle(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb();
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -254,10 +252,8 @@ pub fn debounce<T: 'static>(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |value| {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(value);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -323,10 +319,8 @@ pub fn set_interval(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb();
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -355,10 +349,8 @@ pub fn set_interval_with_handle(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb();
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -385,10 +377,8 @@ pub fn window_event_listener(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}

View File

@@ -781,7 +781,9 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
classes_signal()
let mut this = self;
let this = classes_signal()
.into_iter()
.map(Into::into)
.flat_map(|classes| {
@@ -790,7 +792,9 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
.map(ToString::to_string)
.collect::<SmallVec<[_; 4]>>()
})
.fold(self, |this, class| this.class(class, true))
.fold(this, |this, class| this.class(class, true));
this
}
}

View File

@@ -94,7 +94,12 @@ pub(crate) fn property_helper(
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
property_expression(&el, prop_name, new.clone());
if old.as_ref() != Some(&new)
&& !(old.is_none()
&& new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
new
});
}

View File

@@ -133,7 +133,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
let runtime = create_runtime();
let (
(shell, pending_resources, pending_fragments, serializers),
(shell, prefix, pending_resources, pending_fragments, serializers),
scope,
disposer,
) = run_scope_undisposed(runtime, {
@@ -146,81 +146,34 @@ 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, 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) });
}
for (fragment_id, (fut, _)) in pending_fragments {
fragments.push(async move { (fragment_id, fut.await) })
}
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
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)| {
// TODO can remove id_before_suspense entirely now
let fragments = fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
<script>
var id = "{fragment_id}";
var open = undefined;
var close = undefined;
var open;
var close;
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
while(walker.nextNode()) {{
if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{
@@ -238,7 +191,35 @@ fn fragments_to_chunks(
</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 {

View File

@@ -15,7 +15,7 @@ use leptos_reactive::{
create_runtime, run_scope_undisposed, suspense::StreamChunk, RuntimeId,
Scope, ScopeId,
};
use std::{borrow::Cow, collections::VecDeque};
use std::borrow::Cow;
/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s
/// loaded in `<Suspense/>` elements have finished loading.
@@ -80,48 +80,29 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
// create the runtime
let runtime = create_runtime();
let (
(
blocking_fragments_ready,
chunks,
prefix,
pending_resources,
serializers,
),
scope_id,
disposer,
) = run_scope_undisposed(runtime, |cx| {
// add additional context
additional_context(cx);
let ((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);
(
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 prefix = prefix(cx);
(
view.into_stream_chunks(cx),
prefix,
serde_json::to_string(&cx.pending_resources()).unwrap(),
cx.serialization_resolvers(),
)
});
let (tx, rx) = futures::channel::mpsc::unbounded();
let (prefix_tx, prefix_rx) = futures::channel::oneshot::channel();
leptos_reactive::spawn_local(async move {
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;
handle_chunks(tx, chunks).await;
});
let stream = futures::stream::once(async move {
let prefix = prefix_rx.await.expect("to receive prefix");
format!(
r#"
{prefix}
@@ -145,61 +126,18 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
}
#[async_recursion(?Send)]
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>,
) {
async fn handle_chunks(tx: UnboundedSender<String>, chunks: Vec<StreamChunk>) {
let mut buffer = String::new();
for chunk in chunks {
match chunk {
StreamChunk::Sync(sync) => buffer.push_str(&sync),
StreamChunk::Async { chunks, .. } => {
StreamChunk::Async(suspended) => {
// 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;
let suspended = suspended.await;
handle_chunks(tx.clone(), suspended).await;
}
}
@@ -211,8 +149,8 @@ async fn handle_chunks(
impl View {
/// Renders the view into a set of HTML chunks that can be streamed.
pub fn into_stream_chunks(self, cx: Scope) -> VecDeque<StreamChunk> {
let mut chunks = VecDeque::new();
pub fn into_stream_chunks(self, cx: Scope) -> Vec<StreamChunk> {
let mut chunks = Vec::new();
self.into_stream_chunks_helper(cx, &mut chunks);
chunks
}
@@ -220,42 +158,37 @@ impl View {
fn into_stream_chunks_helper(
self,
cx: Scope,
chunks: &mut VecDeque<StreamChunk>,
chunks: &mut Vec<StreamChunk>,
) {
match self {
View::Suspense(id, _) => {
let id = id.to_string();
if let Some(data) = cx.take_pending_fragment(&id) {
chunks.push_back(StreamChunk::Async {
chunks: data.in_order,
should_block: data.should_block,
});
if let Some((_, fragment)) = cx.take_pending_fragment(&id) {
chunks.push(StreamChunk::Async(fragment));
}
}
View::Text(node) => {
chunks.push_back(StreamChunk::Sync(node.content))
}
View::Text(node) => chunks.push(StreamChunk::Sync(node.content)),
View::Component(node) => {
cfg_if! {
if #[cfg(debug_assertions)] {
let name = crate::ssr::to_kebab_case(&node.name);
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-start-->"#, HydrationCtx::to_string(&node.id, false)).into()));
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-start-->"#, HydrationCtx::to_string(&node.id, false)).into()));
for child in node.children {
child.into_stream_chunks_helper(cx, chunks);
}
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-end-->"#, HydrationCtx::to_string(&node.id, true)).into()));
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-end-->"#, HydrationCtx::to_string(&node.id, true)).into()));
} else {
for child in node.children {
child.into_stream_chunks_helper(cx, chunks);
}
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}-->"#, HydrationCtx::to_string(&node.id, true)).into()))
chunks.push(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_back(StreamChunk::Sync(
chunks.push(StreamChunk::Sync(
format!("<!--leptos-view|{id}|open-->").into(),
));
}
@@ -263,7 +196,7 @@ impl View {
for chunk in el_chunks {
match chunk {
StringOrView::String(string) => {
chunks.push_back(StreamChunk::Sync(string))
chunks.push(StreamChunk::Sync(string))
}
StringOrView::View(view) => {
view().into_stream_chunks_helper(cx, chunks);
@@ -299,18 +232,18 @@ impl View {
.join("");
if el.is_void {
chunks.push_back(StreamChunk::Sync(
chunks.push(StreamChunk::Sync(
format!("<{tag_name}{attrs}/>").into(),
));
} else if let Some(inner_html) = inner_html {
chunks.push_back(StreamChunk::Sync(
chunks.push(StreamChunk::Sync(
format!(
"<{tag_name}{attrs}>{inner_html}</{tag_name}>"
)
.into(),
));
} else {
chunks.push_back(StreamChunk::Sync(
chunks.push(StreamChunk::Sync(
format!("<{tag_name}{attrs}>").into(),
));
@@ -322,20 +255,20 @@ impl View {
}
}
ElementChildren::InnerHtml(inner_html) => {
chunks.push_back(StreamChunk::Sync(inner_html));
chunks.push(StreamChunk::Sync(inner_html));
}
// handled above
ElementChildren::Chunks(_) => unreachable!(),
}
chunks.push_back(StreamChunk::Sync(
chunks.push(StreamChunk::Sync(
format!("</{tag_name}>").into(),
));
}
}
#[cfg(debug_assertions)]
if let Some(id) = &el.view_marker {
chunks.push_back(StreamChunk::Sync(
chunks.push(StreamChunk::Sync(
format!("<!--leptos-view|{id}|close-->").into(),
));
}
@@ -347,10 +280,10 @@ impl View {
u.id.clone(),
"",
false,
Box::new(move |chunks: &mut VecDeque<StreamChunk>| {
Box::new(move |chunks: &mut Vec<StreamChunk>| {
#[cfg(debug_assertions)]
{
chunks.push_back(StreamChunk::Sync(
chunks.push(StreamChunk::Sync(
format!(
"<!--hk={}|leptos-unit-->",
HydrationCtx::to_string(&u.id, true)
@@ -360,7 +293,7 @@ impl View {
}
#[cfg(not(debug_assertions))]
chunks.push_back(StreamChunk::Sync(
chunks.push(StreamChunk::Sync(
format!(
"<!--hk={}-->",
HydrationCtx::to_string(&u.id, true)
@@ -368,7 +301,7 @@ impl View {
.into(),
));
})
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
@@ -376,39 +309,34 @@ impl View {
node.id,
"dyn-child",
true,
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
)
Box::new(move |chunks: &mut Vec<StreamChunk>| {
if let Some(child) = *child {
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
if let View::Text(t) = child {
chunks.push(
if !cfg!(debug_assertions) {
StreamChunk::Sync(
format!("<!>{}", t.content)
.into(),
)
} else {
StreamChunk::Sync(t.content)
},
);
} else {
child.into_stream_chunks_helper(
cx, chunks,
);
}
)
} else {
StreamChunk::Sync(t.content)
},
);
} else {
child.into_stream_chunks_helper(
cx, chunks,
);
}
},
)
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
}
})
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
)
}
CoreComponent::Each(node) => {
@@ -417,40 +345,33 @@ impl View {
node.id,
"each",
true,
Box::new(
move |chunks: &mut VecDeque<StreamChunk>| {
for node in children.into_iter().flatten() {
let id = node.id;
Box::new(move |chunks: &mut Vec<StreamChunk>| {
for node in children.into_iter().flatten() {
let id = node.id;
#[cfg(debug_assertions)]
{
chunks.push_back(
StreamChunk::Sync(
format!(
#[cfg(debug_assertions)]
{
chunks.push(StreamChunk::Sync(
format!(
"<!--hk={}|leptos-each-item-start-->",
HydrationCtx::to_string(&id, false)
)
.into(),
),
);
node.child
.into_stream_chunks_helper(
cx, chunks,
);
chunks.push_back(
StreamChunk::Sync(
format!(
.into(),
));
node.child.into_stream_chunks_helper(
cx, chunks,
);
chunks.push(StreamChunk::Sync(
format!(
"<!--hk={}|leptos-each-item-end-->",
HydrationCtx::to_string(&id, true)
)
.into(),
),
);
}
.into(),
));
}
},
)
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
}
})
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
)
}
};
@@ -458,13 +379,13 @@ impl View {
if wrap {
cfg_if! {
if #[cfg(debug_assertions)] {
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-start-->", HydrationCtx::to_string(&id, false)).into()));
chunks.push(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-start-->", HydrationCtx::to_string(&id, false)).into()));
content(chunks);
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-end-->", HydrationCtx::to_string(&id, true)).into()));
chunks.push(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-end-->", HydrationCtx::to_string(&id, true)).into()));
} else {
let _ = name;
content(chunks);
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}-->", HydrationCtx::to_string(&id, true)).into()))
chunks.push(StreamChunk::Sync(format!("<!--hk={}-->", HydrationCtx::to_string(&id, true)).into()))
}
}
} else {

View File

@@ -43,7 +43,7 @@ impl Parse for Model {
"this method requires a `Scope` parameter";
help = "try `fn {}(cx: Scope, /* ... */)`", item.sig.ident
);
} else if !is_valid_scope_type(&props[0].ty) {
} else if props[0].ty != parse_quote!(Scope) {
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 !is_valid_into_view_return_type(&item.sig.output) {
if item.sig.output != parse_quote!(-> impl IntoView) {
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: ::leptos::Scope,
#scope_name: 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, .. }| !is_valid_scope_type(ty))
.filter(|Prop { ty, .. }| *ty != parse_quote!(Scope))
.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, .. }| !is_valid_scope_type(ty))
.filter(|Prop { ty, .. }| *ty != parse_quote!(Scope))
.map(|Prop { name, .. }| quote! { #name, })
.collect()
}
@@ -642,23 +642,3 @@ 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)
}

View File

@@ -201,15 +201,13 @@ mod template;
/// ```
///
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
/// [NodeRef](leptos_dom::NodeRef) 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 = create_node_ref::<Input>(cx);
/// let my_input = NodeRef::new(cx);
/// view! { cx, <input type="text" _ref=my_input/> }
/// // `my_input` now contains an `Element` that we can use anywhere
/// # ;
@@ -401,9 +399,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](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.
/// 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.
///
/// Every component function should have the return type `-> impl IntoView`.
///
@@ -578,10 +576,8 @@ 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](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)
/// 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)
/// 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
@@ -644,8 +640,8 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
.into()
}
/// 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.
/// 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.
///
/// 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.
@@ -661,8 +657,7 @@ 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](https://docs.rs/leptos/latest/leptos/struct.Scope.html),
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope),
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
/// server-side context into the server function.
///
@@ -685,7 +680,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 cant fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - **Return types must be [Serializable](https://docs.rs/leptos/latest/leptos/trait.Serializable.html).**
/// - **Return types must be [Serializable](leptos_reactive::Serializable).**
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
/// need to deserialize the result to return it to the client.
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
@@ -693,8 +688,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` 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
/// - **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
/// 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 {

View File

@@ -1392,7 +1392,7 @@ fn is_math_ml_element(tag: &str) -> bool {
}
fn is_ambiguous_element(tag: &str) -> bool {
tag == "a" || tag == "script" || tag == "title"
tag == "a" || tag == "script"
}
fn parse_event(event_name: &str) -> (&str, bool) {

View File

@@ -1,6 +1,5 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/component.rs");
t.compile_fail("tests/ui/component_absolute.rs");
t.compile_fail("tests/ui/*.rs");
}

View File

@@ -1,52 +0,0 @@
#[::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() {}

View File

@@ -1,53 +0,0 @@
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,
| ^

View File

@@ -1,77 +0,0 @@
use cfg_if::cfg_if;
// The point of these diagnostics is to give useful error messages when someone
// tries to access a reactive variable outside the reactive scope. They track when
// you create a signal/memo, and where you access it non-reactively.
#[cfg(debug_assertions)]
#[allow(dead_code)] // allowed for SSR
#[derive(Copy, Clone)]
pub(crate) struct AccessDiagnostics {
pub defined_at: &'static std::panic::Location<'static>,
pub called_at: &'static std::panic::Location<'static>,
}
#[cfg(not(debug_assertions))]
#[derive(Copy, Clone, Default)]
pub(crate) struct AccessDiagnostics {}
/// This just tracks whether we're currently in a context in which it really doesn't
/// matter whether something is reactive: for example, in an event listener or timeout.
/// Entering this zone basically turns off the warnings, and exiting it turns them back on.
/// All of this is a no-op in release mode.
#[doc(hidden)]
pub struct SpecialNonReactiveZone {}
cfg_if! {
if #[cfg(debug_assertions)] {
use std::cell::Cell;
thread_local! {
static IS_SPECIAL_ZONE: Cell<bool> = Cell::new(false);
}
}
}
impl SpecialNonReactiveZone {
#[allow(dead_code)] // allowed for SSR
pub(crate) fn is_inside() -> bool {
#[cfg(debug_assertions)]
{
IS_SPECIAL_ZONE.with(|val| val.get())
}
#[cfg(not(debug_assertions))]
false
}
pub fn enter() {
#[cfg(debug_assertions)]
{
IS_SPECIAL_ZONE.with(|val| val.set(true))
}
}
pub fn exit() {
#[cfg(debug_assertions)]
{
IS_SPECIAL_ZONE.with(|val| val.set(false))
}
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! diagnostics {
($this:ident) => {{
cfg_if! {
if #[cfg(debug_assertions)] {
AccessDiagnostics {
defined_at: $this.defined_at,
called_at: std::panic::Location::caller()
}
} else {
AccessDiagnostics { }
}
}
}};
}

View File

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

View File

@@ -72,8 +72,6 @@ extern crate tracing;
#[macro_use]
mod signal;
mod context;
#[macro_use]
mod diagnostics;
mod effect;
mod hydration;
mod memo;
@@ -92,7 +90,6 @@ mod stored_value;
pub mod suspense;
pub use context::*;
pub use diagnostics::SpecialNonReactiveZone;
pub use effect::*;
pub use memo::*;
pub use resource::*;
@@ -130,11 +127,8 @@ mod macros {
}
pub(crate) fn console_warn(s: &str) {
cfg_if::cfg_if! {
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(s));
} else {
eprintln!("{s}");
}
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
eprintln!("{s}");
#[cfg(any(feature = "csr", feature = "hydrate"))]
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(s));
}

View File

@@ -1,10 +1,9 @@
#![forbid(unsafe_code)]
use crate::{
create_effect, diagnostics::AccessDiagnostics, node::NodeId, on_cleanup,
with_runtime, AnyComputation, RuntimeId, Scope, SignalDispose, SignalGet,
SignalGetUntracked, SignalStream, SignalWith, SignalWithUntracked,
create_effect, node::NodeId, on_cleanup, with_runtime, AnyComputation,
RuntimeId, Scope, SignalDispose, SignalGet, SignalGetUntracked,
SignalStream, SignalWith, SignalWithUntracked,
};
use cfg_if::cfg_if;
use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
/// Creates an efficient derived reactive value based on other reactive values.
@@ -71,7 +70,6 @@ use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
)
)
)]
#[track_caller]
pub fn create_memo<T>(
cx: Scope,
f: impl Fn(Option<&T>) -> T + 'static,
@@ -308,7 +306,6 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
)
)
)]
#[track_caller]
fn get(&self) -> T {
self.with(T::clone)
}
@@ -326,7 +323,6 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
)
)
)]
#[track_caller]
fn try_get(&self) -> Option<T> {
self.try_with(T::clone)
}
@@ -346,7 +342,6 @@ impl<T> SignalWith<T> for Memo<T> {
)
)
)]
#[track_caller]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
match self.try_with(f) {
Some(t) => t,
@@ -370,16 +365,13 @@ impl<T> SignalWith<T> for Memo<T> {
)
)
)]
#[track_caller]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
// memo is stored as Option<T>, but will always have T available
// after latest_value() called, so we can unwrap safely
let f = move |maybe_value: &Option<T>| f(maybe_value.as_ref().unwrap());
let diagnostics = diagnostics!(self);
with_runtime(self.runtime, |runtime| {
self.id.subscribe(runtime, diagnostics);
self.id.subscribe(runtime);
self.id.try_with_no_subscription(runtime, f).ok()
})
.ok()

View File

@@ -106,73 +106,6 @@ 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 servers 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 pages 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,
@@ -199,7 +132,7 @@ where
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
suspense_contexts: Default::default(),
serializable,
serializable: true,
});
let id = with_runtime(cx.runtime, |runtime| {
@@ -323,7 +256,7 @@ where
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
suspense_contexts: Default::default(),
serializable: ResourceSerialization::Local,
serializable: false,
});
let id = with_runtime(cx.runtime, |runtime| {
@@ -627,19 +560,7 @@ where
resolved: Rc<Cell<bool>>,
scheduled: Rc<Cell<bool>>,
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
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,
serializable: bool,
}
impl<S, T> ResourceState<S, T>
@@ -679,14 +600,10 @@ where
let serializable = self.serializable;
if let Some(suspense_cx) = &suspense_cx {
if serializable != ResourceSerialization::Local {
if serializable {
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 \
@@ -712,12 +629,7 @@ 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 != ResourceSerialization::Local,
);
if serializable == ResourceSerialization::Blocking {
s.should_block.set_value(true);
}
s.increment(serializable);
}
}
}
@@ -758,12 +670,7 @@ where
let suspense_contexts = self.suspense_contexts.clone();
for suspense_context in suspense_contexts.borrow().iter() {
suspense_context.increment(
self.serializable != ResourceSerialization::Local,
);
if self.serializable == ResourceSerialization::Blocking {
suspense_context.should_block.set_value(true);
}
suspense_context.increment(self.serializable);
}
// run the Future
@@ -781,9 +688,7 @@ where
set_loading.update(|n| *n = false);
for suspense_context in suspense_contexts.borrow().iter() {
suspense_context.decrement(
serializable != ResourceSerialization::Local,
);
suspense_context.decrement(serializable);
}
}
})

View File

@@ -1,18 +1,13 @@
#![forbid(unsafe_code)]
use crate::{
console_warn,
hydration::FragmentData,
node::NodeId,
runtime::{with_runtime, RuntimeId},
suspense::StreamChunk,
PinnedFuture, ResourceId, SpecialNonReactiveZone, StoredValueId,
SuspenseContext,
PinnedFuture, ResourceId, StoredValueId, SuspenseContext,
};
use futures::stream::FuturesUnordered;
use std::{
collections::{HashMap, VecDeque},
fmt,
};
use std::{collections::HashMap, fmt};
#[doc(hidden)]
#[must_use = "Scope will leak memory if the disposer function is never called"]
@@ -177,11 +172,9 @@ impl Scope {
/// ```
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
with_runtime(self.runtime, |runtime| {
SpecialNonReactiveZone::enter();
let prev_observer = runtime.observer.take();
let untracked_result = f();
runtime.observer.set(prev_observer);
SpecialNonReactiveZone::exit();
untracked_result
})
.expect(
@@ -381,7 +374,7 @@ impl Scope {
context: SuspenseContext,
key: &str,
out_of_order_resolver: impl FnOnce() -> String + 'static,
in_order_resolver: impl FnOnce() -> VecDeque<StreamChunk> + 'static,
in_order_resolver: impl FnOnce() -> Vec<StreamChunk> + 'static,
) {
use crate::create_isomorphic_effect;
use futures::StreamExt;
@@ -390,7 +383,6 @@ 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
@@ -401,26 +393,21 @@ impl Scope {
if pending == 0 {
_ = tx1.unbounded_send(());
_ = tx2.unbounded_send(());
_ = tx3.unbounded_send(());
}
});
shared_context.pending_fragments.insert(
key.to_string(),
FragmentData {
out_of_order: Box::pin(async move {
(
Box::pin(async move {
rx1.next().await;
out_of_order_resolver()
}),
in_order: Box::pin(async move {
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;
})),
},
),
);
})
}
@@ -429,7 +416,10 @@ 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, FragmentData> {
pub fn pending_fragments(
&self,
) -> HashMap<String, (PinnedFuture<String>, PinnedFuture<Vec<StreamChunk>>)>
{
with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
std::mem::take(&mut shared_context.pending_fragments)
@@ -437,31 +427,14 @@ 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<FragmentData> {
pub fn take_pending_fragment(
&self,
id: &str,
) -> Option<(PinnedFuture<String>, PinnedFuture<Vec<StreamChunk>>)> {
with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context.pending_fragments.remove(id)

View File

@@ -1,7 +1,6 @@
#![forbid(unsafe_code)]
use crate::{
console_warn, create_effect, diagnostics,
diagnostics::*,
console_warn, create_effect,
macros::debug_warn,
node::NodeId,
on_cleanup,
@@ -533,7 +532,6 @@ impl<T: Clone> SignalGetUntracked<T> for ReadSignal<T> {
)
)
)]
#[track_caller]
fn try_get_untracked(&self) -> Option<T> {
with_runtime(self.runtime, |runtime| {
self.id.try_with_no_subscription(runtime, Clone::clone).ok()
@@ -574,17 +572,12 @@ impl<T> SignalWithUntracked<T> for ReadSignal<T> {
)
)
)]
#[track_caller]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, f, diagnostics)
})
.ok()
.transpose()
.ok()
.flatten()
with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
.ok()
.transpose()
.ok()
.flatten()
}
}
@@ -620,14 +613,9 @@ impl<T> SignalWith<T> for ReadSignal<T> {
)
)
)]
#[track_caller]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
let diagnostics = diagnostics!(self);
match with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, f, diagnostics)
})
.expect("runtime to be alive ")
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
.expect("runtime to be alive ")
{
Ok(o) => o,
Err(_) => panic_getting_dead_signal(
@@ -650,15 +638,10 @@ impl<T> SignalWith<T> for ReadSignal<T> {
)
)
)]
#[track_caller]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, f, diagnostics).ok()
})
.ok()
.flatten()
with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f).ok())
.ok()
.flatten()
}
}
@@ -689,12 +672,9 @@ impl<T: Clone> SignalGet<T> for ReadSignal<T> {
)
)
)]
#[track_caller]
fn get(&self) -> T {
let diagnostics = diagnostics!(self);
match with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, T::clone, diagnostics)
self.id.try_with(runtime, T::clone)
})
.expect("runtime to be alive")
{
@@ -771,16 +751,12 @@ where
/// Applies the function to the current Signal, if it exists, and subscribes
/// the running effect.
#[track_caller]
pub(crate) fn try_with<U>(
&self,
f: impl FnOnce(&T) -> U,
) -> Result<U, SignalError> {
let diagnostics = diagnostics!(self);
match with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, f, diagnostics)
}) {
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
{
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(e),
Err(_) => Err(SignalError::RuntimeDisposed),
@@ -1269,17 +1245,12 @@ impl<T> SignalWithUntracked<T> for RwSignal<T> {
)
)
)]
#[track_caller]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, f, diagnostics)
})
.ok()
.transpose()
.ok()
.flatten()
with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
.ok()
.transpose()
.ok()
.flatten()
}
}
@@ -1417,14 +1388,9 @@ impl<T> SignalWith<T> for RwSignal<T> {
)
)
)]
#[track_caller]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
let diagnostics = diagnostics!(self);
match with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, f, diagnostics)
})
.expect("runtime to be alive")
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
.expect("runtime to be alive")
{
Ok(o) => o,
Err(_) => panic_getting_dead_signal(
@@ -1447,15 +1413,10 @@ impl<T> SignalWith<T> for RwSignal<T> {
)
)
)]
#[track_caller]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, f, diagnostics).ok()
})
.ok()
.flatten()
with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f).ok())
.ok()
.flatten()
}
}
@@ -1487,15 +1448,12 @@ impl<T: Clone> SignalGet<T> for RwSignal<T> {
)
)
)]
#[track_caller]
fn get(&self) -> T
where
T: Clone,
{
let diagnostics = diagnostics!(self);
match with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, T::clone, diagnostics)
self.id.try_with(runtime, T::clone)
})
.expect("runtime to be alive")
{
@@ -1520,12 +1478,9 @@ impl<T: Clone> SignalGet<T> for RwSignal<T> {
)
)
)]
#[track_caller]
fn try_get(&self) -> Option<T> {
let diagnostics = diagnostics!(self);
with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, Clone::clone, diagnostics).ok()
self.id.try_with(runtime, Clone::clone).ok()
})
.ok()
.flatten()
@@ -1808,12 +1763,7 @@ pub(crate) enum SignalError {
}
impl NodeId {
#[track_caller]
pub(crate) fn subscribe(
&self,
runtime: &Runtime,
diagnostics: AccessDiagnostics,
) {
pub(crate) fn subscribe(&self, runtime: &Runtime) {
// add subscriber
if let Some(observer) = runtime.observer.get() {
// add this observer to this node's dependencies (to allow notification)
@@ -1828,36 +1778,9 @@ impl NodeId {
let sources = sources.or_default();
sources.borrow_mut().insert(*self);
}
} else {
#[cfg(all(debug_assertions, not(feature = "ssr")))]
{
if !SpecialNonReactiveZone::is_inside() {
let AccessDiagnostics {
called_at,
defined_at,
} = diagnostics;
crate::macros::debug_warn!(
"At {called_at}, you access a signal or memo (defined \
at {defined_at}) outside a reactive tracking \
context. This might mean your app is not responding \
to changes in signal values in the way you \
expect.\n\nHeres how to fix it:\n\n1. If this is \
inside a `view!` macro, make sure you are passing a \
function, not a value.\n ❌ NO <p>{{x.get() * \
2}}</p>\n ✅ YES <p>{{move || x.get() * \
2}}</p>\n\n2. If its in the body of a component, \
try wrapping this access in a closure: \n ❌ NO \
let y = x.get() * 2\n ✅ YES let y = move || \
x.get() * 2.\n\n3. If youre *trying* to access the \
value without tracking, use `.get_untracked()` or \
`.with_untracked()` instead."
);
}
}
}
}
#[track_caller]
pub(crate) fn try_with_no_subscription<T, U>(
&self,
runtime: &Runtime,
@@ -1881,17 +1804,15 @@ impl NodeId {
Ok(f(value))
}
#[track_caller]
pub(crate) fn try_with<T, U>(
&self,
runtime: &Runtime,
f: impl FnOnce(&T) -> U,
diagnostics: AccessDiagnostics,
) -> Result<U, SignalError>
where
T: 'static,
{
self.subscribe(runtime, diagnostics);
self.subscribe(runtime);
self.try_with_no_subscription(runtime, f)
}

View File

@@ -6,7 +6,7 @@ use crate::{
RwSignal, Scope, SignalUpdate, StoredValue, WriteSignal,
};
use futures::Future;
use std::{borrow::Cow, collections::VecDeque, pin::Pin};
use std::{borrow::Cow, pin::Pin};
/// Tracks [Resource](crate::Resource)s that are read under a suspense context,
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
@@ -17,21 +17,13 @@ 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 serialized
/// Whether the suspense contains local resources at this moment, and therefore can't be
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 {
@@ -54,13 +46,11 @@ 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,
}
}
@@ -111,19 +101,14 @@ pub enum StreamChunk {
/// A chunk of synchronous HTML.
Sync(Cow<'static, str>),
/// A future that resolves to be a list of additional chunks.
Async {
/// The HTML chunks this contains.
chunks: Pin<Box<dyn Future<Output = VecDeque<StreamChunk>>>>,
/// Whether this should block the stream.
should_block: bool,
},
Async(Pin<Box<dyn Future<Output = Vec<StreamChunk>>>>),
}
impl std::fmt::Debug for StreamChunk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StreamChunk::Sync(data) => write!(f, "StreamChunk::Sync({data:?})"),
StreamChunk::Async { .. } => write!(f, "StreamChunk::Async(_)"),
StreamChunk::Async(_) => write!(f, "StreamChunk::Async(_)"),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.2.5"
version = "0.2.4"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,44 +0,0 @@
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!()
}
}

View File

@@ -1,4 +1,4 @@
use crate::{additional_attributes::AdditionalAttributes, TextProp};
use crate::TextProp;
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, rc::Rc};
@@ -7,37 +7,15 @@ 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> {
let class = self
.class
self.class
.borrow()
.as_ref()
.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)
}
.map(|class| format!(" class=\"{}\"", class.get()))
}
}
@@ -79,41 +57,20 @@ 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(debug_assertions)]
crate::feature_warning();
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, {
let el = el.clone();
move |_| {
let value = class.get();
_ = el.set_attribute("class", &value);
}
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.body.class.borrow_mut() = class;
*meta.body.attributes.borrow_mut() = attributes;
}
}
}

View File

@@ -1,4 +1,4 @@
use crate::{additional_attributes::AdditionalAttributes, TextProp};
use crate::TextProp;
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, rc::Rc};
@@ -9,7 +9,6 @@ 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 {
@@ -30,16 +29,7 @@ impl HtmlContext {
.borrow()
.as_ref()
.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 = [lang, dir, class, attributes]
let mut val = [lang, dir, class]
.into_iter()
.flatten()
.collect::<Vec<_>>()
@@ -72,12 +62,7 @@ impl std::fmt::Debug for HtmlContext {
///
/// view! { cx,
/// <main>
/// <Html
/// lang="he"
/// dir="rtl"
/// // arbitrary additional attributes can be passed via `attributes`
/// attributes=AdditionalAttributes::from(vec![("data-theme", "dark")])
/// />
/// <Html lang="he" dir="rtl"/>
/// </main>
/// }
/// }
@@ -94,13 +79,7 @@ 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(debug_assertions)]
crate::feature_warning();
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
let el = document().document_element().expect("there to be a <html> element");
@@ -122,29 +101,16 @@ 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;
}
}
}

View File

@@ -58,7 +58,6 @@ use std::{
#[cfg(any(feature = "csr", feature = "hydrate"))]
use wasm_bindgen::{JsCast, UnwrapThrowExt};
mod additional_attributes;
mod body;
mod html;
mod link;
@@ -67,7 +66,6 @@ mod script;
mod style;
mod stylesheet;
mod title;
pub use additional_attributes::*;
pub use body::*;
pub use html::*;
pub use link::*;
@@ -206,9 +204,6 @@ pub fn provide_meta_context(cx: Scope) {
/// call `use_head()` but a single [MetaContext] has not been provided at the application root.
/// The best practice is always to call [provide_meta_context] early in the application.
pub fn use_head(cx: Scope) -> MetaContext {
#[cfg(debug_assertions)]
feature_warning();
match use_context::<MetaContext>(cx) {
None => {
debug_warn!(
@@ -287,15 +282,6 @@ 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()
@@ -305,7 +291,7 @@ pub fn generate_head_metadata_separated(cx: Scope) -> (String, String) {
.as_ref()
.and_then(|meta| meta.body.as_string())
.unwrap_or_default();
(head, format!("<body{body_meta}>"))
format!("{head}</head><body{body_meta}>")
}
/// Describes a value that is either a static or a reactive string, i.e.,
@@ -346,10 +332,3 @@ where
TextProp(Rc::new(s))
}
}
#[cfg(debug_assertions)]
pub(crate) fn feature_warning() {
if !cfg!(any(feature = "csr", feature = "hydrate", feature = "ssr")) {
leptos::debug_warn!("WARNING: `leptos_meta` does nothing unless you enable one of its features (`csr`, `hydrate`, or `ssr`). See the docs at https://docs.rs/leptos_meta/latest/leptos_meta/ for more information.");
}
}

View File

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

View File

@@ -85,7 +85,6 @@ where
// POST
if method == "post" {
ev.prevent_default();
ev.stop_propagation();
let on_response = on_response.clone();
spawn_local(async move {
@@ -145,7 +144,6 @@ where
.is_ok()
{
ev.prevent_default();
ev.stop_propagation();
}
}
};
@@ -350,7 +348,6 @@ where
}
Ok(input) => {
ev.prevent_default();
ev.stop_propagation();
multi_action.dispatch(input);
if let Some(error) = error {
error.set(None);

View File

@@ -62,9 +62,6 @@ 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
@@ -78,7 +75,6 @@ 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")))]
@@ -113,7 +109,6 @@ where
prop:replace={replace}
aria-current=move || if is_active.get() { Some("page") } else { None }
class=class
id=id
>
{children(cx)}
</a>
@@ -121,5 +116,5 @@ where
}
let href = use_resolved_path(cx, move || href.to_href()());
inner(cx, href, exact, state, replace, class, id, children)
inner(cx, href, exact, state, replace, class, children)
}

View File

@@ -34,25 +34,10 @@ where
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>(cx) {
(redirect_fn.f)(&path);
}
// redirect on the client
else {
let navigate = use_navigate(cx);
#[cfg(any(feature = "csr", feature = "hydrate"))]
leptos::request_animation_frame(move || {
if let Err(e) = navigate(&path, options.unwrap_or_default()) {
leptos::error!("<Redirect/> error: {e:?}");
}
});
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
{
leptos::debug_warn!(
"<Redirect/> is trying to redirect without \
`ServerRedirectFunction` being provided. (If youre getting \
this on initial server start-up, its okay to ignore. It \
just means that your root route is a redirect.)"
);
}
}
let navigate = use_navigate(cx);
navigate(&path, options.unwrap_or_default())
}
/// Wrapping type for a function provided as context to allow for

View File

@@ -34,7 +34,44 @@ where
F: Fn(Scope) -> E + 'static,
P: std::fmt::Display,
{
define_route(
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(
cx,
children,
path.to_string(),
@@ -43,91 +80,6 @@ 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)

View File

@@ -104,7 +104,7 @@ impl RouterContext {
let base_path = resolve_path("", base, None);
if let Some(base_path) = &base_path {
if source.with_untracked(|s| s.value.is_empty()) {
if source.with(|s| s.value.is_empty()) {
history.navigate(&LocationChange {
value: base_path.to_string(),
replace: true,
@@ -116,11 +116,11 @@ impl RouterContext {
// the current URL
let (reference, set_reference) =
create_signal(cx, source.with_untracked(|s| s.value.clone()));
create_signal(cx, source.with(|s| s.value.clone()));
// the current History.state
let (state, set_state) =
create_signal(cx, source.with_untracked(|s| s.state.clone()));
create_signal(cx, source.with(|s| s.state.clone()));
// we'll use this transition to wait for async resources to load when navigating to a new route
#[cfg(feature = "transition")]

View File

@@ -32,17 +32,9 @@ pub fn Routes(
.as_children()
.iter()
.filter_map(|child| {
let def = child
child
.as_transparent()
.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
.and_then(|t| t.downcast_ref::<RouteDefinition>())
})
.cloned()
.collect::<Vec<_>>();

View File

@@ -19,4 +19,4 @@ server_fn = { version = "0.2" }
serde = "1"
[features]
stable = ["server_fn_macro/stable"]
stable = []

View File

@@ -6,9 +6,8 @@ use proc_macro::TokenStream;
use server_fn_macro::server_macro_impl;
use syn::__private::ToTokens;
/// 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.
/// 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.
///
/// 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
@@ -42,7 +41,7 @@ use syn::__private::ToTokens;
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - **Return types must implement [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// - **Return types must implement [Serialize](serde::Serialize).**
/// 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)

View File

@@ -114,6 +114,18 @@ pub fn server_macro_impl(
.as_ref()
.and_then(|ctx| fn_arg_is_cx(f, ctx).then_some(f))
});
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
if let Pat::Ident(id) = &*arg.pat {
quote! {
#[allow(unused)]
let #id = cx;
}
} else {
quote! {}
}
} else {
quote! {}
};
let cx_fn_arg = if cx_arg.is_some() {
quote! { cx, }
} else {
@@ -128,9 +140,9 @@ pub fn server_macro_impl(
FnArg::Typed(t) => t,
};
let is_cx = if let Some(ctx) = &server_context {
fn_arg_is_cx(f, ctx)
!fn_arg_is_cx(f, ctx)
} else {
false
true
};
if is_cx {
quote! {
@@ -218,6 +230,7 @@ pub fn server_macro_impl(
#[cfg(feature = "ssr")]
fn call_fn(self, cx: #server_ctx_path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, server_fn::ServerFnError>>>> {
let #struct_name { #(#field_names),* } = self;
#cx_assign_statement;
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_2),*).await })
}
@@ -234,7 +247,6 @@ pub fn server_macro_impl(
}
#[cfg(not(feature = "ssr"))]
#[allow(unused_variables)]
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
let prefix = #struct_name::prefix().to_string();
let url = prefix + "/" + #struct_name::url();