mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
73 Commits
optional-p
...
mutually-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1adddce0ed | ||
|
|
c8e5d0518a | ||
|
|
c334c576ab | ||
|
|
e14b970291 | ||
|
|
7a5a776cb9 | ||
|
|
06f782aa13 | ||
|
|
6b825fec37 | ||
|
|
b452d8af40 | ||
|
|
e96f1d2129 | ||
|
|
72d6af9c84 | ||
|
|
8198cd0b68 | ||
|
|
fe68b47ba2 | ||
|
|
384d39543c | ||
|
|
225e62d12f | ||
|
|
3905a2aa60 | ||
|
|
ff6ce2dac0 | ||
|
|
16675cbff2 | ||
|
|
9524c6e289 | ||
|
|
bc316c648c | ||
|
|
6753ba21c4 | ||
|
|
efbe32e081 | ||
|
|
55fd6d44f9 | ||
|
|
90972f2d94 | ||
|
|
7382c7e51c | ||
|
|
8a6d129575 | ||
|
|
e20c77710d | ||
|
|
93da88eac0 | ||
|
|
5072539917 | ||
|
|
78c59df1d1 | ||
|
|
75e40eafb2 | ||
|
|
274a1ac5f0 | ||
|
|
17040a4af4 | ||
|
|
b09a5f905e | ||
|
|
683511f311 | ||
|
|
151c58733b | ||
|
|
012ff56cd6 | ||
|
|
493c805993 | ||
|
|
764192af36 | ||
|
|
f969fd7eff | ||
|
|
2c7ee0d415 | ||
|
|
5430c78e18 | ||
|
|
6b052557d1 | ||
|
|
70f3edb0f5 | ||
|
|
4e1f963750 | ||
|
|
3c3d3b33f1 | ||
|
|
be7b9eea25 | ||
|
|
016ad6b7a6 | ||
|
|
60b96c9118 | ||
|
|
7ccb2d9f44 | ||
|
|
2c2090a194 | ||
|
|
de9b2998ac | ||
|
|
29b81a3d50 | ||
|
|
5bc0d89ce7 | ||
|
|
342b10c232 | ||
|
|
ba9d3c1602 | ||
|
|
d3b3ce6980 | ||
|
|
4b79a91287 | ||
|
|
de06c9b2ca | ||
|
|
84c7d00ea9 | ||
|
|
8f5ae0054d | ||
|
|
374f0c4e27 | ||
|
|
a6170f4da9 | ||
|
|
578dd5ef35 | ||
|
|
934a131deb | ||
|
|
5bc1c36e67 | ||
|
|
b1b9853f92 | ||
|
|
5d6a083d1d | ||
|
|
b51da35a9a | ||
|
|
164dcd1b97 | ||
|
|
c0964c2b01 | ||
|
|
af5b226e53 | ||
|
|
3a1db3a191 | ||
|
|
54370e3153 |
13
README.md
13
README.md
@@ -6,6 +6,7 @@
|
||||
[](https://crates.io/crates/leptos)
|
||||
[](https://docs.rs/leptos)
|
||||
[](https://discord.gg/YdRAhS7eQB)
|
||||
[](https://matrix.to/#/#leptos:matrix.org)
|
||||
|
||||
# Leptos
|
||||
|
||||
@@ -65,9 +66,9 @@ Here are some resources for learning more about Leptos:
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` Rust.
|
||||
Most of the examples assume you’re using `nightly` version of Rust. For this, you can either set your toolchain globally or on per-project basis.
|
||||
|
||||
To set up your Rust toolchain using `nightly` (and add the ability to compile Rust to WebAssembly, if you haven’t already)
|
||||
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you haven’t already):
|
||||
|
||||
```
|
||||
rustup toolchain install nightly
|
||||
@@ -75,6 +76,14 @@ rustup default nightly
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
|
||||
|
||||
```toml
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
```
|
||||
|
||||
If you’re on `stable`, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
|
||||
|
||||
@@ -10,8 +10,8 @@ fn leptos_deep_creation(b: &mut Bencher) {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
for _ in 0..1000usize {
|
||||
let prev = memos.last().copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
@@ -34,9 +34,8 @@ fn leptos_deep_update(b: &mut Bencher) {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
@@ -242,9 +241,8 @@ fn l021_deep_creation(b: &mut Bencher) {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
@@ -266,9 +264,8 @@ fn l021_deep_update(b: &mut Bencher) {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
@@ -444,9 +441,8 @@ fn sycamore_deep_creation(b: &mut Bencher) {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
@@ -465,9 +461,8 @@ fn sycamore_deep_update(b: &mut Bencher) {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
|
||||
@@ -27,11 +27,12 @@
|
||||
- [Params and Queries](./router/18_params_and_queries.md)
|
||||
- [`<A/>`](./router/19_a.md)
|
||||
- [`<Form/>`](./router/20_form.md)
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Interlude: Styling](./interlude_styling.md)
|
||||
- [Metadata]()
|
||||
- [SSR]()
|
||||
- [Models of SSR]()
|
||||
- [Server Side Rendering](./ssr/README.md)
|
||||
- [`cargo-leptos`]()
|
||||
- [The Life of a Page Load](./ssr/21_life_cycle.md)
|
||||
- [Async Rendering and SSR “Modes”](./ssr/22_ssr_modes.md)
|
||||
- [Hydration Footguns]()
|
||||
- [Request/Response]()
|
||||
- [Extractors]()
|
||||
|
||||
@@ -26,11 +26,11 @@ let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(cx), b.read(cx)) {
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
(Some(a), Some(b)) => view! { cx,
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
}.into_view(cx)
|
||||
}.into_view(cx),
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
112
docs/book/src/interlude_styling.md
Normal file
112
docs/book/src/interlude_styling.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Interlude: Styling
|
||||
|
||||
Anyone creating a website or application soon runs into the question of styling. For a small app, a single CSS file is probably plenty to style your user interface. But as an application grows, many developers find that plain CSS becomes increasingly hard to manage.
|
||||
|
||||
Some frontend frameworks (like Angular, Vue, and Svelte) provide built-in ways to scope your CSS to particular components, making it easier to manage styles across a whole application without styles meant to modify one small component having a global effect. Other frameworks (like React or Solid) don’t provide built-in CSS scoping, but rely on libraries in the ecosystem to do it for them. Leptos is in this latter camp: the framework itself has no opinions about CSS at all, but provides a few tools and primitives that allow others to build styling libraries.
|
||||
|
||||
Here are a few different approaches to styling your Leptos app, other than plain CSS.
|
||||
|
||||
## TailwindCSS: Utility-first CSS
|
||||
|
||||
[TailwindCSS](https://tailwindcss.com/) is a popular utility-first CSS library. It allows you to style your application by using inline utility classes, with a custom CLI tool that scans your files for Tailwind class names and bundles the necessary CSS.
|
||||
|
||||
This allows you to write components like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn Home(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<main class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
<button
|
||||
class="bg-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
{move || if count() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count().to_string()
|
||||
}}
|
||||
</button>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It can be a little complicated to set up the Tailwind integration at first, but you can check out our two examples of how to use Tailwind with a [client-side-rendered `trunk` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr_trunk) or with a [server-rendered `cargo-leptos` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind). `cargo-leptos` also has some [built-in Tailwind support](https://github.com/leptos-rs/cargo-leptos#site-parameters) that you can use as an alternative to Tailwind’s CLI.
|
||||
|
||||
## Stylers: Compile-time CSS Extraction
|
||||
|
||||
[Stylers](https://github.com/abishekatp/stylers) is a compile-time scoped CSS library that lets you declare scoped CSS in the body of your component. Stylers will extract this CSS at compile time into CSS files that you can then import into your app, which means that it doesn’t add anything to the WASM binary size of your application.
|
||||
|
||||
This allows you to write components like this:
|
||||
|
||||
```rust
|
||||
use stylers::style;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let styler_class = style! { "App",
|
||||
#two{
|
||||
color: blue;
|
||||
}
|
||||
div.one{
|
||||
color: red;
|
||||
content: raw_str(r#"\hello"#);
|
||||
font: "1.3em/1.2" Arial, Helvetica, sans-serif;
|
||||
}
|
||||
div {
|
||||
border: 1px solid black;
|
||||
margin: 25px 50px 75px 100px;
|
||||
background-color: lightblue;
|
||||
}
|
||||
h2 {
|
||||
color: purple;
|
||||
}
|
||||
@media only screen and (max-width: 1000px) {
|
||||
h3 {
|
||||
background-color: lightblue;
|
||||
color: blue
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx, class = styler_class,
|
||||
<div class="one">
|
||||
<h1 id="two">"Hello"</h1>
|
||||
<h2>"World"</h2>
|
||||
<h2>"and"</h2>
|
||||
<h3>"friends!"</h3>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Styled: Runtime CSS Scoping
|
||||
|
||||
[Styled](https://github.com/eboody/styled) is a runtime scoped CSS library that integrates well with Leptos. It lets you declare scoped CSS in the body of your component function, and then applies those styles at runtime.
|
||||
|
||||
```rust
|
||||
use styled::style;
|
||||
|
||||
#[component]
|
||||
pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
let styles = style!(
|
||||
div {
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
);
|
||||
|
||||
styled::view! { cx, styles,
|
||||
<div>"This text should be red with white text."</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Contributions Welcome
|
||||
|
||||
Leptos no opinions on how you style your website or app, but we’re very happy to provide support to any tools you’re trying to create to make it easier. If you’re working on a CSS or styling approach that you’d like to add to this list, please let us know!
|
||||
43
docs/book/src/ssr/21_life_cycle.md
Normal file
43
docs/book/src/ssr/21_life_cycle.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# The Life of a Page Load
|
||||
|
||||
Before we get into the weeds it might be helpful to have a higher-level overview. What exactly happens between the moment you type in the URL of a server-rendered Leptos app, and the moment you click a button and a counter increases?
|
||||
|
||||
I’m assuming some basic knowledge of how the Internet works here, and won’t get into the weeds about HTTP or whatever. Instead, I’ll try to show how different parts of the Leptos APIs map onto each part of the process.
|
||||
|
||||
This description also starts from the premise that your app is being compiled for two separate targets:
|
||||
|
||||
1. A server version, often running on Actix or Axum, compiled with the Leptos `ssr` feature
|
||||
2. A browser version, compiled to WebAssembly (WASM) with the Leptos `hydrate` feature
|
||||
|
||||
The [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool exists to coordinate the process of compiling your app for these two different targets.
|
||||
|
||||
## On the Server
|
||||
|
||||
- Your browser makes a `GET` request for that URL to your server. At this point, the browser knows almost nothing about the page that’s going to be rendered. (The question “How does the browser know where to ask for the page?” is an interesting one, but out of the scope of this tutorial!)
|
||||
- The server receives that request, and checks whether it has a way to handle a `GET` request at that path. This is what the `.leptos_routes()` methods in [`leptos_axum`](https://docs.rs/leptos_axum/0.2.5/leptos_axum/trait.LeptosRoutes.html) and [`leptos_actix`](https://docs.rs/leptos_actix/0.2.5/leptos_actix/trait.LeptosRoutes.html) are for. When the server starts up, these methods walk over the routing structure you provide in `<Routes/>`, generating a list of all possible routes your app can handle and telling the server’s router “for each of these routes, if you get a request... hand it off to Leptos.”
|
||||
- The server sees that this route can be handled by Leptos. So it renders your root component (often called something like `<App/>`), providing it with the URL that’s being requested and some other data like the HTTP headers and request metadata.
|
||||
- Your application runs once on the server, building up an HTML version of the component tree that will be rendered at that route. (There’s more to be said here about resources and `<Suspense/>` in the next chapter.)
|
||||
- The server returns this HTML page, also injecting information on how to load the version of your app that has been compiled to WASM so that it can run in the browser.
|
||||
|
||||
> The HTML page that’s returned is essentially your app, “dehydrated” or “freeze-dried”: it is HTML without any of the reactivity or event listeners you’ve added. The browser will “rehydrate” this HTML page by adding the reactive system and attaching event listeners to that server-rendered HTML. Hence the two feature flags that apply to the two halves of this process: `ssr` on the server for “server-side rendering”, and `hydrate` in the browser for that process of rehydration.
|
||||
|
||||
## In the Browser
|
||||
|
||||
- The browser receives this HTML page from the server. It immediately goes back to the server to begin loading the JS and WASM necessary to run the interactive, client side version of the app.
|
||||
- In the meantime, it renders the HTML version.
|
||||
- When the WASM version has reloaded, it does the same route-matching process that the server did. Because the `<Routes/>` component is identical on the server and in the client, the browser version will read the URL and render the same page that was already returned by the server.
|
||||
- During this initial “hydration” phase, the WASM version of your app doesn’t re-create the DOM nodes that make up your application. Instead, it walks over the existing HTML tree, “picking up” existing elements and adding the necessary interactivity.
|
||||
|
||||
> Note that there are some trade-offs here. Before this hydration process is complete, the page will _appear_ interactive but won’t actually respond to interactions. For example, if you have a counter button and click it before WASM has loaded, the count will not increment, because the necessary event listeners and reactivity have not been added yet. We’ll look at some ways to build in “graceful degradation” in future chapters.
|
||||
|
||||
## Client-Side Navigation
|
||||
|
||||
The next step is very important. Imagine that the user now clicks a link to navigate to another page in your application.
|
||||
|
||||
The browser will _not_ make another round trip to the server, reloading the full page as it would for navigating between plain HTML pages or an application that uses server rendering (for example with PHP) but without a client-side half.
|
||||
|
||||
Instead, the WASM version of your app will load the new page, right there in the browser, without requesting another page from the server. Essentially, your app upgrades itself from a server-loaded “multi-page app” into a browser-rendered “single-page app.” This yields the best of both worlds: a fast initial load time due to the server-rendered HTML, and fast secondary navigations because of the client-side routing.
|
||||
|
||||
Some of what will be described in the following chapters—like the interactions between server functions, resources, and `<Suspense/>`—may seem overly complicated. You might find yourself asking, “If my page is being rendered to HTML on the server, why can’t I just `.await` this on the server? If I can just call library X in a server function, why can’t I call it in my component?” The reason is pretty simple: to enable the upgrade from server rendering to client rendering, everything in your application must be able to run either on the server or in the browser.
|
||||
|
||||
This is not the only way to create a website or web framework, of course. But it’s the most common way, and we happen to think it’s quite a good way, to create the smoothest possible experience for your users.
|
||||
122
docs/book/src/ssr/22_ssr_modes.md
Normal file
122
docs/book/src/ssr/22_ssr_modes.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Async Rendering and SSR “Modes”
|
||||
|
||||
Server-rendering a page that uses only synchronous data is pretty simple: You just walk down the component tree, rendering each element to an HTML string. But this is a pretty big caveat: it doesn’t answer the question of what we should do with pages that includes asynchronous data, i.e., the sort of stuff that would be rendered under a `<Suspense/>` node on the client.
|
||||
|
||||
When a page loads async data that it needs to render, what should we do? Should we wait for all the async data to load, and then render everything at once? (Let’s call this “async” rendering) Should we go all the way in the opposite direction, just sending the HTML we have immediately down to the client and letting the client load the resources and fill them in? (Let’s call this “synchronous” rendering) Or is there some middle-ground solution that somehow beats them both? (Hint: There is.)
|
||||
|
||||
If you’ve ever listened to streaming music or watched a video online, I’m sure you realize that HTTP supports streaming, allowing a single connection to send chunks of data one after another without waiting for the full content to load. You may not realize that browsers are also really good at rendering partial HTML pages. Taken together, this means that you can actually enhance your users’ experience by **streaming HTML**: and this is something that Leptos supports out of the box, with no configuration at all. And there’s actually more than one way to stream HTML: you can stream the chunks of HTML that make up your page in order, like frames of a video, or you can stream them... well, out of order.
|
||||
|
||||
Let me say a little more about what I mean.
|
||||
|
||||
Leptos supports all four different of these different ways to render HTML that includes asynchronous data.
|
||||
|
||||
## Synchronous Rendering
|
||||
|
||||
1. **Synchronous**: Serve an HTML shell that includes `fallback` for any `<Suspense/>`. Load data on the client using `create_local_resource`, replacing `fallback` once resources are loaded.
|
||||
|
||||
- _Pros_: App shell appears very quickly: great TTFB (time to first byte).
|
||||
- _Cons_
|
||||
- Resources load relatively slowly; you need to wait for JS + WASM to load before even making a request.
|
||||
- No ability to include data from async resources in the `<title>` or other `<meta>` tags, hurting SEO and things like social media link previews.
|
||||
|
||||
If you’re using server-side rendering, the synchronous mode is almost never what you actually want, from a performance perspective. This is because it misses out on an important optimization. If you’re loading async resources during server rendering, you can actually begin loading the data on the server. Rather than waiting for the client to receive the HTML response, then loading its JS + WASM, _then_ realize it needs the resources and begin loading them, server rendering can actually begin loading the resources when the client first makes the response. In this sense, during server rendering an async resource is like a `Future` that begins loading on the server and resolves on the client. As long as the resources are actually serializable, this will always lead to a faster total load time.
|
||||
|
||||
> This is why [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) requires resources data to be serializable by default, and why you need to explicitly use [`create_local_resource`](https://docs.rs/leptos/latest/leptos/fn.create_local_resource.html) for any async data that is not serializable and should therefore only be loaded in the browser itself. Creating a local resource when you could create a serializable resource is always a deoptimization.
|
||||
|
||||
## Async Rendering
|
||||
|
||||
<video controls>
|
||||
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/async.mov?raw=true" type="video/mp4">
|
||||
</video>
|
||||
|
||||
2. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
|
||||
|
||||
- _Pros_: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
|
||||
- _Cons_: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client. The page is totally blank until everything is loaded.
|
||||
|
||||
## In-Order Streaming
|
||||
|
||||
<video controls>
|
||||
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/in-order.mov?raw=true" type="video/mp4">
|
||||
</video>
|
||||
|
||||
3. **In-order streaming**: Walk through the component tree, rendering HTML until you hit a `<Suspense/>`. Send down all the HTML you’ve got so far as a chunk in the stream, wait for all the resources accessed under the `<Suspense/>` to load, then render it to HTML and keep walking until you hit another `<Suspense/>` or the end of the page.
|
||||
|
||||
- _Pros_: Rather than a blank screen, shows at least _something_ before the data are ready.
|
||||
- _Cons_
|
||||
- Loads the shell more slowly than synchronous rendering (or out-of-order streaming) because it needs to pause at every `<Suspense/>`.
|
||||
- Unable to show fallback states for `<Suspense/>`.
|
||||
- Can’t begin hydration until the entire page has loaded, so earlier pieces of the page will not be interactive until the suspended chunks have loaded.
|
||||
|
||||
## Out-of-Order Streaming
|
||||
|
||||
<video controls>
|
||||
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/out-of-order.mov?raw=true" type="video/mp4">
|
||||
</video>
|
||||
|
||||
4. **Out-of-order streaming**: Like synchronous rendering, serve an HTML shell that includes `fallback` for any `<Suspense/>`. But load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `<Suspense/>` nodes, which is swapped in to replace the fallback.
|
||||
|
||||
- _Pros_: Combines the best of **synchronous** and **`async`**.
|
||||
- Fast initial response/TTFB because it immediately sends the whole synchronous shell
|
||||
- Fast total time because resources begin loading on the server.
|
||||
- Able to show the fallback loading state and dynamically replace it, instead of showing blank sections for un-loaded data.
|
||||
- _Cons_: Requires JavaScript to be enabled for suspended fragments to appear in correct order. (This small chunk of JS streamed down in a `<script>` tag alongside the `<template>` tag that contains the rendered `<Suspense/>` fragment, so it does not need to load any additional JS files.)
|
||||
|
||||
## Using SSR Modes
|
||||
|
||||
Because it offers the best blend of performance characteristics, Leptos defaults to out-of-order streaming. But it’s really simple to opt into these different modes. You do it by adding an `ssr` property onto one or more of your `<Route/>` components, like in the [`ssr_modes` example](https://github.com/leptos-rs/leptos/blob/main/examples/ssr_modes/src/app.rs).
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
|
||||
|
||||
// We'll load the posts with async rendering, so they can set
|
||||
// the title and metadata *after* loading the data
|
||||
<Route
|
||||
path="/post/:id"
|
||||
view=|cx| view! { cx, <Post/> }
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
For a path that includes multiple nested routes, the most restrictive mode will be used: i.e., if even a single nested route asks for `async` rendering, the whole initial request will be rendered `async`. `async` is the most restricted requirement, followed by in-order, and then out-of-order. (This probably makes sense if you think about it for a few minutes.)
|
||||
|
||||
## Blocking Resources
|
||||
|
||||
Any Leptos versions later than `0.2.5` (i.e., git main and `0.3.x` or later) introduce a new resource primitive with `create_blocking_resource`. A blocking resource still loads asynchronously like any other `async`/`.await` in Rust; it doesn’t block a server thread or anything. Instead, reading from a blocking resource under a `<Suspense/>` blocks the HTML _stream_ from returning anything, including its initial synchronous shell, until that `<Suspense/>` has resolved.
|
||||
|
||||
Now from a performance perspective, this is not ideal. None of the synchronous shell for your page will load until that resource is ready. However, rendering nothing means that you can do things like set the `<title>` or `<meta>` tags in your `<head>` in actual HTML. This sounds a lot like `async` rendering, but there’s one big difference: if you have multiple `<Suspense/>` sections, you can block on _one_ of them but still render a placeholder and then stream in the other.
|
||||
|
||||
For example, think about a blog post. For SEO and for social sharing, I definitely want my blog post’s title and metadata in the initial HTML `<head>`. But I really don’t care whether comments have loaded yet or not; I’d like to load those as lazily as possible.
|
||||
|
||||
With blocking resources, I can do something like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn BlogPost(cx: Scope) -> impl IntoView {
|
||||
let post_data = create_blocking_resource(cx, /* load blog post */);
|
||||
let comment_data = create_resource(cx, /* load blog post */);
|
||||
view! { cx,
|
||||
<Suspense fallback=|| ()>
|
||||
{move || {
|
||||
post_data.with(cx, |data| {
|
||||
view! { cx,
|
||||
<Title text=data.title/>
|
||||
<Meta name="description" content=data.excerpt/>
|
||||
<article>
|
||||
/* render the post content */
|
||||
</article>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
<Suspense fallback=|| "Loading comments...">
|
||||
/* render comment data here */
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. The second `<Suspense/>`, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience.
|
||||
21
docs/book/src/ssr/README.md
Normal file
21
docs/book/src/ssr/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Server Side Rendering
|
||||
|
||||
So far, everything we’ve written has been rendered almost entirely in the browser. When we create an app using Trunk, it’s served using a local development server. If you build it for production and deploy it, it’s served by whatever server or CDN you’re using. In either case, what’s served is an HTML page with
|
||||
|
||||
1. the URL of your Leptos app, which has been compiled to WebAssembly (WASM)
|
||||
2. the URL of the JavaScript used to initialized this WASM blob
|
||||
3. an empty `<body>` element
|
||||
|
||||
When the JS and WASM have loaded, Leptos will render your app into the `<body>`. This means that nothing appears on the screen until JS/WASM have loaded and run. This has some drawbacks:
|
||||
|
||||
1. It increases load time, as your user’s screen is blank until additional resources have been downloaded.
|
||||
2. It’s bad for SEO, as load times are longer and the HTML you serve has no meaningful content.
|
||||
3. It’s broken for users for whom JS/WASM don’t load for some reason (e.g., they’re on a train and just went into a tunnel before WASM finished loading; they’re using an older device that doesn’t support WASM; they have JavaScript or WASM turned off for some reason; etc.)
|
||||
|
||||
These downsides apply across the web ecosystem, but especially to WASM apps.
|
||||
|
||||
So what do you do if you want to return more than just an empty `<body>` tag? Use “server-side rendering.”
|
||||
|
||||
Whole books could be (and probably have been) written about this topic, but at its core, it’s really simple: rather than returning an empty `<body>` tag, return an initial HTML page that reflects the actual starting state of your app or site, so that while JS/WASM are loading, and until they load, the user can access the plain HTML version.
|
||||
|
||||
The rest of this section will cover this topic in some detail!
|
||||
@@ -106,6 +106,9 @@ in as if it were an HTML element attribute. Simple.
|
||||
> sure you include this `ComponentProps` type:
|
||||
>
|
||||
> `use progress_bar::{ProgressBar, ProgressBarProps};`
|
||||
>
|
||||
> **Note**: This is still true as of `0.2.5`, but the requirement has been removed on `main`
|
||||
> and will not apply to later versions.
|
||||
|
||||
### Reactive and Static Props
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ covered some of this in the material on [components and props](./03_components.m
|
||||
Basically if you want the parent to communicate to the child, you can pass a
|
||||
[`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html), a
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html), or even a
|
||||
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/struct.MaybeSignal.html) as a prop.
|
||||
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) as a prop.
|
||||
|
||||
But what about the other direction? How can a child send notifications about events
|
||||
or state changes back up to the parent?
|
||||
|
||||
BIN
docs/video/async.mov
Normal file
BIN
docs/video/async.mov
Normal file
Binary file not shown.
BIN
docs/video/in-order.mov
Normal file
BIN
docs/video/in-order.mov
Normal file
Binary file not shown.
BIN
docs/video/out-of-order.mov
Normal file
BIN
docs/video/out-of-order.mov
Normal file
Binary file not shown.
@@ -3,6 +3,10 @@ name = "counter"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "1"
|
||||
@@ -12,5 +16,4 @@ console_error_panic_hook = "0.1.7"
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
web-sys ="0.3"
|
||||
|
||||
web-sys = "0.3"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use counter::*;
|
||||
use counter::SimpleCounter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
@@ -6,6 +6,10 @@ edition = "2021"
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
|
||||
@@ -3,6 +3,10 @@ name = "counter_without_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
console_log = "1"
|
||||
@@ -10,4 +14,10 @@ log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.34"
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
features = ["HtmlElement", "XPathResult"]
|
||||
version = "0.3.61"
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
[env]
|
||||
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
|
||||
|
||||
[tasks.post-test]
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+stable", "build-all-features"]
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
|
||||
|
||||
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
|
||||
|
||||
Issue the `cargo make test-flow` command to run unit and wasm tests.
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counter_without_macros as counter;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let dec = clear
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let text = dec
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let inc = text
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
inc.click();
|
||||
inc.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
|
||||
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
|
||||
|
||||
clear.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
}
|
||||
86
examples/counter_without_macros/tests/web.rs
Normal file
86
examples/counter_without_macros/tests/web.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use counter_without_macros::counter;
|
||||
use leptos::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increment_counter() {
|
||||
open_counter();
|
||||
|
||||
click_increment();
|
||||
click_increment();
|
||||
|
||||
assert_eq!(see_text(), Some("Value: 2!".to_string()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrement_counter() {
|
||||
open_counter();
|
||||
|
||||
click_decrement();
|
||||
click_decrement();
|
||||
|
||||
assert_eq!(see_text(), Some("Value: -2!".to_string()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_clear_counter() {
|
||||
open_counter();
|
||||
|
||||
click_increment();
|
||||
click_increment();
|
||||
|
||||
click_clear();
|
||||
|
||||
assert_eq!(see_text(), Some("Value: 0!".to_string()));
|
||||
}
|
||||
|
||||
fn open_counter() {
|
||||
remove_existing_counter();
|
||||
mount_to_body(move |cx| counter(cx, 0, 1));
|
||||
}
|
||||
|
||||
fn remove_existing_counter() {
|
||||
if let Some(counter) =
|
||||
leptos::document().query_selector("body div").unwrap()
|
||||
{
|
||||
counter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
fn click_clear() {
|
||||
click_text("Clear");
|
||||
}
|
||||
|
||||
fn click_decrement() {
|
||||
click_text("-1");
|
||||
}
|
||||
|
||||
fn click_increment() {
|
||||
click_text("+1");
|
||||
}
|
||||
|
||||
fn click_text(text: &str) {
|
||||
find_by_text(text).click();
|
||||
}
|
||||
|
||||
fn see_text() -> Option<String> {
|
||||
find_by_text("Value: ").text_content()
|
||||
}
|
||||
|
||||
fn find_by_text(text: &str) -> HtmlElement {
|
||||
let xpath = format!("//*[text()='{}']", text);
|
||||
let document = leptos::document();
|
||||
document
|
||||
.evaluate(&xpath, &document)
|
||||
.unwrap()
|
||||
.iterate_next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use counters::{Counters, CountersProps};
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -3,6 +3,10 @@ name = "error_boundary"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "1"
|
||||
|
||||
@@ -28,8 +28,6 @@ thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
@@ -44,7 +42,7 @@ ssr = [
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -3,15 +3,18 @@ name = "fetch"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.58"
|
||||
leptos = { path = "../../leptos" }
|
||||
reqwasm = "0.5.0"
|
||||
reqwasm = "0.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_error_panic_hook = "0.1"
|
||||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
@@ -1,38 +1,50 @@
|
||||
use anyhow::Result;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Cat {
|
||||
url: String,
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum FetchError {
|
||||
#[error("Please request more than zero cats.")]
|
||||
NonZeroCats,
|
||||
#[error("Error loading data from serving.")]
|
||||
Request,
|
||||
#[error("Error deserializaing cat data from request.")]
|
||||
Json
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
|
||||
if count > 0 {
|
||||
// make the request
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={count}",
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.await
|
||||
.map_err(|_| FetchError::Request)?
|
||||
// convert it to JSON
|
||||
.json::<Vec<Cat>>()
|
||||
.await?
|
||||
.await
|
||||
.map_err(|_| FetchError::Json)?
|
||||
// extract the URL field for each cat
|
||||
.into_iter()
|
||||
.map(|cat| cat.url)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(res)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
Err(FetchError::NonZeroCats)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 0);
|
||||
|
||||
// we use local_resource here because
|
||||
// 1) anyhow::Result isn't serializable/deserializable
|
||||
// 1) our error type isn't serializable/deserializable
|
||||
// 2) we're not doing server-side rendering in this example anyway
|
||||
// (during SSR, create_resource will begin loading on the server and resolve on the client)
|
||||
let cats = create_local_resource(cx, cat_count, fetch_cats);
|
||||
@@ -42,7 +54,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
errors.with(|errors| {
|
||||
errors
|
||||
.iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
@@ -60,11 +72,12 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
// and by using the ErrorBoundary fallback to catch Err(_)
|
||||
// so we'll just implement our happy path and let the framework handle the rest
|
||||
let cats_view = move || {
|
||||
cats.with(cx, |data| {
|
||||
data.iter()
|
||||
.flatten()
|
||||
.map(|cat| view! { cx, <img src={cat}/> })
|
||||
.collect::<Vec<_>>()
|
||||
cats.read(cx).map(|data| {
|
||||
data.map(|data| {
|
||||
data.iter()
|
||||
.map(|s| view! { cx, <span>{s}</span> })
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
@@ -72,8 +85,9 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
<div>
|
||||
<label>
|
||||
"How many cats would you like?"
|
||||
<input type="number"
|
||||
prop:value={move || cat_count.get().to_string()}
|
||||
<input
|
||||
type="number"
|
||||
prop:value=move || cat_count.get().to_string()
|
||||
on:input=move |ev| {
|
||||
let val = event_target_value(&ev).parse::<u32>().unwrap_or(0);
|
||||
set_cat_count(val);
|
||||
@@ -81,7 +95,9 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
/>
|
||||
</label>
|
||||
<ErrorBoundary fallback>
|
||||
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
|
||||
<Transition fallback=move || {
|
||||
view! { cx, <div>"Loading (Suspense Fallback)..."</div> }
|
||||
}>
|
||||
{cats_view}
|
||||
</Transition>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -6,6 +6,10 @@ edition = "2021"
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
|
||||
@@ -6,6 +6,10 @@ edition = "2021"
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
[workspace]
|
||||
members = ["client", "api-boundary", "server"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[patch.crates-io]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router" }
|
||||
|
||||
@@ -3,6 +3,10 @@ name = "parent-child"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "1"
|
||||
|
||||
@@ -3,6 +3,10 @@ name = "router"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
|
||||
@@ -3,17 +3,7 @@
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
<style>
|
||||
a[aria-current] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.contact, .contact-list {
|
||||
border: 1px solid #c0c0c0;
|
||||
border-radius: 3px;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
<link data-trunk rel="css" href="style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -27,7 +27,12 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
<A href="redirect-home">"Redirect to Home"</A>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
<AnimatedRoutes
|
||||
outro="slideOut"
|
||||
intro="slideIn"
|
||||
outro_back="slideOutBack"
|
||||
intro_back="slideInBack"
|
||||
>
|
||||
<ContactRoutes/>
|
||||
<Route
|
||||
path="about"
|
||||
@@ -41,7 +46,7 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
path="redirect-home"
|
||||
view=move |cx| view! { cx, <Redirect path="/"/> }
|
||||
/>
|
||||
</Routes>
|
||||
</AnimatedRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
@@ -102,7 +107,11 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
|
||||
{move || view! { cx, <ul>{contacts}</ul>}}
|
||||
</Suspense>
|
||||
<Outlet/>
|
||||
<AnimatedOutlet
|
||||
class="outlet"
|
||||
outro="fadeOut"
|
||||
intro="fadeIn"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
95
examples/router/style.css
Normal file
95
examples/router/style.css
Normal file
@@ -0,0 +1,95 @@
|
||||
a[aria-current] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.outlet {
|
||||
border: 1px dotted grey;
|
||||
}
|
||||
|
||||
.contact, .contact-list {
|
||||
border: 1px solid #c0c0c0;
|
||||
border-radius: 3px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
animation: 0.5s fadeIn forwards;
|
||||
}
|
||||
|
||||
.fadeOut {
|
||||
animation: 0.5s fadeOut forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.slideIn {
|
||||
animation: 0.125s slideIn forwards;
|
||||
}
|
||||
|
||||
.slideOut {
|
||||
animation: 0.125s slideOut forwards;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translate(100vw, 0);
|
||||
}
|
||||
to {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
to {
|
||||
transform: translate(-100vw, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.slideInBack {
|
||||
animation: 0.125s slideInBack forwards;
|
||||
}
|
||||
|
||||
.slideOutBack {
|
||||
animation: 0.125s slideOutBack forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInBack {
|
||||
from {
|
||||
transform: translate(-100vw, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutBack {
|
||||
from {
|
||||
transform: translate(0px, 0px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(100vw, 0);
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,6 @@ bcrypt = { version = "0.14", optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
@@ -65,7 +63,7 @@ ssr = [
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
54
examples/session_auth_axum/flake.lock
generated
54
examples/session_auth_axum/flake.lock
generated
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1676283394,
|
||||
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -16,12 +19,15 @@
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -61,11 +67,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1677292251,
|
||||
"narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=",
|
||||
"lastModified": 1681525152,
|
||||
"narHash": "sha256-KzI+ILcmU03iFWtB+ysPqtNmp8TP8v1BBReTuPP8MJY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc",
|
||||
"rev": "b6f8d87208336d7cb85003b2e439fc707c38f92a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -73,6 +79,36 @@
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -36,6 +36,10 @@ ssr = [
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "ssr_modes"
|
||||
|
||||
@@ -39,6 +39,10 @@ ssr = [
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "ssr_modes"
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
# Leptos Todo App Sqlite
|
||||
# Leptos Todo App Sqlite
|
||||
|
||||
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
|
||||
## Client Side Rendering
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
|
||||
|
||||
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
|
||||
1. Install cargo-leptos
|
||||
|
||||
```bash
|
||||
cargo install --locked cargo-leptos
|
||||
```
|
||||
```
|
||||
|
||||
2. Build the site in watch mode, recompiling on file changes
|
||||
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
@@ -21,24 +25,30 @@ cargo leptos watch
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
```
|
||||
|
||||
## Server Side Rendering without cargo-leptos
|
||||
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
1. Install wasm-pack
|
||||
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
2. Build the Webassembly used to hydrate the HTML from the server
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ cfg_if! {
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
_ = FormDataHandler::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -106,6 +107,24 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct FormData {
|
||||
hi: String
|
||||
}
|
||||
|
||||
#[server(FormDataHandler, "/api")]
|
||||
pub async fn form_data(cx: Scope) -> Result<FormData, ServerFnError> {
|
||||
use axum::extract::FromRequest;
|
||||
|
||||
let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx).and_then(|req| req.take_request()).unwrap();
|
||||
if req.method() == http::Method::POST {
|
||||
let form = axum::Form::from_request(req, &()).await.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
Ok(form.0)
|
||||
} else {
|
||||
Err(ServerFnError::ServerError("wrong form fields submitted".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
//let id = use_context::<String>(cx);
|
||||
@@ -126,6 +145,23 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
}/> //Route
|
||||
<Route path="weird" methods=&[Method::Get, Method::Post]
|
||||
ssr=SsrMode::Async
|
||||
view=|cx| {
|
||||
let res = create_resource(cx, || (), move |_| async move {
|
||||
form_data(cx).await
|
||||
});
|
||||
view! { cx,
|
||||
<Suspense fallback=|| ()>
|
||||
<pre>
|
||||
{move || {
|
||||
res.with(cx, |body| format!("{body:#?}"))
|
||||
}}
|
||||
</pre>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -147,6 +183,10 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<form method="POST" action="/weird">
|
||||
<input type="text" name="hi" value="John"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
|
||||
@@ -3,6 +3,10 @@ name = "todomvc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", default-features = false }
|
||||
log = "0.4"
|
||||
@@ -21,3 +25,6 @@ default = ["csr"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
@@ -201,15 +201,11 @@ pub fn handle_server_fns_with_context(
|
||||
Encoding::Url | Encoding::Cbor => body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
match (server_fn.trait_obj)(cx, data).await {
|
||||
let res = match (server_fn.trait_obj)(cx, data).await {
|
||||
Ok(serialized) => {
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
let mut res: HttpResponseBuilder;
|
||||
let mut res_parts = res_options.0.write();
|
||||
|
||||
@@ -268,7 +264,11 @@ pub fn handle_server_fns_with_context(
|
||||
serde_json::to_string(&e)
|
||||
.unwrap_or_else(|_| e.to_string()),
|
||||
),
|
||||
}
|
||||
};
|
||||
// clean up the scope
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
res
|
||||
} else {
|
||||
HttpResponse::BadRequest().body(format!(
|
||||
"Could not find a server function at the route {:?}. \
|
||||
@@ -298,6 +298,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos_router::Method;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
@@ -321,6 +322,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// leptos_actix::render_app_to_stream(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// Method::Get,
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
@@ -340,11 +342,12 @@ pub fn handle_server_fns_with_context(
|
||||
pub fn render_app_to_stream<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_with_context(options, |_cx| {}, app_fn)
|
||||
render_app_to_stream_with_context(options, |_cx| {}, app_fn, method)
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -363,6 +366,7 @@ where
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos_router::Method;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
@@ -386,6 +390,7 @@ where
|
||||
/// leptos_actix::render_app_to_stream_in_order(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// Method::Get,
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
@@ -405,11 +410,17 @@ where
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_in_order_with_context(options, |_cx| {}, app_fn)
|
||||
render_app_to_stream_in_order_with_context(
|
||||
options,
|
||||
|_cx| {},
|
||||
app_fn,
|
||||
method,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -426,6 +437,7 @@ where
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos_router::Method;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
@@ -449,6 +461,7 @@ where
|
||||
/// leptos_actix::render_app_async(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// Method::Get,
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
@@ -468,11 +481,12 @@ where
|
||||
pub fn render_app_async<IV>(
|
||||
options: LeptosOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_async_with_context(options, |_cx| {}, app_fn)
|
||||
render_app_async_with_context(options, |_cx| {}, app_fn, method)
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -491,11 +505,12 @@ pub fn render_app_to_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
web::get().to(move |req: HttpRequest| {
|
||||
let handler = move |req: HttpRequest| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
@@ -513,7 +528,14 @@ where
|
||||
|
||||
stream_app(&options, app, res_options, additional_context).await
|
||||
}
|
||||
})
|
||||
};
|
||||
match method {
|
||||
Method::Get => web::get().to(handler),
|
||||
Method::Post => web::post().to(handler),
|
||||
Method::Put => web::put().to(handler),
|
||||
Method::Delete => web::delete().to(handler),
|
||||
Method::Patch => web::patch().to(handler),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -532,11 +554,12 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
web::get().to(move |req: HttpRequest| {
|
||||
let handler = move |req: HttpRequest| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
@@ -555,7 +578,14 @@ where
|
||||
stream_app_in_order(&options, app, res_options, additional_context)
|
||||
.await
|
||||
}
|
||||
})
|
||||
};
|
||||
match method {
|
||||
Method::Get => web::get().to(handler),
|
||||
Method::Post => web::post().to(handler),
|
||||
Method::Put => web::put().to(handler),
|
||||
Method::Delete => web::delete().to(handler),
|
||||
Method::Patch => web::patch().to(handler),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -575,11 +605,12 @@ pub fn render_app_async_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
web::get().to(move |req: HttpRequest| {
|
||||
let handler = move |req: HttpRequest| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let additional_context = additional_context.clone();
|
||||
@@ -603,7 +634,14 @@ where
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
};
|
||||
match method {
|
||||
Method::Get => web::get().to(handler),
|
||||
Method::Post => web::post().to(handler),
|
||||
Method::Put => web::put().to(handler),
|
||||
Method::Delete => web::delete().to(handler),
|
||||
Method::Patch => web::patch().to(handler),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -854,7 +892,7 @@ async fn render_app_async_helper(
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
|
||||
pub fn generate_route_list<IV>(
|
||||
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
|
||||
) -> Vec<(String, SsrMode)>
|
||||
) -> Vec<RouteListing>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -863,11 +901,16 @@ where
|
||||
// Empty strings screw with Actix pathing, they need to be "/"
|
||||
routes = routes
|
||||
.into_iter()
|
||||
.map(|(s, mode)| {
|
||||
if s.is_empty() {
|
||||
return ("/".to_string(), mode);
|
||||
.map(|listing| {
|
||||
let path = listing.path();
|
||||
if path.is_empty() {
|
||||
return RouteListing::new(
|
||||
"/".to_string(),
|
||||
listing.mode(),
|
||||
listing.methods(),
|
||||
);
|
||||
}
|
||||
(s, mode)
|
||||
RouteListing::new(listing.path(), listing.mode(), listing.methods())
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -877,14 +920,19 @@ where
|
||||
// Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}`
|
||||
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
|
||||
|
||||
let routes: Vec<(String, SsrMode)> = routes
|
||||
let routes = routes
|
||||
.into_iter()
|
||||
.map(|(s, m)| (wildcard_re.replace_all(&s, "{tail:.*}").to_string(), m))
|
||||
.map(|(s, m)| (capture_re.replace_all(&s, "{$1}").to_string(), m))
|
||||
.collect();
|
||||
.map(|listing| {
|
||||
let path = wildcard_re
|
||||
.replace_all(listing.path(), "{tail:.*}")
|
||||
.to_string();
|
||||
let path = capture_re.replace_all(&path, "{$1}").to_string();
|
||||
RouteListing::new(path, listing.mode(), listing.methods())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if routes.is_empty() {
|
||||
vec![("/".to_string(), Default::default())]
|
||||
vec![RouteListing::new("/", Default::default(), [Method::Get])]
|
||||
} else {
|
||||
routes
|
||||
}
|
||||
@@ -901,7 +949,7 @@ pub trait LeptosRoutes {
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
@@ -926,7 +974,7 @@ pub trait LeptosRoutes {
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -948,7 +996,7 @@ where
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
@@ -988,7 +1036,7 @@ where
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -996,29 +1044,39 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
for (path, mode) in paths.iter() {
|
||||
router = router.route(
|
||||
path,
|
||||
match mode {
|
||||
SsrMode::OutOfOrder => render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
SsrMode::InOrder => {
|
||||
render_app_to_stream_in_order_with_context(
|
||||
for listing in paths.iter() {
|
||||
let path = listing.path();
|
||||
let mode = listing.mode();
|
||||
|
||||
for method in listing.methods() {
|
||||
router = router.route(
|
||||
path,
|
||||
match mode {
|
||||
SsrMode::OutOfOrder => {
|
||||
render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
)
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
)
|
||||
}
|
||||
SsrMode::Async => render_app_async_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
)
|
||||
}
|
||||
SsrMode::Async => render_app_async_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
},
|
||||
);
|
||||
method,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
router
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ leptos_integration_utils = { workspace = true }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
tokio-util = {version = "0.7.7", features = ["rt"] }
|
||||
tokio-util = {version = "0.7.7", features = ["rt"] }
|
||||
once_cell = "1.17"
|
||||
|
||||
@@ -14,7 +14,7 @@ use axum::{
|
||||
HeaderMap, Request, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
routing::{delete, get, patch, post, put},
|
||||
};
|
||||
use futures::{
|
||||
channel::mpsc::{Receiver, Sender},
|
||||
@@ -31,13 +31,9 @@ use leptos::{
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
use leptos_meta::{generate_head_metadata_separated, MetaContext};
|
||||
use leptos_router::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
sync::{Arc, OnceLock},
|
||||
thread::available_parallelism,
|
||||
};
|
||||
use std::{io, pin::Pin, sync::Arc, thread::available_parallelism};
|
||||
use tokio::task::LocalSet;
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
|
||||
@@ -323,15 +319,11 @@ async fn handle_server_fns_inner(
|
||||
Encoding::Url | Encoding::Cbor => &req_parts.body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
match (server_fn.trait_obj)(cx, data).await {
|
||||
let res = match (server_fn.trait_obj)(cx, data).await {
|
||||
Ok(serialized) => {
|
||||
// If ResponseOptions are set, add the headers and status to the request
|
||||
let res_options = use_context::<ResponseOptions>(cx);
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header = headers
|
||||
.get("Accept")
|
||||
@@ -396,7 +388,11 @@ async fn handle_server_fns_inner(
|
||||
serde_json::to_string(&e)
|
||||
.unwrap_or_else(|_| e.to_string()),
|
||||
)),
|
||||
}
|
||||
};
|
||||
// clean up the scope
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
res
|
||||
} else {
|
||||
Response::builder().status(StatusCode::BAD_REQUEST).body(
|
||||
Full::from(format!(
|
||||
@@ -995,12 +991,12 @@ where
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
|
||||
pub async fn generate_route_list<IV>(
|
||||
app_fn: impl FnOnce(Scope) -> IV + 'static,
|
||||
) -> Vec<(String, SsrMode)>
|
||||
) -> Vec<RouteListing>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Routes(pub Arc<RwLock<Vec<(String, SsrMode)>>>);
|
||||
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
|
||||
|
||||
let routes = Routes::default();
|
||||
let routes_inner = routes.clone();
|
||||
@@ -1024,17 +1020,26 @@ where
|
||||
// Axum's Router defines Root routes as "/" not ""
|
||||
let routes = routes
|
||||
.into_iter()
|
||||
.map(|(s, m)| {
|
||||
if s.is_empty() {
|
||||
("/".to_string(), m)
|
||||
.map(|listing| {
|
||||
let path = listing.path();
|
||||
if path.is_empty() {
|
||||
RouteListing::new(
|
||||
"/",
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
)
|
||||
} else {
|
||||
(s, m)
|
||||
listing
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if routes.is_empty() {
|
||||
vec![("/".to_string(), Default::default())]
|
||||
vec![RouteListing::new(
|
||||
"/",
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
)]
|
||||
} else {
|
||||
routes
|
||||
}
|
||||
@@ -1046,7 +1051,7 @@ pub trait LeptosRoutes {
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
@@ -1055,7 +1060,7 @@ pub trait LeptosRoutes {
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -1064,7 +1069,7 @@ pub trait LeptosRoutes {
|
||||
|
||||
fn leptos_routes_with_handler<H, T>(
|
||||
self,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
@@ -1077,7 +1082,7 @@ impl LeptosRoutes for axum::Router {
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
@@ -1089,7 +1094,7 @@ impl LeptosRoutes for axum::Router {
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
@@ -1097,38 +1102,65 @@ impl LeptosRoutes for axum::Router {
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
for (path, mode) in paths.iter() {
|
||||
router = router.route(
|
||||
path,
|
||||
match mode {
|
||||
SsrMode::OutOfOrder => {
|
||||
get(render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
))
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
get(render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
))
|
||||
}
|
||||
SsrMode::Async => get(render_app_async_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
)),
|
||||
},
|
||||
);
|
||||
for listing in paths.iter() {
|
||||
let path = listing.path();
|
||||
|
||||
for method in listing.methods() {
|
||||
router = router.route(
|
||||
path,
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
router
|
||||
}
|
||||
|
||||
fn leptos_routes_with_handler<H, T>(
|
||||
self,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
@@ -1136,15 +1168,28 @@ impl LeptosRoutes for axum::Router {
|
||||
T: 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
for (path, _) in paths.iter() {
|
||||
router = router.route(path, get(handler.clone()));
|
||||
for listing in paths.iter() {
|
||||
for method in listing.methods() {
|
||||
router = router.route(
|
||||
listing.path(),
|
||||
match method {
|
||||
leptos_router::Method::Get => get(handler.clone()),
|
||||
leptos_router::Method::Post => post(handler.clone()),
|
||||
leptos_router::Method::Put => put(handler.clone()),
|
||||
leptos_router::Method::Delete => {
|
||||
delete(handler.clone())
|
||||
}
|
||||
leptos_router::Method::Patch => patch(handler.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
router
|
||||
}
|
||||
}
|
||||
|
||||
fn get_leptos_pool() -> LocalPoolHandle {
|
||||
static LOCAL_POOL: OnceLock<LocalPoolHandle> = OnceLock::new();
|
||||
static LOCAL_POOL: OnceCell<LocalPoolHandle> = OnceCell::new();
|
||||
LOCAL_POOL
|
||||
.get_or_init(|| {
|
||||
tokio_util::task::LocalPoolHandle::new(
|
||||
|
||||
@@ -216,16 +216,14 @@ async fn handle_server_fns_inner(
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
|
||||
match (server_fn.trait_obj)(cx, data).await {
|
||||
let res = match (server_fn.trait_obj)(cx, data)
|
||||
.await
|
||||
{
|
||||
Ok(serialized) => {
|
||||
// If ResponseOptions are set, add the headers and status to the request
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx);
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header = headers
|
||||
.get("Accept")
|
||||
@@ -305,7 +303,11 @@ async fn handle_server_fns_inner(
|
||||
serde_json::to_string(&e)
|
||||
.unwrap_or_else(|_| e.to_string()),
|
||||
)),
|
||||
}
|
||||
};
|
||||
// clean up the scope
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
res
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
@@ -944,12 +946,12 @@ where
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
|
||||
pub async fn generate_route_list<IV>(
|
||||
app_fn: impl FnOnce(Scope) -> IV + 'static,
|
||||
) -> Vec<(String, SsrMode)>
|
||||
) -> Vec<RouteListing>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Routes(pub Arc<RwLock<Vec<(String, SsrMode)>>>);
|
||||
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
|
||||
|
||||
let routes = Routes::default();
|
||||
let routes_inner = routes.clone();
|
||||
@@ -973,17 +975,26 @@ where
|
||||
// Viz's Router defines Root routes as "/" not ""
|
||||
let routes = routes
|
||||
.into_iter()
|
||||
.map(|(s, m)| {
|
||||
if s.is_empty() {
|
||||
("/".to_string(), m)
|
||||
.map(|listing| {
|
||||
let path = listing.path();
|
||||
if path.is_empty() {
|
||||
RouteListing::new(
|
||||
"/",
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
)
|
||||
} else {
|
||||
(s, m)
|
||||
listing
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if routes.is_empty() {
|
||||
vec![("/".to_string(), Default::default())]
|
||||
vec![RouteListing::new(
|
||||
"/",
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
)]
|
||||
} else {
|
||||
routes
|
||||
}
|
||||
@@ -995,7 +1006,7 @@ pub trait LeptosRoutes {
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
@@ -1004,7 +1015,7 @@ pub trait LeptosRoutes {
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + Clone + Send + Sync + 'static,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
@@ -1013,7 +1024,7 @@ pub trait LeptosRoutes {
|
||||
|
||||
fn leptos_routes_with_handler<H, O>(
|
||||
self,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
@@ -1026,7 +1037,7 @@ impl LeptosRoutes for Router {
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
@@ -1038,52 +1049,93 @@ impl LeptosRoutes for Router {
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
additional_context: impl Fn(leptos::Scope) + Clone + Send + Sync + 'static,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
paths.iter().fold(self, |router, (path, mode)| match mode {
|
||||
SsrMode::OutOfOrder => router.get(
|
||||
path,
|
||||
render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
),
|
||||
SsrMode::InOrder => router.get(
|
||||
path,
|
||||
render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
),
|
||||
SsrMode::Async => router.get(
|
||||
path,
|
||||
render_app_async_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
),
|
||||
paths.iter().fold(self, |router, listing| {
|
||||
let path = listing.path();
|
||||
let mode = listing.mode();
|
||||
|
||||
listing.methods().fold(router, |router, method| match mode {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => router.get(path, s),
|
||||
leptos_router::Method::Post => router.post(path, s),
|
||||
leptos_router::Method::Put => router.put(path, s),
|
||||
leptos_router::Method::Delete => router.delete(path, s),
|
||||
leptos_router::Method::Patch => router.patch(path, s),
|
||||
}
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => router.get(path, s),
|
||||
leptos_router::Method::Post => router.post(path, s),
|
||||
leptos_router::Method::Put => router.put(path, s),
|
||||
leptos_router::Method::Delete => router.delete(path, s),
|
||||
leptos_router::Method::Patch => router.patch(path, s),
|
||||
}
|
||||
}
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => router.get(path, s),
|
||||
leptos_router::Method::Post => router.post(path, s),
|
||||
leptos_router::Method::Put => router.put(path, s),
|
||||
leptos_router::Method::Delete => router.delete(path, s),
|
||||
leptos_router::Method::Patch => router.patch(path, s),
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn leptos_routes_with_handler<H, O>(
|
||||
self,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
H: Handler<Request, Output = Result<O>> + Clone,
|
||||
O: IntoResponse + Send + Sync + 'static,
|
||||
{
|
||||
paths
|
||||
.iter()
|
||||
.fold(self, |router, (path, _)| router.get(path, handler.clone()))
|
||||
paths.iter().fold(self, |router, listing| {
|
||||
listing
|
||||
.methods()
|
||||
.fold(router, |router, method| match method {
|
||||
leptos_router::Method::Get => {
|
||||
router.get(listing.path(), handler.clone())
|
||||
}
|
||||
leptos_router::Method::Post => {
|
||||
router.post(listing.path(), handler.clone())
|
||||
}
|
||||
leptos_router::Method::Put => {
|
||||
router.put(listing.path(), handler.clone())
|
||||
}
|
||||
leptos_router::Method::Delete => {
|
||||
router.delete(listing.path(), handler.clone())
|
||||
}
|
||||
leptos_router::Method::Patch => {
|
||||
router.patch(listing.path(), handler.clone())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ cfg-if = "1"
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
leptos_server = { workspace = true, default-features = false }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.14"
|
||||
server_fn = { workspace = true }
|
||||
server_fn = { workspace = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = ".", default-features = false }
|
||||
@@ -36,6 +36,8 @@ hydrate = [
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
]
|
||||
default-tls = ["leptos_server/default-tls", "server_fn/default-tls"]
|
||||
rustls = ["leptos_server/rustls", "server_fn/rustls"]
|
||||
ssr = [
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
@@ -93,4 +95,8 @@ skip_feature_sets = [
|
||||
"serde-lite",
|
||||
"rkyv",
|
||||
],
|
||||
[
|
||||
"default-tls",
|
||||
"rustls",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -3,6 +3,7 @@ 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)]
|
||||
#[repr(transparent)]
|
||||
pub struct AdditionalAttributes(pub(crate) Vec<(String, TextProp)>);
|
||||
|
||||
impl<I, T, U> From<I> for AdditionalAttributes
|
||||
@@ -22,6 +23,7 @@ where
|
||||
}
|
||||
|
||||
/// Iterator over additional HTML attributes.
|
||||
#[repr(transparent)]
|
||||
pub struct AdditionalAttributesIter<'a>(
|
||||
std::slice::Iter<'a, (String, TextProp)>,
|
||||
);
|
||||
@@ -29,6 +31,7 @@ pub struct AdditionalAttributesIter<'a>(
|
||||
impl<'a> Iterator for AdditionalAttributesIter<'a> {
|
||||
type Item = &'a (String, TextProp);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
@@ -39,6 +42,6 @@ impl<'a> IntoIterator for &'a AdditionalAttributes {
|
||||
type IntoIter = AdditionalAttributesIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
todo!()
|
||||
AdditionalAttributesIter(self.0.iter())
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,8 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
mod additional_attributes;
|
||||
pub use additional_attributes::*;
|
||||
pub use leptos_config::{self, get_configuration, LeptosOptions};
|
||||
#[cfg(not(all(
|
||||
target_arch = "wasm32",
|
||||
@@ -180,7 +182,9 @@ pub use for_loop::*;
|
||||
pub use show::*;
|
||||
mod suspense;
|
||||
pub use suspense::*;
|
||||
mod text_prop;
|
||||
mod transition;
|
||||
pub use text_prop::TextProp;
|
||||
#[cfg(debug_assertions)]
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
@@ -218,3 +222,42 @@ pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
|
||||
/// }
|
||||
/// ```
|
||||
pub type AttributeValue = Box<dyn IntoAttribute>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Component<P> {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Props {
|
||||
type Builder;
|
||||
fn builder() -> Self::Builder;
|
||||
}
|
||||
|
||||
impl<P, F, R> Component<P> for F where F: FnOnce(::leptos::Scope, P) -> R {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_props_builder<P: Props>(
|
||||
_f: &impl Component<P>,
|
||||
) -> <P as Props>::Builder {
|
||||
<P as Props>::builder()
|
||||
}
|
||||
|
||||
#[cfg(all(not(doc), feature = "csr", feature = "ssr"))]
|
||||
compile_error!(
|
||||
"You have both `csr` and `ssr` enabled as features, which may cause \
|
||||
issues like <Suspense/>` failing to work silently. `csr` is enabled by \
|
||||
default on `leptos`, and can be disabled by adding `default-features = \
|
||||
false` to your `leptos` dependency."
|
||||
);
|
||||
|
||||
#[cfg(all(not(doc), feature = "hydrate", feature = "ssr"))]
|
||||
compile_error!(
|
||||
"You have both `hydrate` and `ssr` enabled as features, which may cause \
|
||||
issues like <Suspense/>` failing to work silently."
|
||||
);
|
||||
|
||||
#[cfg(all(not(doc), feature = "hydrate", feature = "csr"))]
|
||||
compile_error!(
|
||||
"You have both `hydrate` and `csr` enabled as features, which may cause \
|
||||
issues. `csr` is enabled by default on `leptos`, and can be disabled by \
|
||||
adding `default-features = false` to your `leptos` dependency."
|
||||
);
|
||||
|
||||
43
leptos/src/text_prop.rs
Normal file
43
leptos/src/text_prop.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use std::{fmt::Debug, rc::Rc};
|
||||
|
||||
/// Describes a value that is either a static or a reactive string, i.e.,
|
||||
/// a [String], a [&str], or a reactive `Fn() -> String`.
|
||||
#[derive(Clone)]
|
||||
pub struct TextProp(Rc<dyn Fn() -> String>);
|
||||
|
||||
impl TextProp {
|
||||
/// Accesses the current value of the property.
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> String {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TextProp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("TextProp").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for TextProp {
|
||||
fn from(s: String) -> Self {
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TextProp {
|
||||
fn from(s: &str) -> Self {
|
||||
let s = s.to_string();
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> From<F> for TextProp
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn from(s: F) -> Self {
|
||||
TextProp(Rc::new(s))
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,7 @@ impl Mountable for ComponentRepr {
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
@@ -156,17 +157,21 @@ impl IntoView for ComponentRepr {
|
||||
|
||||
impl ComponentRepr {
|
||||
/// Creates a new [`Component`].
|
||||
#[inline(always)]
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self::new_with_id(name, HydrationCtx::id())
|
||||
Self::new_with_id_concrete(name.into(), HydrationCtx::id())
|
||||
}
|
||||
|
||||
/// Creates a new [`Component`] with the given hydration ID.
|
||||
#[inline(always)]
|
||||
pub fn new_with_id(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
id: HydrationKey,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
Self::new_with_id_concrete(name.into(), id)
|
||||
}
|
||||
|
||||
fn new_with_id_concrete(name: Cow<'static, str>, id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
@@ -139,13 +139,15 @@ where
|
||||
/// Creates a new dynamic child which will re-render whenever it's
|
||||
/// signal dependencies change.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn new(child_fn: CF) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), child_fn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
|
||||
#[inline(always)]
|
||||
pub const fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
|
||||
Self { id, child_fn }
|
||||
}
|
||||
}
|
||||
@@ -159,8 +161,10 @@ where
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
#[inline]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
// concrete inner function
|
||||
#[inline(never)]
|
||||
fn create_dyn_view(
|
||||
cx: Scope,
|
||||
component: DynChildRepr,
|
||||
@@ -379,6 +383,7 @@ cfg_if! {
|
||||
}
|
||||
|
||||
impl NonViewMarkerSibling for web_sys::Node {
|
||||
#[cfg_attr(not(debug_assertions), inline(always))]
|
||||
fn next_non_view_marker_sibling(&self) -> Option<Node> {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
@@ -395,6 +400,7 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(debug_assertions), inline(always))]
|
||||
fn previous_non_view_marker_sibling(&self) -> Option<Node> {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
|
||||
@@ -155,6 +155,7 @@ impl Mountable for EachRepr {
|
||||
};
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
@@ -257,6 +258,7 @@ impl Mountable for EachItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self.opening.node.clone();
|
||||
@@ -328,7 +330,8 @@ where
|
||||
T: 'static,
|
||||
{
|
||||
/// Creates a new [`Each`] component.
|
||||
pub fn new(items_fn: IF, key_fn: KF, each_fn: EF) -> Self {
|
||||
#[inline(always)]
|
||||
pub const fn new(items_fn: IF, key_fn: KF, each_fn: EF) -> Self {
|
||||
Self {
|
||||
items_fn,
|
||||
each_fn,
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
use crate::{HydrationCtx, IntoView};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::{signal_prelude::*, use_context, RwSignal};
|
||||
use std::{collections::HashMap, error::Error, sync::Arc};
|
||||
use std::{borrow::Cow, collections::HashMap, error::Error, sync::Arc};
|
||||
|
||||
/// A struct to hold all the possible errors that could be provided by child Views
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[repr(transparent)]
|
||||
pub struct Errors(HashMap<ErrorKey, Arc<dyn Error + Send + Sync>>);
|
||||
|
||||
/// A unique key for an error that occurs at a particular location in the user interface.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ErrorKey(String);
|
||||
#[repr(transparent)]
|
||||
pub struct ErrorKey(Cow<'static, str>);
|
||||
|
||||
impl<T> From<T> for ErrorKey
|
||||
where
|
||||
T: Into<String>,
|
||||
T: Into<Cow<'static, str>>,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn from(key: T) -> ErrorKey {
|
||||
ErrorKey(key.into())
|
||||
}
|
||||
@@ -24,12 +27,14 @@ impl IntoIterator for Errors {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
type IntoIter = IntoIter;
|
||||
|
||||
#[inline(always)]
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
IntoIter(self.0.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// An owning iterator over all the errors contained in the [Errors] struct.
|
||||
#[repr(transparent)]
|
||||
pub struct IntoIter(
|
||||
std::collections::hash_map::IntoIter<
|
||||
ErrorKey,
|
||||
@@ -40,6 +45,7 @@ pub struct IntoIter(
|
||||
impl Iterator for IntoIter {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(
|
||||
&mut self,
|
||||
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
|
||||
@@ -48,6 +54,7 @@ impl Iterator for IntoIter {
|
||||
}
|
||||
|
||||
/// An iterator over all the errors contained in the [Errors] struct.
|
||||
#[repr(transparent)]
|
||||
pub struct Iter<'a>(
|
||||
std::collections::hash_map::Iter<
|
||||
'a,
|
||||
@@ -59,6 +66,7 @@ pub struct Iter<'a>(
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = (&'a ErrorKey, &'a Arc<dyn Error + Send + Sync>);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(
|
||||
&mut self,
|
||||
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
|
||||
@@ -72,7 +80,7 @@ where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
let id = ErrorKey(HydrationCtx::peek().previous);
|
||||
let id = ErrorKey(HydrationCtx::peek().previous.into());
|
||||
let errors = use_context::<RwSignal<Errors>>(cx);
|
||||
match self {
|
||||
Ok(stuff) => {
|
||||
@@ -127,6 +135,7 @@ where
|
||||
}
|
||||
impl Errors {
|
||||
/// Returns `true` if there are no errors.
|
||||
#[inline(always)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
@@ -156,6 +165,7 @@ impl Errors {
|
||||
}
|
||||
|
||||
/// An iterator over all the errors, in arbitrary order.
|
||||
#[inline(always)]
|
||||
pub fn iter(&self) -> Iter<'_> {
|
||||
Iter(self.0.iter())
|
||||
}
|
||||
|
||||
@@ -43,17 +43,20 @@ impl From<View> for Fragment {
|
||||
|
||||
impl Fragment {
|
||||
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
|
||||
#[inline(always)]
|
||||
pub fn new(nodes: Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes)
|
||||
}
|
||||
|
||||
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
|
||||
#[inline(always)]
|
||||
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes())
|
||||
}
|
||||
|
||||
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
|
||||
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
|
||||
#[inline(always)]
|
||||
pub const fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
nodes,
|
||||
@@ -63,11 +66,13 @@ impl Fragment {
|
||||
}
|
||||
|
||||
/// Gives access to the [View] children contained within the fragment.
|
||||
#[inline(always)]
|
||||
pub fn as_children(&self) -> &[View] {
|
||||
&self.nodes
|
||||
}
|
||||
|
||||
/// Returns the fragment's hydration ID.
|
||||
#[inline(always)]
|
||||
pub fn id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
|
||||
@@ -40,14 +40,17 @@ impl Default for UnitRepr {
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for UnitRepr {
|
||||
#[inline(always)]
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ thread_local! {
|
||||
// Used in template macro
|
||||
#[doc(hidden)]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(always)]
|
||||
pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
|
||||
target: &web_sys::Element,
|
||||
event: E,
|
||||
@@ -21,8 +22,9 @@ pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
|
||||
mut event_handler: impl FnMut(E::EventType) + 'static,
|
||||
) {
|
||||
let event_name = event.name();
|
||||
let event_handler = Box::new(event_handler);
|
||||
|
||||
if event.bubbles() {
|
||||
if E::BUBBLES {
|
||||
add_event_listener(
|
||||
target,
|
||||
event.event_delegation_key(),
|
||||
@@ -47,8 +49,8 @@ pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
key: Cow<'static, str>,
|
||||
event_name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
|
||||
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
@@ -56,16 +58,16 @@ pub fn add_event_listener<E>(
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let cb = Box::new(move |e| {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let cb = Closure::wrap(cb as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = intern(&key);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
add_delegated_event_listener(&key, event_name, options);
|
||||
@@ -76,26 +78,26 @@ pub fn add_event_listener<E>(
|
||||
pub(crate) fn add_event_listener_undelegated<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &str,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
|
||||
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let cb = Box::new(move |e| {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let event_name = intern(event_name);
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let cb = Closure::wrap(cb as Box<dyn FnMut(E)>).into_js_value();
|
||||
if let Some(options) = options {
|
||||
_ = target
|
||||
.add_event_listener_with_callback_and_add_event_listener_options(
|
||||
|
||||
@@ -8,23 +8,22 @@ pub trait EventDescriptor: Clone {
|
||||
/// The [`web_sys`] event type, such as [`web_sys::MouseEvent`].
|
||||
type EventType: FromWasmAbi;
|
||||
|
||||
/// Indicates if this event bubbles. For example, `click` bubbles,
|
||||
/// but `focus` does not.
|
||||
///
|
||||
/// If this is true, then the event will be delegated globally,
|
||||
/// otherwise, event listeners will be directly attached to the element.
|
||||
const BUBBLES: bool;
|
||||
|
||||
/// The name of the event, such as `click` or `mouseover`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
|
||||
/// The key used for event delegation.
|
||||
fn event_delegation_key(&self) -> Cow<'static, str>;
|
||||
|
||||
/// Indicates if this event bubbles. For example, `click` bubbles,
|
||||
/// but `focus` does not.
|
||||
///
|
||||
/// If this method returns true, then the event will be delegated globally,
|
||||
/// otherwise, event listeners will be directly attached to the element.
|
||||
fn bubbles(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Return the options for this type. This is only used when you create a [`Custom`] event
|
||||
/// handler.
|
||||
#[inline(always)]
|
||||
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
|
||||
&None
|
||||
}
|
||||
@@ -39,17 +38,17 @@ pub struct undelegated<Ev: EventDescriptor>(pub Ev);
|
||||
impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
|
||||
type EventType = Ev::EventType;
|
||||
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
self.0.name()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn event_delegation_key(&self) -> Cow<'static, str> {
|
||||
self.0.event_delegation_key()
|
||||
}
|
||||
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
const BUBBLES: bool = false;
|
||||
}
|
||||
|
||||
/// A custom event.
|
||||
@@ -80,10 +79,9 @@ impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
|
||||
format!("$$${}", self.name).into()
|
||||
}
|
||||
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
const BUBBLES: bool = false;
|
||||
|
||||
#[inline(always)]
|
||||
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
|
||||
&self.options
|
||||
}
|
||||
@@ -142,24 +140,22 @@ macro_rules! generate_event_types {
|
||||
impl EventDescriptor for $event {
|
||||
type EventType = web_sys::$web_sys_event;
|
||||
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
stringify!($event).into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn event_delegation_key(&self) -> Cow<'static, str> {
|
||||
concat!("$$$", stringify!($event)).into()
|
||||
}
|
||||
|
||||
$(
|
||||
generate_event_types!($does_not_bubble);
|
||||
)?
|
||||
const BUBBLES: bool = true $(&& generate_event_types!($does_not_bubble))?;
|
||||
}
|
||||
)*
|
||||
};
|
||||
|
||||
(does_not_bubble) => {
|
||||
fn bubbles(&self) -> bool { false }
|
||||
}
|
||||
(does_not_bubble) => { false }
|
||||
}
|
||||
|
||||
generate_event_types! {
|
||||
|
||||
@@ -97,6 +97,7 @@ impl AnimationFrameRequestHandle {
|
||||
/// Runs the given function between the next repaint using
|
||||
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
#[inline(always)]
|
||||
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
_ = request_animation_frame_with_handle(cb);
|
||||
}
|
||||
@@ -105,6 +106,7 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
|
||||
/// returning a cancelable handle.
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
#[inline(always)]
|
||||
pub fn request_animation_frame_with_handle(
|
||||
cb: impl FnOnce() + 'static,
|
||||
) -> Result<AnimationFrameRequestHandle, JsValue> {
|
||||
@@ -118,10 +120,14 @@ pub fn request_animation_frame_with_handle(
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(cb);
|
||||
window()
|
||||
.request_animation_frame(cb.as_ref().unchecked_ref())
|
||||
.map(AnimationFrameRequestHandle)
|
||||
#[inline(never)]
|
||||
fn raf(cb: JsValue) -> Result<AnimationFrameRequestHandle, JsValue> {
|
||||
window()
|
||||
.request_animation_frame(cb.as_ref().unchecked_ref())
|
||||
.map(AnimationFrameRequestHandle)
|
||||
}
|
||||
|
||||
raf(Closure::once_into_js(cb))
|
||||
}
|
||||
|
||||
/// Handle that is generated by [request_idle_callback_with_handle] and can be
|
||||
@@ -140,6 +146,7 @@ impl IdleCallbackHandle {
|
||||
/// Queues the given function during an idle period using
|
||||
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
#[inline(always)]
|
||||
pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
_ = request_idle_callback_with_handle(cb);
|
||||
}
|
||||
@@ -148,6 +155,7 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
|
||||
/// returning a cancelable handle.
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
#[inline(always)]
|
||||
pub fn request_idle_callback_with_handle(
|
||||
cb: impl Fn() + 'static,
|
||||
) -> Result<IdleCallbackHandle, JsValue> {
|
||||
@@ -161,10 +169,16 @@ pub fn request_idle_callback_with_handle(
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
window()
|
||||
.request_idle_callback(cb.as_ref().unchecked_ref())
|
||||
.map(IdleCallbackHandle)
|
||||
#[inline(never)]
|
||||
fn ric(cb: Box<dyn Fn()>) -> Result<IdleCallbackHandle, JsValue> {
|
||||
let cb = Closure::wrap(cb).into_js_value();
|
||||
|
||||
window()
|
||||
.request_idle_callback(cb.as_ref().unchecked_ref())
|
||||
.map(IdleCallbackHandle)
|
||||
}
|
||||
|
||||
ric(Box::new(cb))
|
||||
}
|
||||
|
||||
/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
|
||||
@@ -195,6 +209,7 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn set_timeout_with_handle(
|
||||
cb: impl FnOnce() + 'static,
|
||||
duration: Duration,
|
||||
@@ -211,13 +226,17 @@ pub fn set_timeout_with_handle(
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
|
||||
window()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)
|
||||
.map(TimeoutHandle)
|
||||
#[inline(never)]
|
||||
fn st(cb: JsValue, duration: Duration) -> Result<TimeoutHandle, JsValue> {
|
||||
window()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)
|
||||
.map(TimeoutHandle)
|
||||
}
|
||||
|
||||
st(Closure::once_into_js(cb), duration)
|
||||
}
|
||||
|
||||
/// "Debounce" a callback function. This will cause it to wait for a period of `delay`
|
||||
@@ -243,7 +262,8 @@ pub fn set_timeout_with_handle(
|
||||
pub fn debounce<T: 'static>(
|
||||
cx: Scope,
|
||||
delay: Duration,
|
||||
#[allow(unused_mut)] mut cb: impl FnMut(T) + 'static,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(T) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(T) + 'static,
|
||||
) -> impl FnMut(T) {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
@@ -347,6 +367,7 @@ pub fn set_interval(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn set_interval_with_handle(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
@@ -363,13 +384,22 @@ pub fn set_interval_with_handle(
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
let handle = window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)?;
|
||||
Ok(IntervalHandle(handle))
|
||||
#[inline(never)]
|
||||
fn si(
|
||||
cb: Box<dyn Fn()>,
|
||||
duration: Duration,
|
||||
) -> Result<IntervalHandle, JsValue> {
|
||||
let cb = Closure::wrap(cb).into_js_value();
|
||||
|
||||
window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)
|
||||
.map(IntervalHandle)
|
||||
}
|
||||
|
||||
si(Box::new(cb), duration)
|
||||
}
|
||||
|
||||
/// Adds an event listener to the `Window`.
|
||||
@@ -377,6 +407,7 @@ pub fn set_interval_with_handle(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(event_name = %event_name))
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn window_event_listener(
|
||||
event_name: &str,
|
||||
cb: impl Fn(web_sys::Event) + 'static,
|
||||
@@ -394,11 +425,16 @@ pub fn window_event_listener(
|
||||
}
|
||||
|
||||
if !is_server() {
|
||||
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
|
||||
#[inline(never)]
|
||||
fn wel(cb: Box<dyn FnMut(web_sys::Event)>, event_name: &str) {
|
||||
let cb = Closure::wrap(cb).into_js_value();
|
||||
_ = window().add_event_listener_with_callback(
|
||||
event_name,
|
||||
cb.unchecked_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(handler).into_js_value();
|
||||
_ = window()
|
||||
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
wel(Box::new(cb), event_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ pub trait ElementDescriptor: ElementDescriptorBounds {
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
|
||||
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
|
||||
#[inline(always)]
|
||||
fn is_void(&self) -> bool {
|
||||
false
|
||||
}
|
||||
@@ -140,6 +141,7 @@ pub struct AnyElement {
|
||||
impl std::ops::Deref for AnyElement {
|
||||
type Target = web_sys::HtmlElement;
|
||||
|
||||
#[inline(always)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
return &self.element;
|
||||
@@ -150,6 +152,7 @@ impl std::ops::Deref for AnyElement {
|
||||
}
|
||||
|
||||
impl std::convert::AsRef<web_sys::HtmlElement> for AnyElement {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &web_sys::HtmlElement {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
return &self.element;
|
||||
@@ -164,11 +167,13 @@ impl ElementDescriptor for AnyElement {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_void(&self) -> bool {
|
||||
self.is_void
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
#[inline(always)]
|
||||
fn hydration_id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
@@ -254,6 +259,7 @@ impl Custom {
|
||||
impl std::ops::Deref for Custom {
|
||||
type Target = web_sys::HtmlElement;
|
||||
|
||||
#[inline(always)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.element
|
||||
}
|
||||
@@ -261,6 +267,7 @@ impl std::ops::Deref for Custom {
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl std::convert::AsRef<web_sys::HtmlElement> for Custom {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &web_sys::HtmlElement {
|
||||
&self.element
|
||||
}
|
||||
@@ -272,6 +279,7 @@ impl ElementDescriptor for Custom {
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
#[inline(always)]
|
||||
fn hydration_id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
@@ -413,6 +421,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
/// Adds an optional marker indicating the view macro source.
|
||||
#[inline(always)]
|
||||
pub fn with_view_marker(mut self, marker: impl Into<String>) -> Self {
|
||||
self.view_marker = Some(marker.into());
|
||||
self
|
||||
@@ -471,15 +480,18 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
/// Adds an `id` to the element.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn id(self, id: impl Into<Cow<'static, str>>) -> Self {
|
||||
let id = id.into();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
self.element
|
||||
.as_ref()
|
||||
.set_attribute(wasm_bindgen::intern("id"), &id)
|
||||
.unwrap();
|
||||
#[inline(never)]
|
||||
fn id_inner(el: &web_sys::HtmlElement, id: &str) {
|
||||
el.set_attribute(wasm_bindgen::intern("id"), id).unwrap()
|
||||
}
|
||||
|
||||
id_inner(self.element.as_ref(), &id);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -495,6 +507,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
|
||||
/// Binds the element reference to [`NodeRef`].
|
||||
#[inline(always)]
|
||||
pub fn node_ref(self, node_ref: NodeRef<El>) -> Self
|
||||
where
|
||||
Self: Clone,
|
||||
@@ -577,13 +590,16 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// of `body`.
|
||||
///
|
||||
/// This method will always return [`None`] on non-wasm CSR targets.
|
||||
#[inline(always)]
|
||||
pub fn is_mounted(&self) -> bool {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
crate::document()
|
||||
.body()
|
||||
.unwrap()
|
||||
.contains(Some(self.element.as_ref()))
|
||||
#[inline(never)]
|
||||
fn is_mounted_inner(el: &web_sys::HtmlElement) -> bool {
|
||||
crate::document().body().unwrap().contains(Some(el))
|
||||
}
|
||||
|
||||
is_mounted_inner(self.element.as_ref())
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
@@ -592,6 +608,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
/// Adds an attribute to this element.
|
||||
#[track_caller]
|
||||
#[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))]
|
||||
pub fn attr(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
@@ -621,7 +638,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
match attr {
|
||||
Attribute::String(value) => {
|
||||
this.attrs.push((name, value.into()));
|
||||
this.attrs.push((name, value));
|
||||
}
|
||||
Attribute::Bool(include) => {
|
||||
if include {
|
||||
@@ -630,7 +647,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
Attribute::Option(_, maybe) => {
|
||||
if let Some(value) = maybe {
|
||||
this.attrs.push((name, value.into()));
|
||||
this.attrs.push((name, value));
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@@ -685,10 +702,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a list of classes separated by ASCII whitespace to an element.
|
||||
#[track_caller]
|
||||
pub fn classes(self, classes: impl Into<Cow<'static, str>>) -> Self {
|
||||
let classes = classes.into();
|
||||
fn classes_inner(self, classes: &str) -> Self {
|
||||
let mut this = self;
|
||||
for class in classes.split_ascii_whitespace() {
|
||||
this = this.class(class.to_string(), true);
|
||||
@@ -696,6 +710,13 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
this
|
||||
}
|
||||
|
||||
/// Adds a list of classes separated by ASCII whitespace to an element.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn classes(self, classes: impl Into<Cow<'static, str>>) -> Self {
|
||||
self.classes_inner(&classes.into())
|
||||
}
|
||||
|
||||
/// Sets the class on the element as the class signal changes.
|
||||
#[track_caller]
|
||||
pub fn dyn_classes<I, C>(
|
||||
@@ -820,6 +841,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
/// Adds an event listener to this element.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn on<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
@@ -842,8 +864,9 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
let event_name = event.name();
|
||||
|
||||
let key = event.event_delegation_key();
|
||||
let event_handler = Box::new(event_handler);
|
||||
|
||||
if event.bubbles() {
|
||||
if E::BUBBLES {
|
||||
add_event_listener(
|
||||
self.element.as_ref(),
|
||||
key,
|
||||
@@ -922,6 +945,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// Be very careful when using this method. Always remember to
|
||||
/// sanitize the input to avoid a cross-site scripting (XSS)
|
||||
/// vulnerability.
|
||||
#[inline(always)]
|
||||
pub fn inner_html(self, html: impl Into<Cow<'static, str>>) -> Self {
|
||||
let html = html.into();
|
||||
|
||||
@@ -945,6 +969,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<HtmlElement />", skip_all, fields(tag = %self.element.name())))]
|
||||
#[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
@@ -1011,6 +1036,7 @@ pub fn custom<El: ElementDescriptor>(cx: Scope, el: El) -> HtmlElement<Custom> {
|
||||
}
|
||||
|
||||
/// Creates a text node.
|
||||
#[inline(always)]
|
||||
pub fn text(text: impl Into<Cow<'static, str>>) -> Text {
|
||||
Text::new(text.into())
|
||||
}
|
||||
@@ -1072,6 +1098,7 @@ macro_rules! generate_html_tags {
|
||||
impl std::ops::Deref for [<$tag:camel $($trailing_)?>] {
|
||||
type Target = web_sys::$el_type;
|
||||
|
||||
#[inline(always)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
@@ -1085,6 +1112,7 @@ macro_rules! generate_html_tags {
|
||||
}
|
||||
|
||||
impl std::convert::AsRef<web_sys::HtmlElement> for [<$tag:camel $($trailing_)?>] {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &web_sys::HtmlElement {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
return &self.element;
|
||||
@@ -1095,11 +1123,13 @@ macro_rules! generate_html_tags {
|
||||
}
|
||||
|
||||
impl ElementDescriptor for [<$tag:camel $($trailing_)?>] {
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
stringify!($tag).into()
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
#[inline(always)]
|
||||
fn hydration_id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
@@ -1127,6 +1157,7 @@ macro_rules! generate_html_tags {
|
||||
};
|
||||
(@void) => {};
|
||||
(@void void) => {
|
||||
#[inline(always)]
|
||||
fn is_void(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ impl<T> IntoView for (Scope, T)
|
||||
where
|
||||
T: IntoView,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
self.1.into_view(self.0)
|
||||
}
|
||||
@@ -373,48 +374,53 @@ struct Comment {
|
||||
}
|
||||
|
||||
impl Comment {
|
||||
#[inline]
|
||||
fn new(
|
||||
content: impl Into<Cow<'static, str>>,
|
||||
id: &HydrationKey,
|
||||
closing: bool,
|
||||
) -> Self {
|
||||
let content = content.into();
|
||||
Self::new_inner(content.into(), id, closing)
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let _ = id;
|
||||
let _ = closing;
|
||||
}
|
||||
fn new_inner(
|
||||
content: Cow<'static, str>,
|
||||
id: &HydrationKey,
|
||||
closing: bool,
|
||||
) -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(not(all(target_arch = "wasm32", feature = "web")))] {
|
||||
let _ = id;
|
||||
let _ = closing;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let node = COMMENT.with(|comment| comment.clone_node().unwrap());
|
||||
Self { content }
|
||||
} else {
|
||||
let node = COMMENT.with(|comment| comment.clone_node().unwrap());
|
||||
|
||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
node.set_text_content(Some(&format!(" {content} ")));
|
||||
#[cfg(debug_assertions)]
|
||||
node.set_text_content(Some(&format!(" {content} ")));
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
if HydrationCtx::is_hydrating() {
|
||||
let id = HydrationCtx::to_string(id, closing);
|
||||
if HydrationCtx::is_hydrating() {
|
||||
let id = HydrationCtx::to_string(id, closing);
|
||||
|
||||
if let Some(marker) = hydration::get_marker(&id) {
|
||||
marker.before_with_node_1(&node).unwrap();
|
||||
if let Some(marker) = hydration::get_marker(&id) {
|
||||
marker.before_with_node_1(&node).unwrap();
|
||||
|
||||
marker.remove();
|
||||
} else {
|
||||
crate::warn!(
|
||||
"component with id {id} not found, ignoring it for \
|
||||
hydration"
|
||||
);
|
||||
marker.remove();
|
||||
} else {
|
||||
crate::warn!(
|
||||
"component with id {id} not found, ignoring it for \
|
||||
hydration"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
node,
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
node,
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,6 +658,7 @@ impl View {
|
||||
///
|
||||
/// This method will attach an event listener to **all** child
|
||||
/// [`HtmlElement`] children.
|
||||
#[inline(always)]
|
||||
pub fn on<E: ev::EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
@@ -680,7 +687,7 @@ impl View {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
match &self {
|
||||
Self::Element(el) => {
|
||||
if event.bubbles() {
|
||||
if E::BUBBLES {
|
||||
add_event_listener(&el.element, event.event_delegation_key(), event.name(), event_handler, &None);
|
||||
} else {
|
||||
add_event_listener_undelegated(
|
||||
@@ -919,6 +926,7 @@ macro_rules! impl_into_view_for_tuples {
|
||||
where
|
||||
$($ty: IntoView),*
|
||||
{
|
||||
#[inline]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
paste::paste! {
|
||||
let ($([<$ty:lower>],)*) = self;
|
||||
@@ -993,12 +1001,14 @@ impl IntoView for String {
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "#text", skip_all)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
View::Text(Text::new(self.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
View::Text(Text::new(self.into()))
|
||||
}
|
||||
@@ -1020,6 +1030,7 @@ macro_rules! viewable_primitive {
|
||||
($($child_type:ty),* $(,)?) => {
|
||||
$(
|
||||
impl IntoView for $child_type {
|
||||
#[inline(always)]
|
||||
fn into_view(self, _cx: Scope) -> View {
|
||||
View::Text(Text::new(self.to_string().into()))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use leptos_reactive::Scope;
|
||||
use std::rc::Rc;
|
||||
use std::{borrow::Cow, rc::Rc};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
@@ -11,11 +11,11 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
#[derive(Clone)]
|
||||
pub enum Attribute {
|
||||
/// A plain string value.
|
||||
String(String),
|
||||
String(Cow<'static, str>),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
|
||||
Fn(Scope, Rc<dyn Fn() -> Attribute>),
|
||||
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
|
||||
Option(Scope, Option<String>),
|
||||
Option(Scope, Option<Cow<'static, str>>),
|
||||
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
|
||||
Bool(bool),
|
||||
}
|
||||
@@ -23,9 +23,14 @@ pub enum Attribute {
|
||||
impl Attribute {
|
||||
/// Converts the attribute to its HTML value at that moment, including the attribute name,
|
||||
/// so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, attr_name: &'static str) -> String {
|
||||
pub fn as_value_string(
|
||||
&self,
|
||||
attr_name: &'static str,
|
||||
) -> Cow<'static, str> {
|
||||
match self {
|
||||
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
|
||||
Attribute::String(value) => {
|
||||
format!("{attr_name}=\"{value}\"").into()
|
||||
}
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
@@ -35,23 +40,19 @@ impl Attribute {
|
||||
}
|
||||
Attribute::Option(_, value) => value
|
||||
.as_ref()
|
||||
.map(|value| format!("{attr_name}=\"{value}\""))
|
||||
.map(|value| format!("{attr_name}=\"{value}\"").into())
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
attr_name.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
Cow::Borrowed(if *include { attr_name } else { "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the attribute to its HTML value at that moment, not including
|
||||
/// the attribute name, so it can be rendered on the server.
|
||||
pub fn as_nameless_value_string(&self) -> Option<String> {
|
||||
pub fn as_nameless_value_string(&self) -> Option<Cow<'static, str>> {
|
||||
match self {
|
||||
Attribute::String(value) => Some(value.to_string()),
|
||||
Attribute::String(value) => Some(value.clone()),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
@@ -59,12 +60,10 @@ impl Attribute {
|
||||
}
|
||||
value.as_nameless_value_string()
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
value.as_ref().map(|value| value.to_string())
|
||||
}
|
||||
Attribute::Option(_, value) => value.as_ref().cloned(),
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
Some("".to_string())
|
||||
Some("".into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -109,18 +108,19 @@ pub trait IntoAttribute {
|
||||
}
|
||||
|
||||
impl<T: IntoAttribute + 'static> From<T> for Box<dyn IntoAttribute> {
|
||||
#[inline(always)]
|
||||
fn from(value: T) -> Self {
|
||||
Box::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Attribute {
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
fn into_attribute_boxed(self: Box<Self>, _: Scope) -> Attribute {
|
||||
*self
|
||||
}
|
||||
@@ -128,7 +128,7 @@ impl IntoAttribute for Attribute {
|
||||
|
||||
macro_rules! impl_into_attr_boxed {
|
||||
() => {
|
||||
#[inline]
|
||||
#[inline(always)]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
@@ -136,6 +136,7 @@ macro_rules! impl_into_attr_boxed {
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Attribute> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
self.unwrap_or(Attribute::Option(cx, None))
|
||||
}
|
||||
@@ -144,6 +145,25 @@ impl IntoAttribute for Option<Attribute> {
|
||||
}
|
||||
|
||||
impl IntoAttribute for String {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(Cow::Owned(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(Cow::Borrowed(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Cow<'static, str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self)
|
||||
}
|
||||
@@ -152,6 +172,7 @@ impl IntoAttribute for String {
|
||||
}
|
||||
|
||||
impl IntoAttribute for bool {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::Bool(self)
|
||||
}
|
||||
@@ -160,6 +181,25 @@ impl IntoAttribute for bool {
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<String> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(Cow::Owned))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<&'static str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(Cow::Borrowed))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Cow<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self)
|
||||
}
|
||||
@@ -181,6 +221,7 @@ where
|
||||
}
|
||||
|
||||
impl<T: IntoAttribute> IntoAttribute for (Scope, T) {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self.1.into_attribute(self.0)
|
||||
}
|
||||
@@ -200,6 +241,7 @@ impl IntoAttribute for (Scope, Option<Box<dyn IntoAttribute>>) {
|
||||
}
|
||||
|
||||
impl IntoAttribute for (Scope, Box<dyn IntoAttribute>) {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self.1.into_attribute_boxed(self.0)
|
||||
}
|
||||
@@ -211,7 +253,7 @@ macro_rules! attr_type {
|
||||
($attr_type:ty) => {
|
||||
impl IntoAttribute for $attr_type {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self.to_string())
|
||||
Attribute::String(self.to_string().into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -222,7 +264,7 @@ macro_rules! attr_type {
|
||||
|
||||
impl IntoAttribute for Option<$attr_type> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(|n| n.to_string()))
|
||||
Attribute::Option(cx, self.map(|n| n.to_string().into()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -234,7 +276,6 @@ macro_rules! attr_type {
|
||||
}
|
||||
|
||||
attr_type!(&String);
|
||||
attr_type!(&str);
|
||||
attr_type!(usize);
|
||||
attr_type!(u8);
|
||||
attr_type!(u16);
|
||||
@@ -251,10 +292,9 @@ attr_type!(f32);
|
||||
attr_type!(f64);
|
||||
attr_type!(char);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
pub fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
@@ -277,6 +317,7 @@ pub fn attribute_helper(
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn attribute_expression(
|
||||
el: &web_sys::Element,
|
||||
attr_name: &str,
|
||||
|
||||
@@ -21,6 +21,7 @@ pub trait IntoClass {
|
||||
}
|
||||
|
||||
impl IntoClass for bool {
|
||||
#[inline(always)]
|
||||
fn into_class(self, _cx: Scope) -> Class {
|
||||
Class::Value(self)
|
||||
}
|
||||
@@ -30,6 +31,7 @@ impl<T> IntoClass for T
|
||||
where
|
||||
T: Fn() -> bool + 'static,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn into_class(self, cx: Scope) -> Class {
|
||||
let modified_fn = Box::new(self);
|
||||
Class::Fn(cx, modified_fn)
|
||||
@@ -60,6 +62,7 @@ impl Class {
|
||||
}
|
||||
|
||||
impl<T: IntoClass> IntoClass for (Scope, T) {
|
||||
#[inline(always)]
|
||||
fn into_class(self, _: Scope) -> Class {
|
||||
self.1.into_class(self.0)
|
||||
}
|
||||
@@ -70,6 +73,7 @@ use std::borrow::Cow;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
pub fn class_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
@@ -95,6 +99,7 @@ pub fn class_helper(
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn class_expression(
|
||||
class_list: &web_sys::DomTokenList,
|
||||
class_name: &str,
|
||||
|
||||
@@ -36,6 +36,7 @@ where
|
||||
}
|
||||
|
||||
impl<T: IntoProperty> IntoProperty for (Scope, T) {
|
||||
#[inline(always)]
|
||||
fn into_property(self, _: Scope) -> Property {
|
||||
self.1.into_property(self.0)
|
||||
}
|
||||
@@ -44,12 +45,14 @@ impl<T: IntoProperty> IntoProperty for (Scope, T) {
|
||||
macro_rules! prop_type {
|
||||
($prop_type:ty) => {
|
||||
impl IntoProperty for $prop_type {
|
||||
#[inline(always)]
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoProperty for Option<$prop_type> {
|
||||
#[inline(always)]
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
@@ -81,6 +84,7 @@ prop_type!(bool);
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn property_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
@@ -106,6 +110,7 @@ pub(crate) fn property_helper(
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn property_expression(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
|
||||
@@ -35,6 +35,7 @@ use std::cell::Cell;
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[repr(transparent)]
|
||||
pub struct NodeRef<T: ElementDescriptor + 'static>(
|
||||
RwSignal<Option<HtmlElement<T>>>,
|
||||
);
|
||||
@@ -70,6 +71,7 @@ pub struct NodeRef<T: ElementDescriptor + 'static>(
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn create_node_ref<T: ElementDescriptor + 'static>(
|
||||
cx: Scope,
|
||||
) -> NodeRef<T> {
|
||||
@@ -89,6 +91,7 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
|
||||
/// Initially, the value will be `None`, but once it is loaded the effect
|
||||
/// will rerun and its value will be `Some(Element)`.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> Option<HtmlElement<T>>
|
||||
where
|
||||
T: Clone,
|
||||
@@ -96,6 +99,18 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
|
||||
self.0.get()
|
||||
}
|
||||
|
||||
/// Gets the element that is currently stored in the reference.
|
||||
///
|
||||
/// This **does not** track reactively.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn get_untracked(&self) -> Option<HtmlElement<T>>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.get_untracked()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Loads an element into the reference. This tracks reactively,
|
||||
/// so that effects that use the node reference will rerun once it is loaded,
|
||||
@@ -120,6 +135,7 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
|
||||
|
||||
/// Runs the provided closure when the `NodeRef` has been connected
|
||||
/// with it's [`HtmlElement`].
|
||||
#[inline(always)]
|
||||
pub fn on_load<F>(self, cx: Scope, f: F)
|
||||
where
|
||||
T: Clone,
|
||||
@@ -148,18 +164,21 @@ cfg_if::cfg_if! {
|
||||
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
|
||||
type Output = Option<HtmlElement<T>>;
|
||||
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + ElementDescriptor + 'static> FnMut<()> for NodeRef<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + ElementDescriptor + Clone + 'static> Fn<()> for NodeRef<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
|
||||
@@ -244,19 +244,28 @@ fn fragments_to_chunks(
|
||||
impl View {
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
|
||||
self.render_to_string_helper()
|
||||
self.render_to_string_helper(false)
|
||||
}
|
||||
|
||||
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
|
||||
pub(crate) fn render_to_string_helper(
|
||||
self,
|
||||
dont_escape_text: bool,
|
||||
) -> Cow<'static, str> {
|
||||
match self {
|
||||
View::Text(node) => {
|
||||
html_escape::encode_safe(&node.content).to_string().into()
|
||||
if dont_escape_text {
|
||||
node.content
|
||||
} else {
|
||||
html_escape::encode_safe(&node.content).to_string().into()
|
||||
}
|
||||
}
|
||||
View::Component(node) => {
|
||||
let content = || {
|
||||
node.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.map(|node| {
|
||||
node.render_to_string_helper(dont_escape_text)
|
||||
})
|
||||
.join("")
|
||||
};
|
||||
cfg_if! {
|
||||
@@ -283,7 +292,8 @@ impl View {
|
||||
}
|
||||
View::Suspense(id, node) => format!(
|
||||
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
|
||||
View::CoreComponent(node).render_to_string_helper()
|
||||
View::CoreComponent(node)
|
||||
.render_to_string_helper(dont_escape_text)
|
||||
)
|
||||
.into(),
|
||||
View::CoreComponent(node) => {
|
||||
@@ -333,7 +343,9 @@ impl View {
|
||||
t.content
|
||||
}
|
||||
} else {
|
||||
child.render_to_string_helper()
|
||||
child.render_to_string_helper(
|
||||
dont_escape_text,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
@@ -356,7 +368,9 @@ impl View {
|
||||
let id = node.id;
|
||||
|
||||
let content = || {
|
||||
node.child.render_to_string_helper()
|
||||
node.child.render_to_string_helper(
|
||||
dont_escape_text,
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -409,6 +423,8 @@ impl View {
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
let is_script_or_style =
|
||||
el.name == "script" || el.name == "style";
|
||||
let el_html = if let ElementChildren::Chunks(chunks) =
|
||||
el.children
|
||||
{
|
||||
@@ -416,9 +432,8 @@ impl View {
|
||||
.into_iter()
|
||||
.map(|chunk| match chunk {
|
||||
StringOrView::String(string) => string,
|
||||
StringOrView::View(view) => {
|
||||
view().render_to_string_helper()
|
||||
}
|
||||
StringOrView::View(view) => view()
|
||||
.render_to_string_helper(is_script_or_style),
|
||||
})
|
||||
.join("")
|
||||
.into()
|
||||
@@ -460,7 +475,11 @@ impl View {
|
||||
ElementChildren::Empty => "".into(),
|
||||
ElementChildren::Children(c) => c
|
||||
.into_iter()
|
||||
.map(View::render_to_string_helper)
|
||||
.map(|v| {
|
||||
v.render_to_string_helper(
|
||||
is_script_or_style,
|
||||
)
|
||||
})
|
||||
.join("")
|
||||
.into(),
|
||||
ElementChildren::InnerHtml(h) => h,
|
||||
|
||||
@@ -213,7 +213,7 @@ 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();
|
||||
self.into_stream_chunks_helper(cx, &mut chunks);
|
||||
self.into_stream_chunks_helper(cx, &mut chunks, false);
|
||||
chunks
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ impl View {
|
||||
self,
|
||||
cx: Scope,
|
||||
chunks: &mut VecDeque<StreamChunk>,
|
||||
dont_escape_text: bool,
|
||||
) {
|
||||
match self {
|
||||
View::Suspense(id, _) => {
|
||||
@@ -241,18 +242,21 @@ impl View {
|
||||
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()));
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
child.into_stream_chunks_helper(cx, chunks, dont_escape_text);
|
||||
}
|
||||
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-end-->"#, HydrationCtx::to_string(&node.id, true)).into()));
|
||||
} else {
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
child.into_stream_chunks_helper(cx, chunks, dont_escape_text);
|
||||
}
|
||||
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}-->"#, HydrationCtx::to_string(&node.id, true)).into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
let is_script_or_style =
|
||||
el.name == "script" || el.name == "style";
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(id) = &el.view_marker {
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
@@ -266,7 +270,11 @@ impl View {
|
||||
chunks.push_back(StreamChunk::Sync(string))
|
||||
}
|
||||
StringOrView::View(view) => {
|
||||
view().into_stream_chunks_helper(cx, chunks);
|
||||
view().into_stream_chunks_helper(
|
||||
cx,
|
||||
chunks,
|
||||
is_script_or_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,7 +326,11 @@ impl View {
|
||||
ElementChildren::Empty => {}
|
||||
ElementChildren::Children(children) => {
|
||||
for child in children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
child.into_stream_chunks_helper(
|
||||
cx,
|
||||
chunks,
|
||||
is_script_or_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
ElementChildren::InnerHtml(inner_html) => {
|
||||
@@ -387,22 +399,33 @@ impl View {
|
||||
// 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 {
|
||||
let content = if dont_escape_text {
|
||||
t.content
|
||||
} else {
|
||||
html_escape::encode_safe(
|
||||
&t.content,
|
||||
)
|
||||
.to_string()
|
||||
.into()
|
||||
};
|
||||
chunks.push_back(
|
||||
if !cfg!(debug_assertions) {
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!>{}",
|
||||
html_escape::encode_safe(&t.content)
|
||||
content
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(html_escape::encode_safe(&t.content).to_string().into())
|
||||
StreamChunk::Sync(content)
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
cx,
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -435,7 +458,9 @@ impl View {
|
||||
);
|
||||
node.child
|
||||
.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
cx,
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
@@ -447,6 +472,26 @@ impl View {
|
||||
),
|
||||
);
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
node.child
|
||||
.into_stream_chunks_helper(
|
||||
cx,
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}-->",
|
||||
HydrationCtx::to_string(
|
||||
&id, true
|
||||
)
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4,10 +4,12 @@ use std::{any::Any, fmt, rc::Rc};
|
||||
|
||||
/// Wrapper for arbitrary data that can be passed through the view.
|
||||
#[derive(Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct Transparent(Rc<dyn Any>);
|
||||
|
||||
impl Transparent {
|
||||
/// Creates a new wrapper for this data.
|
||||
#[inline(always)]
|
||||
pub fn new<T>(value: T) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
@@ -16,6 +18,7 @@ impl Transparent {
|
||||
}
|
||||
|
||||
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
|
||||
#[inline(always)]
|
||||
pub fn downcast_ref<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -31,6 +34,7 @@ impl fmt::Debug for Transparent {
|
||||
}
|
||||
|
||||
impl PartialEq for Transparent {
|
||||
#[inline(always)]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::ptr::eq(&self.0, &other.0)
|
||||
}
|
||||
@@ -39,6 +43,7 @@ impl PartialEq for Transparent {
|
||||
impl Eq for Transparent {}
|
||||
|
||||
impl IntoView for Transparent {
|
||||
#[inline(always)]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
View::Transparent(self)
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ impl ToTokens for Model {
|
||||
let lifetimes = body.sig.generics.lifetimes();
|
||||
|
||||
let props_name = format_ident!("{name}Props");
|
||||
let props_builder_name = format_ident!("{name}PropsBuilder");
|
||||
let trace_name = format!("<{name} />");
|
||||
|
||||
let prop_builder_fields = prop_builder_fields(vis, props);
|
||||
@@ -200,6 +201,13 @@ impl ToTokens for Model {
|
||||
#prop_builder_fields
|
||||
}
|
||||
|
||||
impl #generics ::leptos::Props for #props_name #generics #where_clause {
|
||||
type Builder = #props_builder_name #generics;
|
||||
fn builder() -> Self::Builder {
|
||||
#props_name::builder()
|
||||
}
|
||||
}
|
||||
|
||||
#docs
|
||||
#component_fn_prop_docs
|
||||
#[allow(non_snake_case, clippy::too_many_arguments)]
|
||||
|
||||
@@ -393,6 +393,7 @@ fn element_to_tokens_ssr(
|
||||
.to_string()
|
||||
.replace("svg::", "")
|
||||
.replace("math::", "");
|
||||
let is_script_or_style = tag_name == "script" || tag_name == "style";
|
||||
template.push('<');
|
||||
template.push_str(&tag_name);
|
||||
|
||||
@@ -406,6 +407,7 @@ fn element_to_tokens_ssr(
|
||||
template,
|
||||
holes,
|
||||
exprs_for_compiler,
|
||||
global_class,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -461,9 +463,16 @@ fn element_to_tokens_ssr(
|
||||
}
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
template.push_str(&html_escape::encode_safe(
|
||||
&value,
|
||||
));
|
||||
let value = if is_script_or_style {
|
||||
value.into()
|
||||
} else {
|
||||
html_escape::encode_safe(&value)
|
||||
};
|
||||
template.push_str(
|
||||
&value
|
||||
.replace('{', "{{")
|
||||
.replace('}', "}}"),
|
||||
);
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
let value = text.value.as_ref();
|
||||
@@ -513,14 +522,17 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> Option<&'a NodeValueExpr> {
|
||||
let name = node.key.to_string();
|
||||
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
|
||||
// ignore refs on SSR
|
||||
} else if name.strip_prefix("on:").is_some() {
|
||||
let (event_type, handler) = event_from_attribute_node(node, false);
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = attribute_value(node);
|
||||
let (event_type, _, _) = parse_event_name(name);
|
||||
|
||||
exprs_for_compiler.push(quote! {
|
||||
leptos::leptos_dom::helpers::ssr_event_listener(#event_type, #handler);
|
||||
leptos::leptos_dom::helpers::ssr_event_listener(::leptos::ev::#event_type, #handler);
|
||||
})
|
||||
} else if name.strip_prefix("prop:").is_some()
|
||||
|| name.strip_prefix("class:").is_some()
|
||||
@@ -532,6 +544,18 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
} else {
|
||||
let name = name.replacen("attr:", "", 1);
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& node.value.as_ref().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = node.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
|
||||
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
|
||||
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
|
||||
};
|
||||
|
||||
if name != "class" {
|
||||
template.push(' ');
|
||||
|
||||
@@ -763,7 +787,7 @@ fn node_to_tokens(
|
||||
let value = node.value.as_ref();
|
||||
quote! { #value }
|
||||
}
|
||||
Node::Attribute(node) => attribute_to_tokens(cx, node),
|
||||
Node::Attribute(node) => attribute_to_tokens(cx, node, global_class),
|
||||
Node::Element(node) => {
|
||||
element_to_tokens(cx, node, parent_type, global_class, view_marker)
|
||||
}
|
||||
@@ -821,7 +845,7 @@ fn element_to_tokens(
|
||||
if node.key.to_string().trim().starts_with("class:") {
|
||||
None
|
||||
} else {
|
||||
Some(attribute_to_tokens(cx, node))
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -830,7 +854,7 @@ fn element_to_tokens(
|
||||
let class_attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
if node.key.to_string().trim().starts_with("class:") {
|
||||
Some(attribute_to_tokens(cx, node))
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -850,37 +874,67 @@ fn element_to_tokens(
|
||||
}
|
||||
};
|
||||
let children = node.children.iter().map(|node| {
|
||||
let child = match node {
|
||||
Node::Fragment(fragment) => fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
None,
|
||||
let (child, is_static) = match node {
|
||||
Node::Fragment(fragment) => (
|
||||
fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
None,
|
||||
),
|
||||
false,
|
||||
),
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
quote! {
|
||||
#[allow(unused_braces)] #value
|
||||
if let Some(primitive) = value_to_string(&node.value) {
|
||||
(quote! { #primitive }, true)
|
||||
} else {
|
||||
let value = node.value.as_ref();
|
||||
(
|
||||
quote! {
|
||||
#[allow(unused_braces)] #value
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
Node::Block(node) => {
|
||||
let value = node.value.as_ref();
|
||||
quote! {
|
||||
#[allow(unused_braces)] #value
|
||||
if let Some(primitive) = value_to_string(&node.value) {
|
||||
(quote! { #primitive }, true)
|
||||
} else {
|
||||
let value = node.value.as_ref();
|
||||
(
|
||||
quote! {
|
||||
#[allow(unused_braces)] #value
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
Node::Element(node) => {
|
||||
element_to_tokens(cx, node, parent_type, global_class, None)
|
||||
}
|
||||
Node::Element(node) => (
|
||||
element_to_tokens(
|
||||
cx,
|
||||
node,
|
||||
parent_type,
|
||||
global_class,
|
||||
None,
|
||||
),
|
||||
false,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => {
|
||||
quote! {}
|
||||
(quote! {}, false)
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
.child((#cx, #child))
|
||||
if is_static {
|
||||
quote! {
|
||||
.child(#child)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.child((#cx, #child))
|
||||
}
|
||||
}
|
||||
});
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
@@ -899,7 +953,11 @@ fn element_to_tokens(
|
||||
}
|
||||
}
|
||||
|
||||
fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
fn attribute_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeAttribute,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let span = node.key.span();
|
||||
let name = node.key.to_string();
|
||||
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
|
||||
@@ -911,24 +969,9 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = attribute_value(node);
|
||||
let (name, is_force_undelegated) = parse_event(name);
|
||||
|
||||
let event_type = TYPED_EVENTS
|
||||
.iter()
|
||||
.find(|e| **e == name)
|
||||
.copied()
|
||||
.unwrap_or("Custom");
|
||||
let is_custom = event_type == "Custom";
|
||||
|
||||
let Ok(event_type) = event_type.parse::<TokenStream>() else {
|
||||
abort!(event_type, "couldn't parse event name");
|
||||
};
|
||||
|
||||
let event_type = if is_custom {
|
||||
quote! { Custom::new(#name) }
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
let (event_type, is_custom, is_force_undelegated) =
|
||||
parse_event_name(name);
|
||||
|
||||
let event_name_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => {
|
||||
@@ -1025,6 +1068,18 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
return fancy;
|
||||
}
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& node.value.as_ref().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = node.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
|
||||
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
|
||||
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
|
||||
};
|
||||
|
||||
// all other attributes
|
||||
let value = match node.value.as_ref() {
|
||||
Some(value) => {
|
||||
@@ -1034,6 +1089,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
}
|
||||
None => quote_spanned! { span => "" },
|
||||
};
|
||||
|
||||
let attr = match &node.key {
|
||||
NodeName::Punctuated(parts) => Some(&parts[0]),
|
||||
_ => None,
|
||||
@@ -1054,6 +1110,28 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
|
||||
let (name, is_force_undelegated) = parse_event(name);
|
||||
|
||||
let event_type = TYPED_EVENTS
|
||||
.iter()
|
||||
.find(|e| **e == name)
|
||||
.copied()
|
||||
.unwrap_or("Custom");
|
||||
let is_custom = event_type == "Custom";
|
||||
|
||||
let Ok(event_type) = event_type.parse::<TokenStream>() else {
|
||||
abort!(event_type, "couldn't parse event name");
|
||||
};
|
||||
|
||||
let event_type = if is_custom {
|
||||
quote! { Custom::new(#name) }
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
(event_type, is_custom, is_force_undelegated)
|
||||
}
|
||||
|
||||
pub(crate) fn component_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
@@ -1062,7 +1140,6 @@ pub(crate) fn component_to_tokens(
|
||||
let name = &node.name;
|
||||
let component_name = ident_from_tag_name(&node.name);
|
||||
let span = node.name.span();
|
||||
let component_props_name = format_ident!("{component_name}Props");
|
||||
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
@@ -1154,7 +1231,7 @@ pub(crate) fn component_to_tokens(
|
||||
let component = quote! {
|
||||
#name(
|
||||
#cx,
|
||||
#component_props_name::builder()
|
||||
::leptos::component_props_builder(&#name)
|
||||
#(#props)*
|
||||
#children
|
||||
.build()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn rs_deep_update(c: &mut Criterion) {
|
||||
use reactive_signals::{Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
use reactive_signals::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Signal,
|
||||
};
|
||||
|
||||
c.bench_function("rs_deep_update", |b| {
|
||||
b.iter(|| {
|
||||
@@ -25,78 +27,84 @@ fn rs_deep_update(c: &mut Criterion) {
|
||||
fn l021_deep_update(c: &mut Criterion) {
|
||||
use l021::*;
|
||||
|
||||
c.bench_function("l021_deep_update", |b| {
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
runtime.dispose();
|
||||
});
|
||||
c.bench_function("l021_deep_update", |b| {
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
runtime.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
fn sycamore_deep_update(c: &mut Criterion) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
c.bench_function("sycamore_deep_update", |b| {
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(*memos[999].get(), 1001);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
});
|
||||
c.bench_function("sycamore_deep_update", |b| {
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(*memos[999].get(), 1001);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn leptos_deep_update(c: &mut Criterion) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
c.bench_function("leptos_deep_update", |b| {
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
});
|
||||
c.bench_function("leptos_deep_update", |b| {
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
criterion_group!(deep, rs_deep_update, l021_deep_update, sycamore_deep_update, leptos_deep_update);
|
||||
criterion_main!(deep);
|
||||
criterion_group!(
|
||||
deep,
|
||||
rs_deep_update,
|
||||
l021_deep_update,
|
||||
sycamore_deep_update,
|
||||
leptos_deep_update
|
||||
);
|
||||
criterion_main!(deep);
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn rs_fan_out(c: &mut Criterion) {
|
||||
use reactive_signals::{Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
use reactive_signals::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Signal,
|
||||
};
|
||||
|
||||
c.bench_function("rs_fan_out", |b| {
|
||||
b.iter(|| {
|
||||
let cx = ClientRuntime::bench_root_scope();
|
||||
let sig = signal!(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| signal!(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
let sig = signal!(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| signal!(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -20,63 +22,72 @@ fn rs_fan_out(c: &mut Criterion) {
|
||||
fn l021_fan_out(c: &mut Criterion) {
|
||||
use l021::*;
|
||||
|
||||
c.bench_function("l021_fan_out", |b| {
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
runtime.dispose();
|
||||
});
|
||||
c.bench_function("l021_fan_out", |b| {
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
runtime.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
fn sycamore_fan_out(c: &mut Criterion) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
c.bench_function("sycamore_fan_out", |b| {
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let sig = create_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 1000);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
});
|
||||
c.bench_function("sycamore_fan_out", |b| {
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let sig = create_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(
|
||||
memos.iter().map(|m| *(*m.get())).sum::<i32>(),
|
||||
1000
|
||||
);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn leptos_fan_out(c: &mut Criterion) {
|
||||
use leptos_reactive::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
c.bench_function("leptos_fan_out", |b| {
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
});
|
||||
c.bench_function("leptos_fan_out", |b| {
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
criterion_group!(fan_out, rs_fan_out, l021_fan_out, sycamore_fan_out, leptos_fan_out);
|
||||
criterion_main!(fan_out);
|
||||
criterion_group!(
|
||||
fan_out,
|
||||
rs_fan_out,
|
||||
l021_fan_out,
|
||||
sycamore_fan_out,
|
||||
leptos_fan_out
|
||||
);
|
||||
criterion_main!(fan_out);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use std::rc::Rc;
|
||||
use std::cell::Cell;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
fn rs_narrow_down(c: &mut Criterion) {
|
||||
use reactive_signals::{Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
use reactive_signals::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Signal,
|
||||
};
|
||||
|
||||
c.bench_function("rs_narrow_down", |b| {
|
||||
b.iter(|| {
|
||||
let cx = ClientRuntime::bench_root_scope();
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = signal!(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo.get(), 499500);
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
Rc::new((0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>());
|
||||
let memo = signal!(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo.get(), 499500);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -24,69 +24,75 @@ fn rs_narrow_down(c: &mut Criterion) {
|
||||
fn l021_narrow_down(c: &mut Criterion) {
|
||||
use l021::*;
|
||||
|
||||
c.bench_function("l021_narrow_down", |b| {
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
runtime.dispose();
|
||||
});
|
||||
c.bench_function("l021_narrow_down", |b| {
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
runtime.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
fn sycamore_narrow_down(c: &mut Criterion) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
c.bench_function("sycamore_narrow_down", |b| {
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(*memo.get(), 499500);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
});
|
||||
c.bench_function("sycamore_narrow_down", |b| {
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(*memo.get(), 499500);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn leptos_narrow_down(c: &mut Criterion) {
|
||||
use leptos_reactive::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
c.bench_function("leptos_narrow_down", |b| {
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
});
|
||||
c.bench_function("leptos_narrow_down", |b| {
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
criterion_group!(narrow_down, rs_narrow_down, l021_narrow_down, sycamore_narrow_down, leptos_narrow_down);
|
||||
criterion_main!(narrow_down);
|
||||
criterion_group!(
|
||||
narrow_down,
|
||||
rs_narrow_down,
|
||||
l021_narrow_down,
|
||||
sycamore_narrow_down,
|
||||
leptos_narrow_down
|
||||
);
|
||||
criterion_main!(narrow_down);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use crate::{runtime::with_runtime, Scope};
|
||||
use std::any::{Any, TypeId};
|
||||
|
||||
/// Provides a context value of type `T` to the current reactive [Scope](crate::Scope)
|
||||
/// and all of its descendants. This can be consumed using [use_context](crate::use_context).
|
||||
/// Provides a context value of type `T` to the current reactive [`Scope`](crate::Scope)
|
||||
/// and all of its descendants. This can be consumed using [`use_context`](crate::use_context).
|
||||
///
|
||||
/// This is useful for passing values down to components or functions lower in a
|
||||
/// hierarchy without needs to “prop drill” by passing them through each layer as
|
||||
@@ -27,7 +27,7 @@ use std::any::{Any, TypeId};
|
||||
/// #[component]
|
||||
/// pub fn Provider(cx: Scope) -> impl IntoView {
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
///
|
||||
///
|
||||
/// // the newtype pattern isn't *necessary* here but is a good practice
|
||||
/// // it avoids confusion with other possible future `WriteSignal<bool>` contexts
|
||||
/// // and makes it easier to refer to it in ButtonD
|
||||
@@ -61,9 +61,9 @@ where
|
||||
}
|
||||
|
||||
/// Extracts a context value of type `T` from the reactive system by traversing
|
||||
/// it upwards, beginning from the current [Scope](crate::Scope) and iterating
|
||||
/// it upwards, beginning from the current [`Scope`](crate::Scope) and iterating
|
||||
/// through its parents, if any. The context value should have been provided elsewhere
|
||||
/// using [provide_context](crate::provide_context).
|
||||
/// using [`provide_context`](crate::provide_context).
|
||||
///
|
||||
/// This is useful for passing values down to components or functions lower in a
|
||||
/// hierarchy without needs to “prop drill” by passing them through each layer as
|
||||
@@ -86,7 +86,7 @@ where
|
||||
/// #[component]
|
||||
/// pub fn Provider(cx: Scope) -> impl IntoView {
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
///
|
||||
///
|
||||
/// // the newtype pattern isn't *necessary* here but is a good practice
|
||||
/// // it avoids confusion with other possible future `WriteSignal<bool>` contexts
|
||||
/// // and makes it easier to refer to it in ButtonD
|
||||
@@ -140,3 +140,61 @@ where
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Extracts a context value of type `T` from the reactive system by traversing
|
||||
/// it upwards, beginning from the current [Scope](crate::Scope) and iterating
|
||||
/// through its parents, if any. The context value should have been provided elsewhere
|
||||
/// using [provide_context](crate::provide_context).
|
||||
///
|
||||
/// This is useful for passing values down to components or functions lower in a
|
||||
/// hierarchy without needs to “prop drill” by passing them through each layer as
|
||||
/// arguments to a function or properties of a component.
|
||||
///
|
||||
/// Context works similarly to variable scope: a context that is provided higher in
|
||||
/// the component tree can be used lower down, but a context that is provided lower
|
||||
/// in the tree cannot be used higher up.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// // define a newtype we'll provide as context
|
||||
/// // contexts are stored by their types, so it can be useful to create
|
||||
/// // a new type to avoid confusion with other `WriteSignal<i32>`s we may have
|
||||
/// // all types to be shared via context should implement `Clone`
|
||||
/// #[derive(Copy, Clone)]
|
||||
/// struct ValueSetter(WriteSignal<i32>);
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn Provider(cx: Scope) -> impl IntoView {
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
///
|
||||
/// // the newtype pattern isn't *necessary* here but is a good practice
|
||||
/// // it avoids confusion with other possible future `WriteSignal<bool>` contexts
|
||||
/// // and makes it easier to refer to it in ButtonD
|
||||
/// provide_context(cx, ValueSetter(set_value));
|
||||
///
|
||||
/// // because <Consumer/> is nested inside <Provider/>,
|
||||
/// // it has access to the provided context
|
||||
/// view! { cx, <div><Consumer/></div> }
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn Consumer(cx: Scope) -> impl IntoView {
|
||||
/// // consume the provided context of type `ValueSetter` using `use_context`
|
||||
/// // this traverses up the tree of `Scope`s and gets the nearest provided `ValueSetter`
|
||||
/// let set_value = expect_context::<ValueSetter>(cx).0;
|
||||
///
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
pub fn expect_context<T>(cx: Scope) -> T
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
use_context(cx).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"context of type {:?} to be present",
|
||||
std::any::type_name::<T>()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
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.
|
||||
@@ -23,7 +21,7 @@ pub(crate) struct AccessDiagnostics {}
|
||||
#[doc(hidden)]
|
||||
pub struct SpecialNonReactiveZone {}
|
||||
|
||||
cfg_if! {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
use std::cell::Cell;
|
||||
|
||||
@@ -35,6 +33,7 @@ cfg_if! {
|
||||
|
||||
impl SpecialNonReactiveZone {
|
||||
#[allow(dead_code)] // allowed for SSR
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_inside() -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -44,6 +43,7 @@ impl SpecialNonReactiveZone {
|
||||
false
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn enter() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -51,6 +51,7 @@ impl SpecialNonReactiveZone {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn exit() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -63,7 +64,7 @@ impl SpecialNonReactiveZone {
|
||||
#[macro_export]
|
||||
macro_rules! diagnostics {
|
||||
($this:ident) => {{
|
||||
cfg_if! {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
AccessDiagnostics {
|
||||
defined_at: $this.defined_at,
|
||||
|
||||
@@ -11,14 +11,14 @@ use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
/// Effects are intended to run *side-effects* of the system, not to synchronize state
|
||||
/// *within* the system. In other words: don't write to signals within effects.
|
||||
/// (If you need to define a signal that depends on the value of other signals, use a
|
||||
/// derived signal or [create_memo](crate::create_memo)).
|
||||
/// derived signal or [`create_memo`](crate::create_memo)).
|
||||
///
|
||||
/// The effect function is called with an argument containing whatever value it returned
|
||||
/// the last time it ran. On the initial run, this is `None`.
|
||||
///
|
||||
/// By default, effects **do not run on the server**. This means you can call browser-specific
|
||||
/// APIs within the effect function without causing issues. If you need an effect to run on
|
||||
/// the server, use [create_isomorphic_effect].
|
||||
/// the server, use [`create_isomorphic_effect`].
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use log::*;
|
||||
@@ -58,6 +58,7 @@ use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn create_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
|
||||
where
|
||||
T: 'static,
|
||||
@@ -66,7 +67,7 @@ where
|
||||
if #[cfg(not(feature = "ssr"))] {
|
||||
let e = cx.runtime.create_effect(f);
|
||||
//eprintln!("created effect {e:?}");
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
|
||||
cx.push_scope_property(ScopeProperty::Effect(e))
|
||||
} else {
|
||||
// clear warnings
|
||||
_ = cx;
|
||||
@@ -75,7 +76,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an effect; unlike effects created by [create_effect], isomorphic effects will run on
|
||||
/// Creates an effect; unlike effects created by [`create_effect`], isomorphic effects will run on
|
||||
/// the server as well as the client.
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
@@ -113,6 +114,7 @@ where
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn create_isomorphic_effect<T>(
|
||||
cx: Scope,
|
||||
f: impl Fn(Option<T>) -> T + 'static,
|
||||
@@ -121,7 +123,7 @@ pub fn create_isomorphic_effect<T>(
|
||||
{
|
||||
let e = cx.runtime.create_effect(f);
|
||||
//eprintln!("created effect {e:?}");
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
|
||||
cx.push_scope_property(ScopeProperty::Effect(e))
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -136,6 +138,7 @@ pub fn create_isomorphic_effect<T>(
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn create_render_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
|
||||
where
|
||||
T: 'static,
|
||||
|
||||
@@ -17,18 +17,19 @@
|
||||
//! Here are the most commonly-used functions and types you'll need to build a reactive system:
|
||||
//!
|
||||
//! ### Signals
|
||||
//! 1. *Signals:* [create_signal](crate::create_signal), which returns a ([ReadSignal](crate::ReadSignal),
|
||||
//! [WriteSignal](crate::WriteSignal)) tuple, or [create_rw_signal](crate::create_rw_signal), which returns
|
||||
//! a signal [RwSignal](crate::RwSignal) without this read-write segregation.
|
||||
//! 1. *Signals:* [`create_signal`](crate::create_signal), which returns a ([`ReadSignal`](crate::ReadSignal),
|
||||
//! [`WriteSignal`](crate::WriteSignal)) tuple, or [`create_rw_signal`](crate::create_rw_signal), which returns
|
||||
//! a signal [`RwSignal`](crate::RwSignal) without this read-write segregation.
|
||||
//! 2. *Derived Signals:* any function that relies on another signal.
|
||||
//! 3. *Memos:* [create_memo](crate::create_memo), which returns a [Memo](crate::Memo).
|
||||
//! 4. *Resources:* [create_resource], which converts an `async` [std::future::Future] into a
|
||||
//! synchronous [Resource](crate::Resource) signal.
|
||||
//! 3. *Memos:* [`create_memo`], which returns a [`Memo`](crate::Memo).
|
||||
//! 4. *Resources:* [`create_resource`], which converts an `async` [`Future`](std::future::Future) into a
|
||||
//! synchronous [`Resource`](crate::Resource) signal.
|
||||
//! 5. *Triggers:* [`create_trigger`], creates a purely reactive [`Trigger`] primitive without any associated state.
|
||||
//!
|
||||
//! ### Effects
|
||||
//! 1. Use [create_effect](crate::create_effect) when you need to synchronize the reactive system
|
||||
//! 1. Use [`create_effect`](crate::create_effect) when you need to synchronize the reactive system
|
||||
//! with something outside it (for example: logging to the console, writing to a file or local storage)
|
||||
//! 2. The Leptos DOM renderer wraps any [Fn] in your template with [create_effect](crate::create_effect), so
|
||||
//! 2. The Leptos DOM renderer wraps any [`Fn`] in your template with [`create_effect`](crate::create_effect), so
|
||||
//! components you write do *not* need explicit effects to synchronize with the DOM.
|
||||
//!
|
||||
//! ### Example
|
||||
@@ -90,6 +91,7 @@ mod spawn;
|
||||
mod spawn_microtask;
|
||||
mod stored_value;
|
||||
pub mod suspense;
|
||||
mod trigger;
|
||||
|
||||
pub use context::*;
|
||||
pub use diagnostics::SpecialNonReactiveZone;
|
||||
@@ -109,6 +111,7 @@ pub use spawn::*;
|
||||
pub use spawn_microtask::*;
|
||||
pub use stored_value::*;
|
||||
pub use suspense::SuspenseContext;
|
||||
pub use trigger::*;
|
||||
|
||||
mod macros {
|
||||
macro_rules! debug_warn {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_effect, diagnostics::AccessDiagnostics, node::NodeId, on_cleanup,
|
||||
with_runtime, AnyComputation, RuntimeId, Scope, SignalDispose, SignalGet,
|
||||
SignalGetUntracked, SignalStream, SignalWith, SignalWithUntracked,
|
||||
with_runtime, AnyComputation, RuntimeId, Scope, ScopeProperty,
|
||||
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.
|
||||
@@ -20,7 +20,7 @@ use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
|
||||
/// create a derived signal. But if the derivation calculation is expensive, you should
|
||||
/// create a memo.
|
||||
///
|
||||
/// As with [create_effect](crate::create_effect), the argument to the memo function is the previous value,
|
||||
/// As with [`create_effect`](crate::create_effect), the argument to the memo function is the previous value,
|
||||
/// i.e., the current value of the memo, which will be `None` for the initial calculation.
|
||||
///
|
||||
/// ```
|
||||
@@ -72,6 +72,7 @@ use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn create_memo<T>(
|
||||
cx: Scope,
|
||||
f: impl Fn(Option<&T>) -> T + 'static,
|
||||
@@ -79,7 +80,9 @@ pub fn create_memo<T>(
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
{
|
||||
cx.runtime.create_memo(f)
|
||||
let memo = cx.runtime.create_memo(f);
|
||||
cx.push_scope_property(ScopeProperty::Effect(memo.id));
|
||||
memo
|
||||
}
|
||||
|
||||
/// An efficient derived reactive value based on other reactive values.
|
||||
@@ -95,7 +98,7 @@ where
|
||||
/// create a derived signal. But if the derivation calculation is expensive, you should
|
||||
/// create a memo.
|
||||
///
|
||||
/// As with [create_effect](crate::create_effect), the argument to the memo function is the previous value,
|
||||
/// As with [`create_effect`](crate::create_effect), the argument to the memo function is the previous value,
|
||||
/// i.e., the current value of the memo, which will be `None` for the initial calculation.
|
||||
///
|
||||
/// ## Core Trait Implementations
|
||||
@@ -218,12 +221,9 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_get_untracked(&self) -> Option<T> {
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
self.id.try_with_no_subscription(runtime, T::clone).ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
self.try_with_untracked(T::clone)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
self.id.try_with_no_subscription(runtime, |v: &T| f(v)).ok()
|
||||
@@ -309,6 +310,7 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
fn get(&self) -> T {
|
||||
self.with(T::clone)
|
||||
}
|
||||
@@ -327,6 +329,7 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
fn try_get(&self) -> Option<T> {
|
||||
self.try_with(T::clone)
|
||||
}
|
||||
@@ -481,6 +484,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
fn format_memo_warning(
|
||||
msg: &str,
|
||||
@@ -503,6 +508,8 @@ fn format_memo_warning(
|
||||
format!("{msg}\n{defined_at_msg}warning happened here: {location}",)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
pub(crate) fn panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<'static>,
|
||||
|
||||
@@ -8,13 +8,22 @@ slotmap::new_key_type! {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ReactiveNode {
|
||||
pub value: Rc<RefCell<dyn Any>>,
|
||||
pub value: Option<Rc<RefCell<dyn Any>>>,
|
||||
pub state: ReactiveNodeState,
|
||||
pub node_type: ReactiveNodeType,
|
||||
}
|
||||
|
||||
impl ReactiveNode {
|
||||
pub fn value(&self) -> Rc<RefCell<dyn Any>> {
|
||||
self.value
|
||||
.clone()
|
||||
.expect("ReactiveNode.value to have a value")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ReactiveNodeType {
|
||||
Trigger,
|
||||
Signal,
|
||||
Memo { f: Rc<dyn AnyComputation> },
|
||||
Effect { f: Rc<dyn AnyComputation> },
|
||||
|
||||
@@ -5,8 +5,8 @@ use crate::{
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
serialization::Serializable,
|
||||
spawn::spawn_local,
|
||||
use_context, Memo, ReadSignal, Scope, ScopeProperty, SignalUpdate,
|
||||
SignalWith, SuspenseContext, WriteSignal,
|
||||
use_context, Memo, ReadSignal, Scope, ScopeProperty, SignalGetUntracked,
|
||||
SignalSet, SignalUpdate, SignalWith, SuspenseContext, WriteSignal,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -20,19 +20,19 @@ use std::{
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// Creates [Resource](crate::Resource), which is a signal that reflects the
|
||||
/// Creates a [`Resource`](crate::Resource), which is a signal that reflects the
|
||||
/// current state of an asynchronous task, allowing you to integrate `async`
|
||||
/// [Future]s into the synchronous reactive system.
|
||||
/// [`Future`]s into the synchronous reactive system.
|
||||
///
|
||||
/// Takes a `fetcher` function that generates a [Future] when called and a
|
||||
/// Takes a `fetcher` function that generates a [`Future`] when called and a
|
||||
/// `source` signal that provides the argument for the `fetcher`. Whenever the
|
||||
/// value of the `source` changes, a new [Future] will be created and run.
|
||||
/// value of the `source` changes, a new [`Future`] will be created and run.
|
||||
///
|
||||
/// When server-side rendering is used, the server will handle running the
|
||||
/// [Future] and will stream the result to the client. This process requires the
|
||||
/// output type of the Future to be [Serializable]. If your output cannot be
|
||||
/// serialized, or you just want to make sure the [Future] runs locally, use
|
||||
/// [create_local_resource()].
|
||||
/// [`Future`] and will stream the result to the client. This process requires the
|
||||
/// output type of the Future to be [`Serializable`]. If your output cannot be
|
||||
/// serialized, or you just want to make sure the [`Future`] runs locally, use
|
||||
/// [`create_local_resource()`].
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
@@ -79,14 +79,14 @@ where
|
||||
create_resource_with_initial_value(cx, source, fetcher, initial_value)
|
||||
}
|
||||
|
||||
/// Creates a [Resource](crate::Resource) with the given initial value, which
|
||||
/// will only generate and run a [Future] using the `fetcher` when the `source` changes.
|
||||
/// Creates a [`Resource`](crate::Resource) with the given initial value, which
|
||||
/// will only generate and run a [`Future`] using the `fetcher` when the `source` changes.
|
||||
///
|
||||
/// When server-side rendering is used, the server will handle running the
|
||||
/// [Future] and will stream the result to the client. This process requires the
|
||||
/// output type of the Future to be [Serializable]. If your output cannot be
|
||||
/// serialized, or you just want to make sure the [Future] runs locally, use
|
||||
/// [create_local_resource_with_initial_value()].
|
||||
/// [`Future`] and will stream the result to the client. This process requires the
|
||||
/// output type of the Future to be [`Serializable`]. If your output cannot be
|
||||
/// serialized, or you just want to make sure the [`Future`] runs locally, use
|
||||
/// [`create_local_resource_with_initial_value()`].
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -120,7 +120,7 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a “blocking” [Resource](crate::Resource). When server-side rendering is used,
|
||||
/// 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
|
||||
@@ -198,6 +198,7 @@ where
|
||||
fetcher,
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
preempted: Rc::new(Cell::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable,
|
||||
});
|
||||
@@ -216,7 +217,7 @@ where
|
||||
}
|
||||
});
|
||||
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Resource(id)));
|
||||
cx.push_scope_property(ScopeProperty::Resource(id));
|
||||
|
||||
Resource {
|
||||
runtime: cx.runtime,
|
||||
@@ -228,16 +229,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a _local_ [Resource](crate::Resource), which is a signal that
|
||||
/// Creates a _local_ [`Resource`](crate::Resource), which is a signal that
|
||||
/// reflects the current state of an asynchronous task, allowing you to
|
||||
/// integrate `async` [Future]s into the synchronous reactive system.
|
||||
/// integrate `async` [`Future`]s into the synchronous reactive system.
|
||||
///
|
||||
/// Takes a `fetcher` function that generates a [Future] when called and a
|
||||
/// Takes a `fetcher` function that generates a [`Future`] when called and a
|
||||
/// `source` signal that provides the argument for the `fetcher`. Whenever the
|
||||
/// value of the `source` changes, a new [Future] will be created and run.
|
||||
/// value of the `source` changes, a new [`Future`] will be created and run.
|
||||
///
|
||||
/// Unlike [create_resource()], this [Future] is always run on the local system
|
||||
/// and therefore it's result type does not need to be [Serializable].
|
||||
/// Unlike [`create_resource()`], this [`Future`] is always run on the local system
|
||||
/// and therefore it's result type does not need to be [`Serializable`].
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
@@ -273,13 +274,13 @@ where
|
||||
create_local_resource_with_initial_value(cx, source, fetcher, initial_value)
|
||||
}
|
||||
|
||||
/// Creates a _local_ [Resource](crate::Resource) with the given initial value,
|
||||
/// which will only generate and run a [Future] using the `fetcher` when the
|
||||
/// Creates a _local_ [`Resource`](crate::Resource) with the given initial value,
|
||||
/// which will only generate and run a [`Future`] using the `fetcher` when the
|
||||
/// `source` changes.
|
||||
///
|
||||
/// Unlike [create_resource_with_initial_value()], this [Future] will always run
|
||||
/// Unlike [`create_resource_with_initial_value()`], this [`Future`] will always run
|
||||
/// on the local system and therefore its output type does not need to be
|
||||
/// [Serializable].
|
||||
/// [`Serializable`].
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -322,6 +323,7 @@ where
|
||||
fetcher,
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
preempted: Rc::new(Cell::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable: ResourceSerialization::Local,
|
||||
});
|
||||
@@ -339,7 +341,7 @@ where
|
||||
move |_| r.load(false)
|
||||
});
|
||||
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Resource(id)));
|
||||
cx.push_scope_property(ScopeProperty::Resource(id));
|
||||
|
||||
Resource {
|
||||
runtime: cx.runtime,
|
||||
@@ -443,7 +445,7 @@ where
|
||||
/// resource is still pending). Also subscribes the running effect to this
|
||||
/// resource.
|
||||
///
|
||||
/// If you want to get the value without cloning it, use [Resource::with].
|
||||
/// If you want to get the value without cloning it, use [`Resource::with`].
|
||||
/// (`value.read(cx)` is equivalent to `value.with(cx, T::clone)`.)
|
||||
#[track_caller]
|
||||
pub fn read(&self, cx: Scope) -> Option<T>
|
||||
@@ -463,10 +465,10 @@ where
|
||||
/// Applies a function to the current value of the resource, and subscribes
|
||||
/// the running effect to this resource. If the resource hasn't yet
|
||||
/// resolved, the function won't be called and this will return
|
||||
/// [Option::None].
|
||||
/// [`Option::None`].
|
||||
///
|
||||
/// If you want to get the value by cloning it, you can use
|
||||
/// [Resource::read].
|
||||
/// [`Resource::read`].
|
||||
#[track_caller]
|
||||
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
let location = std::panic::Location::caller();
|
||||
@@ -501,8 +503,8 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns a [std::future::Future] that will resolve when the resource has loaded,
|
||||
/// yield its [ResourceId] and a JSON string.
|
||||
/// Returns a [`Future`] that will resolve when the resource has loaded,
|
||||
/// yield its [`ResourceId`] and a JSON string.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub async fn to_serialization_resolver(
|
||||
&self,
|
||||
@@ -524,19 +526,114 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> SignalUpdate<Option<T>> for Resource<S, T> {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Resource::update()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update(&self, f: impl FnOnce(&mut Option<T>)) {
|
||||
self.try_update(f);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Resource::try_update()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_update<O>(&self, f: impl FnOnce(&mut Option<T>) -> O) -> Option<O> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
if resource.loading.get_untracked() {
|
||||
resource.preempted.set(true);
|
||||
for suspense_context in
|
||||
resource.suspense_contexts.borrow().iter()
|
||||
{
|
||||
suspense_context.decrement(
|
||||
resource.serializable
|
||||
!= ResourceSerialization::Local,
|
||||
);
|
||||
}
|
||||
}
|
||||
resource.set_loading.set(false);
|
||||
resource.set_value.try_update(f)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> SignalSet<T> for Resource<S, T> {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Resource::set()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn set(&self, new_value: T) {
|
||||
self.try_set(new_value);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Resource::try_set()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_set(&self, new_value: T) -> Option<T> {
|
||||
self.update(|n| *n = Some(new_value));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A signal that reflects the
|
||||
/// current state of an asynchronous task, allowing you to integrate `async`
|
||||
/// [Future]s into the synchronous reactive system.
|
||||
/// [`Future`]s into the synchronous reactive system.
|
||||
///
|
||||
/// Takes a `fetcher` function that generates a [Future] when called and a
|
||||
/// Takes a `fetcher` function that generates a [`Future`] when called and a
|
||||
/// `source` signal that provides the argument for the `fetcher`. Whenever the
|
||||
/// value of the `source` changes, a new [Future] will be created and run.
|
||||
/// value of the `source` changes, a new [`Future`] will be created and run.
|
||||
///
|
||||
/// When server-side rendering is used, the server will handle running the
|
||||
/// [Future] and will stream the result to the client. This process requires the
|
||||
/// output type of the Future to be [Serializable]. If your output cannot be
|
||||
/// serialized, or you just want to make sure the [Future] runs locally, use
|
||||
/// [create_local_resource()].
|
||||
/// [`Future`] and will stream the result to the client. This process requires the
|
||||
/// output type of the Future to be [`Serializable`]. If your output cannot be
|
||||
/// serialized, or you just want to make sure the [`Future`] runs locally, use
|
||||
/// [`create_local_resource()`].
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
@@ -583,7 +680,7 @@ where
|
||||
|
||||
// Resources
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a [Resource](crate::Resource).
|
||||
/// Unique ID assigned to a [`Resource`](crate::Resource).
|
||||
pub struct ResourceId;
|
||||
}
|
||||
|
||||
@@ -626,6 +723,7 @@ where
|
||||
fetcher: Rc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
|
||||
resolved: Rc<Cell<bool>>,
|
||||
scheduled: Rc<Cell<bool>>,
|
||||
preempted: Rc<Cell<bool>>,
|
||||
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
|
||||
serializable: ResourceSerialization,
|
||||
}
|
||||
@@ -738,6 +836,7 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
self.preempted.set(false);
|
||||
self.scheduled.set(false);
|
||||
|
||||
_ = self.source.try_with(|source| {
|
||||
@@ -772,19 +871,27 @@ where
|
||||
let resolved = self.resolved.clone();
|
||||
let set_value = self.set_value;
|
||||
let set_loading = self.set_loading;
|
||||
let preempted = self.preempted.clone();
|
||||
async move {
|
||||
let res = fut.await;
|
||||
|
||||
resolved.set(true);
|
||||
|
||||
set_value.update(|n| *n = Some(res));
|
||||
if !preempted.get() {
|
||||
set_value.update(|n| *n = Some(res));
|
||||
}
|
||||
|
||||
set_loading.update(|n| *n = false);
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
suspense_context.decrement(
|
||||
serializable != ResourceSerialization::Local,
|
||||
);
|
||||
if !preempted.get() {
|
||||
for suspense_context in
|
||||
suspense_contexts.borrow().iter()
|
||||
{
|
||||
suspense_context.decrement(
|
||||
serializable != ResourceSerialization::Local,
|
||||
);
|
||||
}
|
||||
}
|
||||
preempted.set(false);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::{
|
||||
node::{NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType},
|
||||
AnyComputation, AnyResource, Effect, Memo, MemoState, ReadSignal,
|
||||
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
|
||||
ScopeProperty, SerializableResource, StoredValueId, UnserializableResource,
|
||||
WriteSignal,
|
||||
ScopeProperty, SerializableResource, StoredValueId, Trigger,
|
||||
UnserializableResource, WriteSignal,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use core::hash::BuildHasherDefault;
|
||||
@@ -117,15 +117,16 @@ impl Runtime {
|
||||
// memos and effects rerun
|
||||
// signals simply have their value
|
||||
let changed = match node.node_type {
|
||||
ReactiveNodeType::Signal => true,
|
||||
ReactiveNodeType::Memo { f }
|
||||
| ReactiveNodeType::Effect { f } => {
|
||||
ReactiveNodeType::Signal | ReactiveNodeType::Trigger => true,
|
||||
ReactiveNodeType::Memo { ref f }
|
||||
| ReactiveNodeType::Effect { ref f } => {
|
||||
let value = node.value();
|
||||
// set this node as the observer
|
||||
self.with_observer(node_id, move || {
|
||||
// clean up sources of this memo/effect
|
||||
self.cleanup(node_id);
|
||||
|
||||
f.run(Rc::clone(&node.value))
|
||||
f.run(value)
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -191,12 +192,17 @@ impl Runtime {
|
||||
pub(crate) fn mark_dirty(&self, node: NodeId) {
|
||||
//crate::macros::debug_warn!("marking {node:?} dirty");
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
let mut pending_effects = self.pending_effects.borrow_mut();
|
||||
let subscribers = self.node_subscribers.borrow();
|
||||
let current_observer = self.observer.get();
|
||||
|
||||
// mark self dirty
|
||||
if let Some(current_node) = nodes.get_mut(node) {
|
||||
if current_node.state == ReactiveNodeState::DirtyMarked {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut pending_effects = self.pending_effects.borrow_mut();
|
||||
let subscribers = self.node_subscribers.borrow();
|
||||
let current_observer = self.observer.get();
|
||||
|
||||
// mark self dirty
|
||||
Runtime::mark(
|
||||
node,
|
||||
current_node,
|
||||
@@ -246,11 +252,11 @@ impl Runtime {
|
||||
|
||||
while let Some(iter) = stack.last_mut() {
|
||||
let res = iter.with_iter_mut(|iter| {
|
||||
let Some(&child) = iter.next() else {
|
||||
let Some(mut child) = iter.next().copied() else {
|
||||
return IterResult::Empty;
|
||||
};
|
||||
|
||||
if let Some(node) = nodes.get_mut(child) {
|
||||
while let Some(node) = nodes.get_mut(child) {
|
||||
if node.state == ReactiveNodeState::Check
|
||||
|| node.state == ReactiveNodeState::DirtyMarked
|
||||
{
|
||||
@@ -266,11 +272,23 @@ impl Runtime {
|
||||
);
|
||||
|
||||
if let Some(children) = subscribers.get(child) {
|
||||
return IterResult::NewIter(RefIter::new(
|
||||
children.borrow(),
|
||||
|children| children.iter(),
|
||||
));
|
||||
let children = children.borrow();
|
||||
|
||||
if !children.is_empty() {
|
||||
// avoid going through an iterator in the simple psuedo-recursive case
|
||||
if children.len() == 1 {
|
||||
child = children[0];
|
||||
continue;
|
||||
}
|
||||
|
||||
return IterResult::NewIter(RefIter::new(
|
||||
children,
|
||||
|children| children.iter(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
IterResult::Continue
|
||||
@@ -313,16 +331,12 @@ impl Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_effects(runtime_id: RuntimeId) {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
runtime.run_your_effects();
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn run_your_effects(&self) {
|
||||
let effects = self.pending_effects.take();
|
||||
for effect_id in effects {
|
||||
self.update_if_necessary(effect_id);
|
||||
pub(crate) fn run_effects(&self) {
|
||||
if !self.batching.get() {
|
||||
let effects = self.pending_effects.take();
|
||||
for effect_id in effects {
|
||||
self.update_if_necessary(effect_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +360,7 @@ impl Debug for Runtime {
|
||||
}
|
||||
/// Get the selected runtime from the thread-local set of runtimes. On the server,
|
||||
/// this will return the correct runtime. In the browser, there should only be one runtime.
|
||||
#[inline(always)] // it monomorphizes anyway
|
||||
pub(crate) fn with_runtime<T>(
|
||||
id: RuntimeId,
|
||||
f: impl FnOnce(&Runtime) -> T,
|
||||
@@ -369,7 +384,7 @@ pub(crate) fn with_runtime<T>(
|
||||
|
||||
#[doc(hidden)]
|
||||
#[must_use = "Runtime will leak memory if Runtime::dispose() is never called."]
|
||||
/// Creates a new reactive [Runtime]. This should almost always be handled by the framework.
|
||||
/// Creates a new reactive [`Runtime`]. This should almost always be handled by the framework.
|
||||
pub fn create_runtime() -> RuntimeId {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
@@ -382,17 +397,17 @@ pub fn create_runtime() -> RuntimeId {
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a [Runtime](crate::Runtime).
|
||||
/// Unique ID assigned to a Runtime.
|
||||
pub struct RuntimeId;
|
||||
}
|
||||
|
||||
/// Unique ID assigned to a [Runtime](crate::Runtime).
|
||||
/// Unique ID assigned to a Runtime.
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct RuntimeId;
|
||||
|
||||
impl RuntimeId {
|
||||
/// Removes the runtime, disposing all its child [Scope](crate::Scope)s.
|
||||
/// Removes the runtime, disposing all its child [`Scope`](crate::Scope)s.
|
||||
pub fn dispose(self) {
|
||||
cfg_if! {
|
||||
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
|
||||
@@ -406,7 +421,7 @@ impl RuntimeId {
|
||||
with_runtime(self, |runtime| {
|
||||
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
|
||||
let scope = Scope { runtime: self, id };
|
||||
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
|
||||
let disposer = ScopeDisposer(scope);
|
||||
(scope, disposer)
|
||||
})
|
||||
.expect(
|
||||
@@ -415,24 +430,34 @@ impl RuntimeId {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope_undisposed<T>(
|
||||
pub(crate) fn raw_scope_and_disposer_with_parent(
|
||||
self,
|
||||
f: impl FnOnce(Scope) -> T,
|
||||
parent: Option<Scope>,
|
||||
) -> (T, ScopeId, ScopeDisposer) {
|
||||
) -> (Scope, ScopeDisposer) {
|
||||
with_runtime(self, |runtime| {
|
||||
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
|
||||
if let Some(parent) = parent {
|
||||
runtime.scope_parents.borrow_mut().insert(id, parent.id);
|
||||
}
|
||||
let scope = Scope { runtime: self, id };
|
||||
let val = f(scope);
|
||||
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
|
||||
(val, id, disposer)
|
||||
let disposer = ScopeDisposer(scope);
|
||||
(scope, disposer)
|
||||
})
|
||||
.expect("tried to run scope in a runtime that has been disposed")
|
||||
.expect("tried to crate scope in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn run_scope_undisposed<T>(
|
||||
self,
|
||||
f: impl FnOnce(Scope) -> T,
|
||||
parent: Option<Scope>,
|
||||
) -> (T, ScopeId, ScopeDisposer) {
|
||||
let (scope, disposer) = self.raw_scope_and_disposer_with_parent(parent);
|
||||
|
||||
(f(scope), scope.id, disposer)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn run_scope<T>(
|
||||
self,
|
||||
f: impl FnOnce(Scope) -> T,
|
||||
@@ -444,13 +469,34 @@ impl RuntimeId {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)] // only because it's placed here to fit in with the other create methods
|
||||
pub(crate) fn create_trigger(self) -> Trigger {
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value: None,
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Trigger,
|
||||
})
|
||||
})
|
||||
.expect(
|
||||
"tried to create a trigger in a runtime that has been disposed",
|
||||
);
|
||||
|
||||
Trigger {
|
||||
id,
|
||||
runtime: self,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_concrete_signal(
|
||||
self,
|
||||
value: Rc<RefCell<dyn Any>>,
|
||||
) -> NodeId {
|
||||
with_runtime(self, |runtime| {
|
||||
runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value,
|
||||
value: Some(value),
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Signal,
|
||||
})
|
||||
@@ -459,6 +505,7 @@ impl RuntimeId {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub(crate) fn create_signal<T>(
|
||||
self,
|
||||
value: T,
|
||||
@@ -514,7 +561,7 @@ impl RuntimeId {
|
||||
values
|
||||
.map(|value| {
|
||||
signals.insert(ReactiveNode {
|
||||
value: Rc::new(RefCell::new(value)),
|
||||
value: Some(Rc::new(RefCell::new(value))),
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Signal,
|
||||
})
|
||||
@@ -545,6 +592,7 @@ impl RuntimeId {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub(crate) fn create_rw_signal<T>(self, value: T) -> RwSignal<T>
|
||||
where
|
||||
T: Any + 'static,
|
||||
@@ -565,7 +613,6 @@ impl RuntimeId {
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_concrete_effect(
|
||||
self,
|
||||
value: Rc<RefCell<dyn Any>>,
|
||||
@@ -573,7 +620,7 @@ impl RuntimeId {
|
||||
) -> NodeId {
|
||||
with_runtime(self, |runtime| {
|
||||
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value: Rc::clone(&value),
|
||||
value: Some(Rc::clone(&value)),
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Effect {
|
||||
f: Rc::clone(&effect),
|
||||
@@ -593,7 +640,25 @@ impl RuntimeId {
|
||||
.expect("tried to create an effect in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
pub(crate) fn create_concrete_memo(
|
||||
self,
|
||||
value: Rc<RefCell<dyn Any>>,
|
||||
computation: Rc<dyn AnyComputation>,
|
||||
) -> NodeId {
|
||||
with_runtime(self, |runtime| {
|
||||
runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value: Some(value),
|
||||
// memos are lazy, so are dirty when created
|
||||
// will be run the first time we ask for it
|
||||
state: ReactiveNodeState::Dirty,
|
||||
node_type: ReactiveNodeType::Memo { f: computation },
|
||||
})
|
||||
})
|
||||
.expect("tried to create a memo in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub(crate) fn create_effect<T>(
|
||||
self,
|
||||
f: impl Fn(Option<T>) -> T + 'static,
|
||||
@@ -601,21 +666,19 @@ impl RuntimeId {
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
let defined_at = std::panic::Location::caller();
|
||||
|
||||
let effect = Effect {
|
||||
f,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
};
|
||||
|
||||
let value = Rc::new(RefCell::new(None::<T>));
|
||||
self.create_concrete_effect(value, Rc::new(effect))
|
||||
self.create_concrete_effect(
|
||||
Rc::new(RefCell::new(None::<T>)),
|
||||
Rc::new(Effect {
|
||||
f,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub(crate) fn create_memo<T>(
|
||||
self,
|
||||
f: impl Fn(Option<&T>) -> T + 'static,
|
||||
@@ -623,33 +686,20 @@ impl RuntimeId {
|
||||
where
|
||||
T: PartialEq + Any + 'static,
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
let defined_at = std::panic::Location::caller();
|
||||
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value: Rc::new(RefCell::new(None::<T>)),
|
||||
// memos are lazy, so are dirty when created
|
||||
// will be run the first time we ask for it
|
||||
state: ReactiveNodeState::Dirty,
|
||||
node_type: ReactiveNodeType::Memo {
|
||||
f: Rc::new(MemoState {
|
||||
f,
|
||||
t: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
.expect("tried to create a memo in a runtime that has been disposed");
|
||||
|
||||
Memo {
|
||||
runtime: self,
|
||||
id,
|
||||
id: self.create_concrete_memo(
|
||||
Rc::new(RefCell::new(None::<T>)),
|
||||
Rc::new(MemoState {
|
||||
f,
|
||||
t: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}),
|
||||
),
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -746,6 +796,15 @@ impl Runtime {
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
/// Do not call on triggers
|
||||
pub(crate) fn get_value(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Option<Rc<RefCell<dyn Any>>> {
|
||||
let signals = self.nodes.borrow();
|
||||
signals.get(node_id).map(|node| node.value())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Runtime {
|
||||
|
||||
@@ -82,7 +82,7 @@ pub fn run_scope_undisposed<T>(
|
||||
/// when it is removed from the list.
|
||||
///
|
||||
/// Every other function in this crate takes a `Scope` as its first argument. Since `Scope`
|
||||
/// is [Copy] and `'static` this does not add much overhead or lifetime complexity.
|
||||
/// is [`Copy`] and `'static` this does not add much overhead or lifetime complexity.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Scope {
|
||||
#[doc(hidden)]
|
||||
@@ -116,6 +116,7 @@ impl Scope {
|
||||
/// This is useful for applications like a list or a router, which may want to create child scopes and
|
||||
/// dispose of them when they are no longer needed (e.g., a list item has been destroyed or the user
|
||||
/// has navigated away from the route.)
|
||||
#[inline(always)]
|
||||
pub fn child_scope(self, f: impl FnOnce(Scope)) -> ScopeDisposer {
|
||||
let (_, disposer) = self.run_child_scope(f);
|
||||
disposer
|
||||
@@ -130,12 +131,20 @@ impl Scope {
|
||||
/// This is useful for applications like a list or a router, which may want to create child scopes and
|
||||
/// dispose of them when they are no longer needed (e.g., a list item has been destroyed or the user
|
||||
/// has navigated away from the route.)
|
||||
#[inline(always)]
|
||||
pub fn run_child_scope<T>(
|
||||
self,
|
||||
f: impl FnOnce(Scope) -> T,
|
||||
) -> (T, ScopeDisposer) {
|
||||
let (res, child_id, disposer) =
|
||||
self.runtime.run_scope_undisposed(f, Some(self));
|
||||
|
||||
self.push_child(child_id);
|
||||
|
||||
(res, disposer)
|
||||
}
|
||||
|
||||
fn push_child(&self, child_id: ScopeId) {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let mut children = runtime.scope_children.borrow_mut();
|
||||
children
|
||||
@@ -147,7 +156,6 @@ impl Scope {
|
||||
.or_default()
|
||||
.push(child_id);
|
||||
});
|
||||
(res, disposer)
|
||||
}
|
||||
|
||||
/// Suspends reactive tracking while running the given function.
|
||||
@@ -175,13 +183,23 @@ impl Scope {
|
||||
///
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let untracked_result;
|
||||
|
||||
SpecialNonReactiveZone::enter();
|
||||
let prev_observer = runtime.observer.take();
|
||||
let untracked_result = f();
|
||||
runtime.observer.set(prev_observer);
|
||||
|
||||
let prev_observer =
|
||||
SetObserverOnDrop(self.runtime, runtime.observer.take());
|
||||
|
||||
untracked_result = f();
|
||||
|
||||
runtime.observer.set(prev_observer.1);
|
||||
std::mem::forget(prev_observer); // avoid Drop
|
||||
|
||||
SpecialNonReactiveZone::exit();
|
||||
|
||||
untracked_result
|
||||
})
|
||||
.expect(
|
||||
@@ -191,6 +209,16 @@ impl Scope {
|
||||
}
|
||||
}
|
||||
|
||||
struct SetObserverOnDrop(RuntimeId, Option<NodeId>);
|
||||
|
||||
impl Drop for SetObserverOnDrop {
|
||||
fn drop(&mut self) {
|
||||
_ = with_runtime(self.0, |rt| {
|
||||
rt.observer.set(self.1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Internals
|
||||
|
||||
impl Scope {
|
||||
@@ -198,7 +226,7 @@ impl Scope {
|
||||
///
|
||||
/// This will
|
||||
/// 1. dispose of all child `Scope`s
|
||||
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
|
||||
/// 2. run all cleanup functions defined for this scope by [`on_cleanup`](crate::on_cleanup).
|
||||
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
|
||||
pub fn dispose(self) {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
@@ -226,6 +254,8 @@ impl Scope {
|
||||
}
|
||||
}
|
||||
|
||||
runtime.scope_parents.borrow_mut().remove(self.id);
|
||||
|
||||
// remove everything we own and run cleanups
|
||||
let owned = {
|
||||
let owned = runtime.scopes.borrow_mut().remove(self.id);
|
||||
@@ -234,7 +264,8 @@ impl Scope {
|
||||
if let Some(owned) = owned {
|
||||
for property in owned {
|
||||
match property {
|
||||
ScopeProperty::Signal(id) => {
|
||||
ScopeProperty::Signal(id)
|
||||
| ScopeProperty::Trigger(id) => {
|
||||
// remove the signal
|
||||
runtime.nodes.borrow_mut().remove(id);
|
||||
let subs = runtime
|
||||
@@ -271,14 +302,11 @@ impl Scope {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn with_scope_property(
|
||||
&self,
|
||||
f: impl FnOnce(&mut Vec<ScopeProperty>),
|
||||
) {
|
||||
pub(crate) fn push_scope_property(&self, prop: ScopeProperty) {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let scopes = runtime.scopes.borrow();
|
||||
if let Some(scope) = scopes.get(self.id) {
|
||||
f(&mut scope.borrow_mut());
|
||||
scope.borrow_mut().push(prop);
|
||||
} else {
|
||||
console_warn(
|
||||
"tried to add property to a scope that has been disposed",
|
||||
@@ -289,82 +317,90 @@ impl Scope {
|
||||
|
||||
/// Returns the the parent Scope, if any.
|
||||
pub fn parent(&self) -> Option<Scope> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
match with_runtime(self.runtime, |runtime| {
|
||||
runtime.scope_parents.borrow().get(self.id).copied()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|id| Scope {
|
||||
runtime: self.runtime,
|
||||
id,
|
||||
})
|
||||
}) {
|
||||
Ok(Some(id)) => Some(Scope {
|
||||
runtime: self.runtime,
|
||||
id,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a cleanup function, which will be run when a [Scope] is disposed.
|
||||
///
|
||||
/// It runs after child scopes have been disposed, but before signals, effects, and resources
|
||||
/// are invalidated.
|
||||
pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
|
||||
fn push_cleanup(cx: Scope, cleanup_fn: Box<dyn FnOnce()>) {
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut cleanups = runtime.scope_cleanups.borrow_mut();
|
||||
let cleanups = cleanups
|
||||
.entry(cx.id)
|
||||
.expect("trying to clean up a Scope that has already been disposed")
|
||||
.or_insert_with(Default::default);
|
||||
cleanups.push(Box::new(cleanup_fn));
|
||||
})
|
||||
cleanups.push(cleanup_fn);
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates a cleanup function, which will be run when a [`Scope`] is disposed.
|
||||
///
|
||||
/// It runs after child scopes have been disposed, but before signals, effects, and resources
|
||||
/// are invalidated.
|
||||
#[inline(always)]
|
||||
pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
|
||||
push_cleanup(cx, Box::new(cleanup_fn))
|
||||
}
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a [Scope](crate::Scope).
|
||||
/// Unique ID assigned to a [`Scope`](crate::Scope).
|
||||
pub struct ScopeId;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ScopeProperty {
|
||||
Trigger(NodeId),
|
||||
Signal(NodeId),
|
||||
Effect(NodeId),
|
||||
Resource(ResourceId),
|
||||
StoredValue(StoredValueId),
|
||||
}
|
||||
|
||||
/// Creating a [Scope](crate::Scope) gives you a disposer, which can be called
|
||||
/// Creating a [`Scope`](crate::Scope) gives you a disposer, which can be called
|
||||
/// to dispose of that reactive scope.
|
||||
///
|
||||
/// This will
|
||||
/// 1. dispose of all child `Scope`s
|
||||
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
|
||||
/// 2. run all cleanup functions defined for this scope by [`on_cleanup`](crate::on_cleanup).
|
||||
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
|
||||
pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>);
|
||||
#[repr(transparent)]
|
||||
pub struct ScopeDisposer(pub(crate) Scope);
|
||||
|
||||
impl ScopeDisposer {
|
||||
/// Disposes of a reactive [Scope](crate::Scope).
|
||||
/// Disposes of a reactive [`Scope`](crate::Scope).
|
||||
///
|
||||
/// This will
|
||||
/// 1. dispose of all child `Scope`s
|
||||
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
|
||||
/// 2. run all cleanup functions defined for this scope by [`on_cleanup`](crate::on_cleanup).
|
||||
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
|
||||
#[inline(always)]
|
||||
pub fn dispose(self) {
|
||||
(self.0)()
|
||||
self.0.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
|
||||
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope.
|
||||
pub fn all_resources(&self) -> Vec<ResourceId> {
|
||||
with_runtime(self.runtime, |runtime| runtime.all_resources())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope that are
|
||||
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope that are
|
||||
/// pending from the server.
|
||||
pub fn pending_resources(&self) -> Vec<ResourceId> {
|
||||
with_runtime(self.runtime, |runtime| runtime.pending_resources())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
|
||||
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope.
|
||||
pub fn serialization_resolvers(
|
||||
&self,
|
||||
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
@@ -374,7 +410,7 @@ impl Scope {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
|
||||
/// Registers the given [`SuspenseContext`](crate::SuspenseContext) with the current scope,
|
||||
/// calling the `resolver` when its resources are all resolved.
|
||||
pub fn register_suspense(
|
||||
&self,
|
||||
@@ -476,12 +512,19 @@ impl Scope {
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the runtime this scope belongs to has already been disposed.
|
||||
#[inline(always)]
|
||||
pub fn batch<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
let batching =
|
||||
SetBatchingOnDrop(self.runtime, runtime.batching.get());
|
||||
runtime.batching.set(true);
|
||||
|
||||
let val = f();
|
||||
runtime.batching.set(false);
|
||||
runtime.run_your_effects();
|
||||
|
||||
runtime.batching.set(batching.1);
|
||||
std::mem::forget(batching);
|
||||
|
||||
runtime.run_effects();
|
||||
val
|
||||
})
|
||||
.expect(
|
||||
@@ -490,6 +533,16 @@ impl Scope {
|
||||
}
|
||||
}
|
||||
|
||||
struct SetBatchingOnDrop(RuntimeId, bool);
|
||||
|
||||
impl Drop for SetBatchingOnDrop {
|
||||
fn drop(&mut self) {
|
||||
_ = with_runtime(self.0, |rt| {
|
||||
rt.batching.set(self.1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ScopeDisposer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("ScopeDisposer").finish()
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
|
||||
/// Creates a conditional signal that only notifies subscribers when a change
|
||||
/// in the source signal’s value changes whether it is equal to the key value
|
||||
/// (as determined by [PartialEq].)
|
||||
/// (as determined by [`PartialEq`].)
|
||||
///
|
||||
/// **You probably don’t need this,** but it can be a very useful optimization
|
||||
/// in certain situations (e.g., “set the class `selected` if `selected() == this_row_index`)
|
||||
@@ -46,6 +46,7 @@ use std::{
|
||||
/// # })
|
||||
/// # .dispose()
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn create_selector<T>(
|
||||
cx: Scope,
|
||||
source: impl Fn() -> T + Clone + 'static,
|
||||
@@ -53,7 +54,7 @@ pub fn create_selector<T>(
|
||||
where
|
||||
T: PartialEq + Eq + Debug + Clone + Hash + 'static,
|
||||
{
|
||||
create_selector_with_fn(cx, source, |a, b| a == b)
|
||||
create_selector_with_fn(cx, source, PartialEq::eq)
|
||||
}
|
||||
|
||||
/// Creates a conditional signal that only notifies subscribers when a change
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::rc::Rc;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Describes errors that can occur while serializing and deserializing data,
|
||||
/// typically during the process of streaming [Resource](crate::Resource)s from
|
||||
/// typically during the process of streaming [`Resource`](crate::Resource)s from
|
||||
/// the server to the client.
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum SerializationError {
|
||||
@@ -19,7 +19,7 @@ pub enum SerializationError {
|
||||
/// Describes an object that can be serialized to or from a supported format
|
||||
/// Currently those are JSON and Cbor
|
||||
///
|
||||
/// This is primarily used for serializing and deserializing [Resource](crate::Resource)s
|
||||
/// This is primarily used for serializing and deserializing [`Resource`](crate::Resource)s
|
||||
/// so they can begin on the server and be resolved on the client, but can be used
|
||||
/// for any data that needs to be serialized/deserialized.
|
||||
///
|
||||
|
||||
@@ -8,9 +8,10 @@ use crate::{
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
Runtime, Scope, ScopeProperty,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::Stream;
|
||||
use std::{fmt::Debug, marker::PhantomData, pin::Pin, rc::Rc};
|
||||
use std::{
|
||||
any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, pin::Pin, rc::Rc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
macro_rules! impl_get_fn_traits {
|
||||
@@ -20,6 +21,7 @@ macro_rules! impl_get_fn_traits {
|
||||
impl<T: Clone> FnOnce<()> for $ty<T> {
|
||||
type Output = T;
|
||||
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||
impl_get_fn_traits!(@method_name self $($method_name)?)
|
||||
}
|
||||
@@ -27,6 +29,7 @@ macro_rules! impl_get_fn_traits {
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T: Clone> FnMut<()> for $ty<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
impl_get_fn_traits!(@method_name self $($method_name)?)
|
||||
}
|
||||
@@ -34,6 +37,7 @@ macro_rules! impl_get_fn_traits {
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T: Clone> Fn<()> for $ty<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
impl_get_fn_traits!(@method_name self $($method_name)?)
|
||||
}
|
||||
@@ -55,6 +59,7 @@ macro_rules! impl_set_fn_traits {
|
||||
impl<T> FnOnce<(T,)> for $ty<T> {
|
||||
type Output = ();
|
||||
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_once(self, args: (T,)) -> Self::Output {
|
||||
impl_set_fn_traits!(@method_name self $($method_name)? args)
|
||||
}
|
||||
@@ -62,6 +67,7 @@ macro_rules! impl_set_fn_traits {
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnMut<(T,)> for $ty<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_mut(&mut self, args: (T,)) -> Self::Output {
|
||||
impl_set_fn_traits!(@method_name self $($method_name)? args)
|
||||
}
|
||||
@@ -69,6 +75,7 @@ macro_rules! impl_set_fn_traits {
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> Fn<(T,)> for $ty<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call(&self, args: (T,)) -> Self::Output {
|
||||
impl_set_fn_traits!(@method_name self $($method_name)? args)
|
||||
}
|
||||
@@ -102,7 +109,7 @@ pub trait SignalGet<T> {
|
||||
/// the running effect to this signal.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
|
||||
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
|
||||
#[track_caller]
|
||||
fn get(&self) -> T;
|
||||
|
||||
@@ -118,7 +125,7 @@ pub trait SignalWith<T> {
|
||||
/// the running effect to this signal.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
|
||||
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
|
||||
#[track_caller]
|
||||
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O;
|
||||
|
||||
@@ -146,7 +153,7 @@ pub trait SignalSet<T> {
|
||||
/// if the signal is still valid, [`Some(T)`] otherwise.
|
||||
///
|
||||
/// **Note:** `set()` does not auto-memoize, i.e., it will notify subscribers
|
||||
/// even if the value has not actually changed.
|
||||
/// even if the value has not actually changed.
|
||||
fn try_set(&self, new_value: T) -> Option<T>;
|
||||
}
|
||||
|
||||
@@ -190,7 +197,7 @@ pub trait SignalGetUntracked<T> {
|
||||
/// current scope.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
|
||||
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
|
||||
#[track_caller]
|
||||
fn get_untracked(&self) -> T;
|
||||
|
||||
@@ -207,7 +214,7 @@ pub trait SignalWithUntracked<T> {
|
||||
/// value without creating a dependency on the current scope.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
|
||||
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
|
||||
#[track_caller]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O;
|
||||
|
||||
@@ -247,6 +254,7 @@ pub trait SignalUpdateUntracked<T> {
|
||||
/// the value the closure returned.
|
||||
#[deprecated = "Please use `try_update_untracked` instead. This method \
|
||||
will be removed in a future version of `leptos`"]
|
||||
#[inline(always)]
|
||||
fn update_returning_untracked<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
@@ -267,7 +275,7 @@ pub trait SignalStream<T> {
|
||||
/// whenever it changes.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
|
||||
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
|
||||
// We're returning an opaque type until impl trait in trait
|
||||
// positions are stabilized, and also so any underlying
|
||||
// changes are non-breaking
|
||||
@@ -275,7 +283,7 @@ pub trait SignalStream<T> {
|
||||
fn to_stream(&self, cx: Scope) -> Pin<Box<dyn Stream<Item = T>>>;
|
||||
}
|
||||
|
||||
/// This trait allows disposing a signal before its [Scope] has been disposed.
|
||||
/// This trait allows disposing a signal before its [`Scope`] has been disposed.
|
||||
pub trait SignalDispose {
|
||||
/// Disposes of the signal. This:
|
||||
/// 1. Detaches the signal from the reactive graph, preventing it from triggering
|
||||
@@ -291,8 +299,8 @@ pub trait SignalDispose {
|
||||
/// and notifies other code when it has changed. This is the
|
||||
/// core primitive of Leptos’s reactive system.
|
||||
///
|
||||
/// Takes a reactive [Scope] and the initial value as arguments,
|
||||
/// and returns a tuple containing a [ReadSignal] and a [WriteSignal],
|
||||
/// Takes a reactive [`Scope`] and the initial value as arguments,
|
||||
/// and returns a tuple containing a [`ReadSignal`] and a [`WriteSignal`],
|
||||
/// each of which can be called as a function.
|
||||
///
|
||||
/// ```
|
||||
@@ -340,11 +348,11 @@ pub fn create_signal<T>(
|
||||
value: T,
|
||||
) -> (ReadSignal<T>, WriteSignal<T>) {
|
||||
let s = cx.runtime.create_signal(value);
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Signal(s.0.id)));
|
||||
cx.push_scope_property(ScopeProperty::Signal(s.0.id));
|
||||
s
|
||||
}
|
||||
|
||||
/// Works exactly as [create_signal], but creates multiple signals at once.
|
||||
/// Works exactly as [`create_signal`], but creates multiple signals at once.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -364,7 +372,7 @@ pub fn create_many_signals<T>(
|
||||
cx.runtime.create_many_signals_with_map(cx, values, |x| x)
|
||||
}
|
||||
|
||||
/// Works exactly as [create_many_signals], but applies the map function to each signal pair.
|
||||
/// Works exactly as [`create_many_signals`], but applies the map function to each signal pair.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -389,7 +397,7 @@ where
|
||||
}
|
||||
|
||||
/// Creates a signal that always contains the most recent value emitted by a
|
||||
/// [Stream](futures::stream::Stream).
|
||||
/// [`Stream`](futures::stream::Stream).
|
||||
/// If the stream has not yet emitted a value since the signal was created, the signal's
|
||||
/// value will be `None`.
|
||||
///
|
||||
@@ -410,7 +418,7 @@ pub fn create_signal_from_stream<T>(
|
||||
#[allow(unused_mut)] // allowed because needed for SSR
|
||||
mut stream: impl Stream<Item = T> + Unpin + 'static,
|
||||
) -> ReadSignal<Option<T>> {
|
||||
cfg_if! {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
_ = stream;
|
||||
let (read, _) = create_signal(cx, None);
|
||||
@@ -436,7 +444,7 @@ pub fn create_signal_from_stream<T>(
|
||||
/// and notifies other code when it has changed. This is the
|
||||
/// core primitive of Leptos’s reactive system.
|
||||
///
|
||||
/// `ReadSignal` is also [Copy] and `'static`, so it can very easily moved into closures
|
||||
/// `ReadSignal` is also [`Copy`] and `'static`, so it can very easily moved into closures
|
||||
/// or copied structs.
|
||||
///
|
||||
/// ## Core Trait Implementations
|
||||
@@ -557,6 +565,7 @@ impl<T> SignalWithUntracked<T> for ReadSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
self.with_no_subscription(f)
|
||||
}
|
||||
@@ -575,16 +584,16 @@ impl<T> SignalWithUntracked<T> for ReadSignal<T> {
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
let diagnostics = diagnostics!(self);
|
||||
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
match with_runtime(self.runtime, |runtime| {
|
||||
self.id.try_with(runtime, f, diagnostics)
|
||||
})
|
||||
.ok()
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
}) {
|
||||
Ok(Ok(o)) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,13 +630,14 @@ impl<T> SignalWith<T> for ReadSignal<T> {
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
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 ")
|
||||
.expect("runtime to be alive")
|
||||
{
|
||||
Ok(o) => o,
|
||||
Err(_) => panic_getting_dead_signal(
|
||||
@@ -651,6 +661,7 @@ impl<T> SignalWith<T> for ReadSignal<T> {
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
let diagnostics = diagnostics!(self);
|
||||
|
||||
@@ -765,6 +776,7 @@ impl<T> ReadSignal<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
#[inline(always)]
|
||||
pub(crate) fn with_no_subscription<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
self.id.with_no_subscription(self.runtime, f)
|
||||
}
|
||||
@@ -772,6 +784,7 @@ where
|
||||
/// Applies the function to the current Signal, if it exists, and subscribes
|
||||
/// the running effect.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub(crate) fn try_with<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&T) -> U,
|
||||
@@ -790,13 +803,7 @@ where
|
||||
|
||||
impl<T> Clone for ReadSignal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,13 +815,13 @@ impl<T> Copy for ReadSignal<T> {}
|
||||
/// and notifies other code when it has changed. This is the
|
||||
/// core primitive of Leptos’s reactive system.
|
||||
///
|
||||
/// Calling [WriteSignal::update] will mutate the signal’s value in place,
|
||||
/// Calling [`WriteSignal::update`] will mutate the signal’s value in place,
|
||||
/// and notify all subscribers that the signal’s value has changed.
|
||||
///
|
||||
/// `WriteSignal` implements [Fn], such that `set_value(new_value)` is equivalent to
|
||||
/// `WriteSignal` implements [`Fn`], such that `set_value(new_value)` is equivalent to
|
||||
/// `set_value.update(|value| *value = new_value)`.
|
||||
///
|
||||
/// `WriteSignal` is [Copy] and `'static`, so it can very easily moved into closures
|
||||
/// `WriteSignal` is [`Copy`] and `'static`, so it can very easily moved into closures
|
||||
/// or copied structs.
|
||||
///
|
||||
/// ## Core Trait Implementations
|
||||
@@ -918,6 +925,7 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
|
||||
self.id.update_with_no_effect(self.runtime, f);
|
||||
}
|
||||
@@ -935,6 +943,7 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update_returning_untracked<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
@@ -942,6 +951,7 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
|
||||
self.id.update_with_no_effect(self.runtime, f)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn try_update_untracked<O>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> O,
|
||||
@@ -980,6 +990,7 @@ impl<T> SignalUpdate<T> for WriteSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update(&self, f: impl FnOnce(&mut T)) {
|
||||
if self.id.update(self.runtime, f).is_none() {
|
||||
warn_updating_dead_signal(
|
||||
@@ -1002,6 +1013,7 @@ impl<T> SignalUpdate<T> for WriteSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
|
||||
self.id.update(self.runtime, f)
|
||||
}
|
||||
@@ -1073,13 +1085,7 @@ impl<T> SignalDispose for WriteSignal<T> {
|
||||
|
||||
impl<T> Clone for WriteSignal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1119,12 +1125,12 @@ impl<T> Copy for WriteSignal<T> {}
|
||||
#[track_caller]
|
||||
pub fn create_rw_signal<T>(cx: Scope, value: T) -> RwSignal<T> {
|
||||
let s = cx.runtime.create_rw_signal(value);
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Signal(s.id)));
|
||||
cx.push_scope_property(ScopeProperty::Signal(s.id));
|
||||
s
|
||||
}
|
||||
|
||||
/// A signal that combines the getter and setter into one value, rather than
|
||||
/// separating them into a [ReadSignal] and a [WriteSignal]. You may prefer this
|
||||
/// separating them into a [`ReadSignal`] and a [`WriteSignal`]. You may prefer this
|
||||
/// its style, or it may be easier to pass around in a context or as a function argument.
|
||||
///
|
||||
/// ## Core Trait Implementations
|
||||
@@ -1180,13 +1186,7 @@ where
|
||||
|
||||
impl<T> Clone for RwSignal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: self.ty,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1252,6 +1252,7 @@ impl<T> SignalWithUntracked<T> for RwSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
self.id.with_no_subscription(self.runtime, f)
|
||||
}
|
||||
@@ -1270,16 +1271,16 @@ impl<T> SignalWithUntracked<T> for RwSignal<T> {
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
let diagnostics = diagnostics!(self);
|
||||
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
match with_runtime(self.runtime, |runtime| {
|
||||
self.id.try_with(runtime, f, diagnostics)
|
||||
})
|
||||
.ok()
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
}) {
|
||||
Ok(Ok(o)) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1339,6 +1340,7 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
|
||||
self.id.update_with_no_effect(self.runtime, f);
|
||||
}
|
||||
@@ -1356,6 +1358,7 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update_returning_untracked<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
@@ -1376,6 +1379,7 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_update_untracked<O>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> O,
|
||||
@@ -1418,6 +1422,7 @@ impl<T> SignalWith<T> for RwSignal<T> {
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
let diagnostics = diagnostics!(self);
|
||||
|
||||
@@ -1448,6 +1453,7 @@ impl<T> SignalWith<T> for RwSignal<T> {
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
let diagnostics = diagnostics!(self);
|
||||
|
||||
@@ -1567,6 +1573,7 @@ impl<T> SignalUpdate<T> for RwSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update(&self, f: impl FnOnce(&mut T)) {
|
||||
if self.id.update(self.runtime, f).is_none() {
|
||||
warn_updating_dead_signal(
|
||||
@@ -1589,6 +1596,7 @@ impl<T> SignalUpdate<T> for RwSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
|
||||
self.id.update(self.runtime, f)
|
||||
}
|
||||
@@ -1715,7 +1723,7 @@ impl<T> RwSignal<T> {
|
||||
/// Returns a write-only handle to the signal.
|
||||
///
|
||||
/// Useful if you're trying to give write access to another component, or split an
|
||||
/// `RwSignal` into a [ReadSignal] and a [WriteSignal].
|
||||
/// [`RwSignal`] into a [`ReadSignal`] and a [`WriteSignal`].
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
@@ -1857,7 +1865,18 @@ impl NodeId {
|
||||
}
|
||||
}
|
||||
|
||||
fn try_with_no_subscription_inner(
|
||||
&self,
|
||||
runtime: &Runtime,
|
||||
) -> Result<Rc<RefCell<dyn Any>>, SignalError> {
|
||||
runtime.update_if_necessary(*self);
|
||||
let nodes = runtime.nodes.borrow();
|
||||
let node = nodes.get(*self).ok_or(SignalError::Disposed)?;
|
||||
Ok(node.value())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub(crate) fn try_with_no_subscription<T, U>(
|
||||
&self,
|
||||
runtime: &Runtime,
|
||||
@@ -1866,13 +1885,7 @@ impl NodeId {
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
runtime.update_if_necessary(*self);
|
||||
let value = {
|
||||
let nodes = runtime.nodes.borrow();
|
||||
let node = nodes.get(*self).ok_or(SignalError::Disposed)?;
|
||||
Rc::clone(&node.value)
|
||||
};
|
||||
|
||||
let value = self.try_with_no_subscription_inner(runtime)?;
|
||||
let value = value.borrow();
|
||||
let value = value
|
||||
.downcast_ref::<T>()
|
||||
@@ -1882,6 +1895,7 @@ impl NodeId {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub(crate) fn try_with<T, U>(
|
||||
&self,
|
||||
runtime: &Runtime,
|
||||
@@ -1896,6 +1910,7 @@ impl NodeId {
|
||||
self.try_with_no_subscription(runtime, f)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn with_no_subscription<T, U>(
|
||||
&self,
|
||||
runtime: RuntimeId,
|
||||
@@ -1910,6 +1925,7 @@ impl NodeId {
|
||||
.expect("runtime to be alive")
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn update_value<T, U>(
|
||||
&self,
|
||||
runtime: RuntimeId,
|
||||
@@ -1919,11 +1935,7 @@ impl NodeId {
|
||||
T: 'static,
|
||||
{
|
||||
with_runtime(runtime, |runtime| {
|
||||
let value = {
|
||||
let signals = runtime.nodes.borrow();
|
||||
signals.get(*self).map(|node| Rc::clone(&node.value))
|
||||
};
|
||||
if let Some(value) = value {
|
||||
if let Some(value) = runtime.get_value(*self) {
|
||||
let mut value = value.borrow_mut();
|
||||
if let Some(value) = value.downcast_mut::<T>() {
|
||||
Some(f(value))
|
||||
@@ -1950,6 +1962,7 @@ impl NodeId {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn update<T, U>(
|
||||
&self,
|
||||
runtime_id: RuntimeId,
|
||||
@@ -1959,11 +1972,7 @@ impl NodeId {
|
||||
T: 'static,
|
||||
{
|
||||
with_runtime(runtime_id, |runtime| {
|
||||
let value = {
|
||||
let signals = runtime.nodes.borrow();
|
||||
signals.get(*self).map(|node| Rc::clone(&node.value))
|
||||
};
|
||||
let updated = if let Some(value) = value {
|
||||
let updated = if let Some(value) = runtime.get_value(*self) {
|
||||
let mut value = value.borrow_mut();
|
||||
if let Some(value) = value.downcast_mut::<T>() {
|
||||
Some(f(value))
|
||||
@@ -1987,18 +1996,20 @@ impl NodeId {
|
||||
None
|
||||
};
|
||||
|
||||
// mark descendants dirty
|
||||
runtime.mark_dirty(*self);
|
||||
|
||||
// notify subscribers
|
||||
if updated.is_some() && !runtime.batching.get() {
|
||||
Runtime::run_effects(runtime_id);
|
||||
};
|
||||
if updated.is_some() {
|
||||
// mark descendants dirty
|
||||
runtime.mark_dirty(*self);
|
||||
|
||||
runtime.run_effects();
|
||||
}
|
||||
|
||||
updated
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn update_with_no_effect<T, U>(
|
||||
&self,
|
||||
runtime: RuntimeId,
|
||||
@@ -2012,6 +2023,8 @@ impl NodeId {
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
fn format_signal_warning(
|
||||
msg: &str,
|
||||
@@ -2034,6 +2047,8 @@ fn format_signal_warning(
|
||||
format!("{msg}\n{defined_at_msg}warning happened here: {location}",)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
pub(crate) fn panic_getting_dead_signal(
|
||||
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<'static>,
|
||||
@@ -2048,6 +2063,8 @@ pub(crate) fn panic_getting_dead_signal(
|
||||
)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
pub(crate) fn warn_updating_dead_signal(
|
||||
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<'static>,
|
||||
|
||||
@@ -21,8 +21,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for any kind of readable reactive signal: a [ReadSignal](crate::ReadSignal),
|
||||
/// [Memo](crate::Memo), [RwSignal](crate::RwSignal), or derived signal closure.
|
||||
/// A wrapper for any kind of readable reactive signal: a [`ReadSignal`](crate::ReadSignal),
|
||||
/// [`Memo`](crate::Memo), [`RwSignal`](crate::RwSignal), or derived signal closure.
|
||||
///
|
||||
/// This allows you to create APIs that take any kind of `Signal<T>` as an argument,
|
||||
/// rather than adding a generic `F: Fn() -> T`. Values can be access with the same
|
||||
|
||||
@@ -18,8 +18,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for any kind of settable reactive signal: a [WriteSignal](crate::WriteSignal),
|
||||
/// [RwSignal](crate::RwSignal), or closure that receives a value and sets a signal depending
|
||||
/// A wrapper for any kind of settable reactive signal: a [`WriteSignal`](crate::WriteSignal),
|
||||
/// [`RwSignal`](crate::RwSignal), or closure that receives a value and sets a signal depending
|
||||
/// on it.
|
||||
///
|
||||
/// This allows you to create APIs that take any kind of `SignalSetter<T>` as an argument,
|
||||
|
||||
@@ -3,9 +3,9 @@ use crate::{
|
||||
SignalUpdate, SignalWith,
|
||||
};
|
||||
|
||||
/// Derives a reactive slice of an [RwSignal](crate::RwSignal).
|
||||
/// Derives a reactive slice of an [`RwSignal`](crate::RwSignal).
|
||||
///
|
||||
/// Slices have the same guarantees as [Memos](crate::Memo):
|
||||
/// Slices have the same guarantees as [`Memo`s](crate::Memo):
|
||||
/// they only emit their value when it has actually been changed.
|
||||
///
|
||||
/// Slices need a getter and a setter, and you must make sure that
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use cfg_if::cfg_if;
|
||||
use std::future::Future;
|
||||
|
||||
/// Spawns and runs a thread-local [std::future::Future] in a platform-independent way.
|
||||
/// Spawns and runs a thread-local [`Future`] in a platform-independent way.
|
||||
///
|
||||
/// This can be used to interface with any `async` code.
|
||||
pub fn spawn_local<F>(fut: F)
|
||||
|
||||
@@ -7,7 +7,7 @@ use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// Exposes the [`queueMicrotask`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
pub fn queue_microtask(task: impl FnOnce() + 'static) {
|
||||
microtask(wasm_bindgen::closure::Closure::once_into_js(task));
|
||||
@@ -21,7 +21,7 @@ cfg_if! {
|
||||
fn microtask(task: wasm_bindgen::JsValue);
|
||||
}
|
||||
} else {
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// Exposes the [`queueMicrotask`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
pub fn queue_microtask(task: impl FnOnce() + 'static) {
|
||||
task();
|
||||
|
||||
@@ -3,17 +3,17 @@ use crate::{with_runtime, RuntimeId, Scope, ScopeProperty};
|
||||
use std::{cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a [StoredValue].
|
||||
/// Unique ID assigned to a [`StoredValue`].
|
||||
pub(crate) struct StoredValueId;
|
||||
}
|
||||
|
||||
/// A **non-reactive** wrapper for any value, which can be created with [store_value].
|
||||
/// A **non-reactive** wrapper for any value, which can be created with [`store_value`].
|
||||
///
|
||||
/// If you want a reactive wrapper, use [create_signal](crate::create_signal).
|
||||
/// If you want a reactive wrapper, use [`create_signal`](crate::create_signal).
|
||||
///
|
||||
/// This allows you to create a stable reference for any value by storing it within
|
||||
/// the reactive system. Like the signal types (e.g., [ReadSignal](crate::ReadSignal)
|
||||
/// and [RwSignal](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
|
||||
/// the reactive system. Like the signal types (e.g., [`ReadSignal`](crate::ReadSignal)
|
||||
/// and [`RwSignal`](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
|
||||
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
|
||||
/// updating it does not notify anything else.
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
@@ -43,7 +43,7 @@ impl<T> StoredValue<T> {
|
||||
/// to this signal.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@@ -77,7 +77,7 @@ impl<T> StoredValue<T> {
|
||||
/// to this signal.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@@ -128,7 +128,7 @@ impl<T> StoredValue<T> {
|
||||
/// Applies a function to the current stored value.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@@ -155,7 +155,7 @@ impl<T> StoredValue<T> {
|
||||
/// Applies a function to the current stored value.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
|
||||
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@@ -375,8 +375,8 @@ impl<T> StoredValue<T> {
|
||||
/// Creates a **non-reactive** wrapper for any value by storing it within
|
||||
/// the reactive system.
|
||||
///
|
||||
/// Like the signal types (e.g., [ReadSignal](crate::ReadSignal)
|
||||
/// and [RwSignal](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
|
||||
/// Like the signal types (e.g., [`ReadSignal`](crate::ReadSignal)
|
||||
/// and [`RwSignal`](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
|
||||
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
|
||||
/// updating it does not notify anything else.
|
||||
/// ```compile_fail
|
||||
@@ -418,7 +418,7 @@ where
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::StoredValue(id)));
|
||||
cx.push_scope_property(ScopeProperty::StoredValue(id));
|
||||
StoredValue {
|
||||
runtime: cx.runtime,
|
||||
id,
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
use futures::Future;
|
||||
use std::{borrow::Cow, collections::VecDeque, pin::Pin};
|
||||
|
||||
/// Tracks [Resource](crate::Resource)s that are read under a suspense context,
|
||||
/// 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.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct SuspenseContext {
|
||||
|
||||
252
leptos_reactive/src/trigger.rs
Normal file
252
leptos_reactive/src/trigger.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use crate::{
|
||||
diagnostics,
|
||||
diagnostics::*,
|
||||
node::NodeId,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
Scope, ScopeProperty, SignalGet, SignalSet, SignalUpdate,
|
||||
};
|
||||
|
||||
/// Reactive Trigger, notifies reactive code to rerun.
|
||||
///
|
||||
/// See [`create_trigger`] for more.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Trigger {
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: NodeId,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl Trigger {
|
||||
/// Notifies any reactive code where this trigger is tracked to rerun.
|
||||
pub fn notify(&self) {
|
||||
assert!(self.try_notify(), "Trigger::notify(): runtime not alive")
|
||||
}
|
||||
|
||||
/// Attempts to notify any reactive code where this trigger is tracked to rerun.
|
||||
///
|
||||
/// Returns `None` if the runtime has been disposed.
|
||||
pub fn try_notify(&self) -> bool {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.mark_dirty(self.id);
|
||||
runtime.run_effects();
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Subscribes the running effect to this trigger.
|
||||
pub fn track(&self) {
|
||||
assert!(self.try_track(), "Trigger::track(): runtime not alive")
|
||||
}
|
||||
|
||||
/// Attempts to subscribe the running effect to this trigger, returning
|
||||
/// `None` if the runtime has been disposed.
|
||||
pub fn try_track(&self) -> bool {
|
||||
let diagnostics = diagnostics!(self);
|
||||
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
self.id.subscribe(runtime, diagnostics);
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Trigger`], a kind of reactive primitive.
|
||||
///
|
||||
/// A trigger is a data-less signal with the sole purpose
|
||||
/// of notifying other reactive code of a change. This can be useful
|
||||
/// for when using external data not stored in signals, for example.
|
||||
///
|
||||
/// Take a reactive [`Scope`] and returns the [`Trigger`] handle, which
|
||||
/// can be called as a function to track the trigger in the current
|
||||
/// reactive context.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// use std::{cell::RefCell, fmt::Write, rc::Rc};
|
||||
///
|
||||
/// let external_data = Rc::new(RefCell::new(1));
|
||||
/// let output = Rc::new(RefCell::new(String::new()));
|
||||
///
|
||||
/// let rerun_on_data = create_trigger(cx);
|
||||
///
|
||||
/// let o = output.clone();
|
||||
/// let e = external_data.clone();
|
||||
/// create_effect(cx, move |_| {
|
||||
/// rerun_on_data(); // or rerun_on_data.track();
|
||||
/// write!(o.borrow_mut(), "{}", *e.borrow());
|
||||
/// *e.borrow_mut() += 1;
|
||||
/// });
|
||||
/// # if !cfg!(feature = "ssr") {
|
||||
/// assert_eq!(*output.borrow(), "1");
|
||||
///
|
||||
/// rerun_on_data.notify(); // reruns the above effect
|
||||
///
|
||||
/// assert_eq!(*output.borrow(), "12");
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(scope = ?cx.id)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_trigger(cx: Scope) -> Trigger {
|
||||
let t = cx.runtime.create_trigger();
|
||||
cx.push_scope_property(ScopeProperty::Trigger(t.id));
|
||||
t
|
||||
}
|
||||
|
||||
impl SignalGet<()> for Trigger {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Trigger::get()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
fn get(&self) {
|
||||
self.track()
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Trigger::try_get()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_get(&self) -> Option<()> {
|
||||
self.try_track().then_some(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalUpdate<()> for Trigger {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Trigger::update()",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn update(&self, f: impl FnOnce(&mut ())) {
|
||||
self.try_update(f).expect("runtime to be alive")
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Trigger::try_update()",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_update<O>(&self, f: impl FnOnce(&mut ()) -> O) -> Option<O> {
|
||||
// run callback with runtime before dirtying the trigger,
|
||||
// consistent with signals.
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let res = f(&mut ());
|
||||
|
||||
runtime.mark_dirty(self.id);
|
||||
runtime.run_effects();
|
||||
|
||||
Some(res)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalSet<()> for Trigger {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Trigger::set()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn set(&self, _: ()) {
|
||||
self.notify();
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Trigger::try_set()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn try_set(&self, _: ()) -> Option<()> {
|
||||
self.try_notify().then_some(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl FnOnce<()> for Trigger {
|
||||
type Output = ();
|
||||
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||
self.track()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl FnMut<()> for Trigger {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.track()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl Fn<()> for Trigger {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.track()
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
leptos_reactive = { workspace = true }
|
||||
server_fn = { workspace = true }
|
||||
server_fn = { workspace = true, default-features = false }
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
@@ -19,8 +19,11 @@ thiserror = "1"
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
default = ["default-tls"]
|
||||
csr = ["leptos_reactive/csr"]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
hydrate = ["leptos_reactive/hydrate"]
|
||||
rustls = ["server_fn/rustls"]
|
||||
ssr = ["leptos_reactive/ssr", "server_fn/ssr"]
|
||||
stable = ["leptos_reactive/stable", "server_fn/stable"]
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::{additional_attributes::AdditionalAttributes, TextProp};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
@@ -20,8 +19,7 @@ impl BodyContext {
|
||||
.map(|val| format!("class=\"{}\"", val.get()));
|
||||
let attributes = self.attributes.borrow().as_ref().map(|val| {
|
||||
val.with(|val| {
|
||||
val.0
|
||||
.iter()
|
||||
val.into_iter()
|
||||
.map(|(n, v)| format!("{}=\"{}\"", n, v.get()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
@@ -102,8 +100,10 @@ pub fn Body(
|
||||
|
||||
if let Some(attributes) = attributes {
|
||||
let attributes = attributes.get();
|
||||
for (attr_name, attr_value) in attributes.0.into_iter() {
|
||||
for (attr_name, attr_value) in attributes.into_iter() {
|
||||
let el = el.clone();
|
||||
let attr_name = attr_name.to_owned();
|
||||
let attr_value = attr_value.to_owned();
|
||||
create_render_effect(cx, move |_|{
|
||||
let value = attr_value.get();
|
||||
_ = el.set_attribute(&attr_name, &value);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::{additional_attributes::AdditionalAttributes, TextProp};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
@@ -32,8 +31,7 @@ impl HtmlContext {
|
||||
.map(|val| format!("class=\"{}\"", val.get()));
|
||||
let attributes = self.attributes.borrow().as_ref().map(|val| {
|
||||
val.with(|val| {
|
||||
val.0
|
||||
.iter()
|
||||
val.into_iter()
|
||||
.map(|(n, v)| format!("{}=\"{}\"", n, v.get()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
@@ -131,8 +129,10 @@ pub fn Html(
|
||||
|
||||
if let Some(attributes) = attributes {
|
||||
let attributes = attributes.get();
|
||||
for (attr_name, attr_value) in attributes.0.into_iter() {
|
||||
for (attr_name, attr_value) in attributes.into_iter() {
|
||||
let el = el.clone();
|
||||
let attr_name = attr_name.to_owned();
|
||||
let attr_value = attr_value.to_owned();
|
||||
create_render_effect(cx, move |_|{
|
||||
let value = attr_value.get();
|
||||
_ = el.set_attribute(&attr_name, &value);
|
||||
|
||||
@@ -50,6 +50,7 @@ use leptos::{
|
||||
*,
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
@@ -58,7 +59,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 +67,6 @@ mod script;
|
||||
mod style;
|
||||
mod stylesheet;
|
||||
mod title;
|
||||
pub use additional_attributes::*;
|
||||
pub use body::*;
|
||||
pub use html::*;
|
||||
pub use link::*;
|
||||
@@ -101,7 +100,7 @@ pub struct MetaTagsContext {
|
||||
els: Rc<
|
||||
RefCell<
|
||||
HashMap<
|
||||
String,
|
||||
Cow<'static, str>,
|
||||
(HtmlElement<AnyElement>, Scope, Option<web_sys::Element>),
|
||||
>,
|
||||
>,
|
||||
@@ -131,7 +130,7 @@ impl MetaTagsContext {
|
||||
pub fn register(
|
||||
&self,
|
||||
cx: Scope,
|
||||
id: String,
|
||||
id: Cow<'static, str>,
|
||||
builder_el: HtmlElement<AnyElement>,
|
||||
) {
|
||||
cfg_if! {
|
||||
@@ -308,45 +307,6 @@ pub fn generate_head_metadata_separated(cx: Scope) -> (String, String) {
|
||||
(head, format!("<body{body_meta}>"))
|
||||
}
|
||||
|
||||
/// Describes a value that is either a static or a reactive string, i.e.,
|
||||
/// a [String], a [&str], or a reactive `Fn() -> String`.
|
||||
#[derive(Clone)]
|
||||
pub struct TextProp(Rc<dyn Fn() -> String>);
|
||||
|
||||
impl TextProp {
|
||||
fn get(&self) -> String {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TextProp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("TextProp").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for TextProp {
|
||||
fn from(s: String) -> Self {
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TextProp {
|
||||
fn from(s: &str) -> Self {
|
||||
let s = s.to_string();
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> From<F> for TextProp
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
{
|
||||
fn from(s: F) -> Self {
|
||||
TextProp(Rc::new(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn feature_warning() {
|
||||
if !cfg!(any(feature = "csr", feature = "hydrate", feature = "ssr")) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::use_head;
|
||||
use leptos::*;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
@@ -28,68 +29,69 @@ pub fn Link(
|
||||
cx: Scope,
|
||||
/// The [`id`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-id) attribute.
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
id: Option<Cow<'static, str>>,
|
||||
/// The [`as`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as) attribute.
|
||||
#[prop(optional, into)]
|
||||
as_: Option<String>,
|
||||
as_: Option<Cow<'static, str>>,
|
||||
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-crossorigin) attribute.
|
||||
#[prop(optional, into)]
|
||||
crossorigin: Option<String>,
|
||||
crossorigin: Option<Cow<'static, str>>,
|
||||
/// The [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-disabled) attribute.
|
||||
#[prop(optional, into)]
|
||||
disabled: Option<bool>,
|
||||
/// The [`fetchpriority`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-fetchpriority) attribute.
|
||||
#[prop(optional, into)]
|
||||
fetchpriority: Option<String>,
|
||||
fetchpriority: Option<Cow<'static, str>>,
|
||||
/// The [`href`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-href) attribute.
|
||||
#[prop(optional, into)]
|
||||
href: Option<String>,
|
||||
href: Option<Cow<'static, str>>,
|
||||
/// The [`hreflang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-hreflang) attribute.
|
||||
#[prop(optional, into)]
|
||||
hreflang: Option<String>,
|
||||
hreflang: Option<Cow<'static, str>>,
|
||||
/// The [`imagesizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesizes) attribute.
|
||||
#[prop(optional, into)]
|
||||
imagesizes: Option<String>,
|
||||
imagesizes: Option<Cow<'static, str>>,
|
||||
/// The [`imagesrcset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesrcset) attribute.
|
||||
#[prop(optional, into)]
|
||||
imagesrcset: Option<String>,
|
||||
imagesrcset: Option<Cow<'static, str>>,
|
||||
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-integrity) attribute.
|
||||
#[prop(optional, into)]
|
||||
integrity: Option<String>,
|
||||
integrity: Option<Cow<'static, str>>,
|
||||
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-media) attribute.
|
||||
#[prop(optional, into)]
|
||||
media: Option<String>,
|
||||
media: Option<Cow<'static, str>>,
|
||||
/// The [`prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-prefetch) attribute.
|
||||
#[prop(optional, into)]
|
||||
prefetch: Option<String>,
|
||||
prefetch: Option<Cow<'static, str>>,
|
||||
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-referrerpolicy) attribute.
|
||||
#[prop(optional, into)]
|
||||
referrerpolicy: Option<String>,
|
||||
referrerpolicy: Option<Cow<'static, str>>,
|
||||
/// The [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-rel) attribute.
|
||||
#[prop(optional, into)]
|
||||
rel: Option<String>,
|
||||
rel: Option<Cow<'static, str>>,
|
||||
/// The [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes) attribute.
|
||||
#[prop(optional, into)]
|
||||
sizes: Option<String>,
|
||||
sizes: Option<Cow<'static, str>>,
|
||||
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-title) attribute.
|
||||
#[prop(optional, into)]
|
||||
title: Option<String>,
|
||||
title: Option<Cow<'static, str>>,
|
||||
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-type) attribute.
|
||||
#[prop(optional, into)]
|
||||
type_: Option<String>,
|
||||
type_: Option<Cow<'static, str>>,
|
||||
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-blocking) attribute.
|
||||
#[prop(optional, into)]
|
||||
blocking: Option<String>,
|
||||
blocking: Option<Cow<'static, str>>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head(cx);
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
|
||||
let id: Cow<'static, str> =
|
||||
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
|
||||
|
||||
let builder_el = leptos::leptos_dom::html::as_meta_tag({
|
||||
let id = id.clone();
|
||||
move || {
|
||||
leptos::leptos_dom::html::link(cx)
|
||||
.attr("id", &id)
|
||||
.attr("id", id)
|
||||
.attr("as", as_)
|
||||
.attr("crossorigin", crossorigin)
|
||||
.attr("disabled", disabled.unwrap_or(false))
|
||||
|
||||
@@ -53,5 +53,5 @@ pub fn Meta(
|
||||
.attr("content", move || content.as_ref().map(|v| v.get()))
|
||||
});
|
||||
|
||||
meta.tags.register(cx, id, builder_el.into_any());
|
||||
meta.tags.register(cx, id.into(), builder_el.into_any());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::use_head;
|
||||
use leptos::*;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLScriptElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
@@ -25,53 +26,54 @@ pub fn Script(
|
||||
cx: Scope,
|
||||
/// An ID for the `<script>` tag.
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
id: Option<Cow<'static, str>>,
|
||||
/// The [`async`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-async) attribute.
|
||||
#[prop(optional, into)]
|
||||
async_: Option<String>,
|
||||
async_: Option<Cow<'static, str>>,
|
||||
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-crossorigin) attribute.
|
||||
#[prop(optional, into)]
|
||||
crossorigin: Option<String>,
|
||||
crossorigin: Option<Cow<'static, str>>,
|
||||
/// The [`defer`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer) attribute.
|
||||
#[prop(optional, into)]
|
||||
defer: Option<String>,
|
||||
defer: Option<Cow<'static, str>>,
|
||||
/// The [`fetchpriority `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-fetchpriority ) attribute.
|
||||
#[prop(optional, into)]
|
||||
fetchpriority: Option<String>,
|
||||
fetchpriority: Option<Cow<'static, str>>,
|
||||
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-integrity) attribute.
|
||||
#[prop(optional, into)]
|
||||
integrity: Option<String>,
|
||||
integrity: Option<Cow<'static, str>>,
|
||||
/// The [`nomodule`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nomodule) attribute.
|
||||
#[prop(optional, into)]
|
||||
nomodule: Option<String>,
|
||||
nomodule: Option<Cow<'static, str>>,
|
||||
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nonce) attribute.
|
||||
#[prop(optional, into)]
|
||||
nonce: Option<String>,
|
||||
nonce: Option<Cow<'static, str>>,
|
||||
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-referrerpolicy) attribute.
|
||||
#[prop(optional, into)]
|
||||
referrerpolicy: Option<String>,
|
||||
referrerpolicy: Option<Cow<'static, str>>,
|
||||
/// The [`src`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-src) attribute.
|
||||
#[prop(optional, into)]
|
||||
src: Option<String>,
|
||||
src: Option<Cow<'static, str>>,
|
||||
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type) attribute.
|
||||
#[prop(optional, into)]
|
||||
type_: Option<String>,
|
||||
type_: Option<Cow<'static, str>>,
|
||||
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-blocking) attribute.
|
||||
#[prop(optional, into)]
|
||||
blocking: Option<String>,
|
||||
blocking: Option<Cow<'static, str>>,
|
||||
/// The content of the `<script>` tag.
|
||||
#[prop(optional)]
|
||||
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head(cx);
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
|
||||
let id: Cow<'static, str> =
|
||||
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
|
||||
|
||||
let builder_el = leptos::leptos_dom::html::as_meta_tag({
|
||||
let id = id.clone();
|
||||
move || {
|
||||
leptos::leptos_dom::html::script(cx)
|
||||
.attr("id", &id)
|
||||
.attr("id", id)
|
||||
.attr("async", async_)
|
||||
.attr("crossorigin", crossorigin)
|
||||
.attr("defer", defer)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::use_head;
|
||||
use leptos::*;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLStyleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) into the document
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
@@ -25,32 +26,33 @@ pub fn Style(
|
||||
cx: Scope,
|
||||
/// An ID for the `<script>` tag.
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
id: Option<Cow<'static, str>>,
|
||||
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-media) attribute.
|
||||
#[prop(optional, into)]
|
||||
media: Option<String>,
|
||||
media: Option<Cow<'static, str>>,
|
||||
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-nonce) attribute.
|
||||
#[prop(optional, into)]
|
||||
nonce: Option<String>,
|
||||
nonce: Option<Cow<'static, str>>,
|
||||
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-title) attribute.
|
||||
#[prop(optional, into)]
|
||||
title: Option<String>,
|
||||
title: Option<Cow<'static, str>>,
|
||||
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-blocking) attribute.
|
||||
#[prop(optional, into)]
|
||||
blocking: Option<String>,
|
||||
blocking: Option<Cow<'static, str>>,
|
||||
/// The content of the `<style>` tag.
|
||||
#[prop(optional)]
|
||||
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head(cx);
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
|
||||
let id: Cow<'static, str> =
|
||||
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
|
||||
|
||||
let builder_el = leptos::leptos_dom::html::as_meta_tag({
|
||||
let id = id.clone();
|
||||
move || {
|
||||
leptos::leptos_dom::html::style(cx)
|
||||
.attr("id", &id)
|
||||
.attr("id", id)
|
||||
.attr("media", media)
|
||||
.attr("nonce", nonce)
|
||||
.attr("title", title)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user