mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 14:52:35 -05:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54733e1b34 | ||
|
|
56f01888b7 | ||
|
|
8320f16716 | ||
|
|
0b16e5992d | ||
|
|
248beb4a55 | ||
|
|
c9f608d030 | ||
|
|
f837d3e6a2 | ||
|
|
8847d5fc42 | ||
|
|
7819a6fac0 | ||
|
|
c199185808 | ||
|
|
e0b5738606 | ||
|
|
f3e3880a57 | ||
|
|
d44b90c16d | ||
|
|
cc32a3e863 | ||
|
|
5740c9b76b | ||
|
|
80fa6ad3eb | ||
|
|
7bc1ad2b4f | ||
|
|
82a2fe7cbe | ||
|
|
40bf944957 | ||
|
|
7ef7546fa9 | ||
|
|
5e26e84d77 | ||
|
|
e67bc2083a | ||
|
|
a3cb3f7f77 | ||
|
|
daeb47e72e | ||
|
|
8c5ab99fa7 | ||
|
|
984a7388f1 | ||
|
|
274b105676 | ||
|
|
a689d1b4c0 | ||
|
|
1581e91317 | ||
|
|
20f4034c1c | ||
|
|
9fb1c4b67c | ||
|
|
2e559d6a06 | ||
|
|
71de6c395b | ||
|
|
b09f9e4814 | ||
|
|
ec4bfb0e8a | ||
|
|
39bf38d1e4 | ||
|
|
e6fd1379b8 | ||
|
|
1d9931a5a8 | ||
|
|
06164d34b5 | ||
|
|
f3de288e19 | ||
|
|
62bf315059 |
@@ -70,6 +70,25 @@ are a few guidelines that will make it a better experience for everyone:
|
||||
`cargo-make` and using `cargo make check && cargo make test && cargo make
|
||||
check-examples`.
|
||||
|
||||
## Before Submitting a PR
|
||||
|
||||
We have a fairly extensive CI setup that runs both lints (like `rustfmt` and `clippy`)
|
||||
and tests on PRs. You can run most of these locally if you have `cargo-make` installed.
|
||||
|
||||
If you added an example, make sure to add it to the list in `examples/Makefile.toml`.
|
||||
|
||||
From the root directory of the repo, run
|
||||
- `cargo +nightly fmt`
|
||||
- `cargo +nightly make check`
|
||||
- `cargo +nightly make test`
|
||||
- `cargo +nightly make check-examples`
|
||||
- `cargo +nightly make --profile=github-actions ci`
|
||||
|
||||
If you modified an example:
|
||||
- `cd examples/your_example`
|
||||
- `cargo +nightly fmt -- --config-path ../..`
|
||||
- `cargo +nightly make --profile=github-actions verify-flow`
|
||||
|
||||
## Architecture
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md).
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.5"
|
||||
version = "0.4.7"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.4.5" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.5" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.5" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.5" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.5" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.5" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.5" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.5" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.5" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.5" }
|
||||
leptos_router = { path = "./router", version = "0.4.5" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.5" }
|
||||
leptos = { path = "./leptos", version = "0.4.7" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.7" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.7" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.7" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.7" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.7" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.7" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.7" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.7" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.7" }
|
||||
leptos_router = { path = "./router", version = "0.4.7" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.7" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.7" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -20,6 +20,18 @@ cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "ci-clean"]
|
||||
|
||||
[tasks.check-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "check-clean"]
|
||||
|
||||
[tasks.build-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "build-clean"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
|
||||
@@ -88,8 +88,6 @@ targets = ["wasm32-unknown-unknown"]
|
||||
|
||||
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
|
||||
|
||||
> Note: The `nightly` feature is present on the main branch version right now, but not in 0.3.x. For 0.3.x, nightly is the default and `stable` has a special feature.
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
|
||||
@@ -4,10 +4,10 @@ We just defined the following set of routes:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=Home
|
||||
<Route path="/users" view=Users
|
||||
<Route path="/users/:id" view=UserProfile
|
||||
<Route path="/*any" view=NotFound
|
||||
<Route path="/" view=Home/>
|
||||
<Route path="/users" view=Users/>
|
||||
<Route path="/users/:id" view=UserProfile/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
@@ -17,11 +17,11 @@ Well... you can!
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=Home
|
||||
<Route path="/users" view=Users
|
||||
<Route path=":id" view=UserProfile
|
||||
<Route path="/" view=Home/>
|
||||
<Route path="/users" view=Users>
|
||||
<Route path=":id" view=UserProfile/>
|
||||
</Route>
|
||||
<Route path="/*any" view=NotFound
|
||||
<Route path="/*any" view=NotFound/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
@@ -39,8 +39,8 @@ Let’s look back at our practical example.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=Users
|
||||
<Route path="/users/:id" view=UserProfile
|
||||
<Route path="/users" view=Users/>
|
||||
<Route path="/users/:id" view=UserProfile/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
@@ -53,8 +53,8 @@ Let’s say I use nested routes instead:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=Users
|
||||
<Route path=":id" view=UserProfile
|
||||
<Route path="/users" view=Users>
|
||||
<Route path=":id" view=UserProfile/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
@@ -68,9 +68,9 @@ I actually need to add a fallback route
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=Users
|
||||
<Route path=":id" view=UserProfile
|
||||
<Route path="" view=NoUser
|
||||
<Route path="/users" view=Users>
|
||||
<Route path=":id" view=UserProfile/>
|
||||
<Route path="" view=NoUser/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
@@ -94,8 +94,8 @@ You can easily define this with nested routes
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=ContactList
|
||||
<Route path=":id" view=ContactInfo
|
||||
<Route path="/contacts" view=ContactList>
|
||||
<Route path=":id" view=ContactInfo/>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
@@ -107,11 +107,11 @@ You can go even deeper. Say you want to have tabs for each contact’s address,
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=ContactList
|
||||
<Route path=":id" view=ContactInfo
|
||||
<Route path="" view=EmailAndPhone
|
||||
<Route path="address" view=Address
|
||||
<Route path="messages" view=Messages
|
||||
<Route path="/contacts" view=ContactList>
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=EmailAndPhone/>
|
||||
<Route path="address" view=Address/>
|
||||
<Route path="messages" view=Messages/>
|
||||
</Route>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
@@ -203,7 +203,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
path="/contacts"
|
||||
view=ContactList
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<div class="tab">
|
||||
"(Contact Info)"
|
||||
|
||||
@@ -36,6 +36,14 @@ struct ContactSearch {
|
||||
```
|
||||
|
||||
> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure you’re importing the right one for the derive macro.
|
||||
>
|
||||
> If you are not using the `nightly` feature, you will get the error
|
||||
>
|
||||
> ```
|
||||
> no function or associated item named `into_param` found for struct `std::string::String` in the current scope
|
||||
> ```
|
||||
>
|
||||
> At the moment, supporting both `T: FromStr` and `Option<T>` for typed params requires a nightly feature. You can fix this by simply changing the struct to use `q: Option<String>` instead of `q: String`.
|
||||
|
||||
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
|
||||
|
||||
@@ -109,8 +117,9 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=ContactList
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<div class="tab">
|
||||
"(Contact Info)"
|
||||
|
||||
@@ -11,6 +11,8 @@ The router will bail out of handling an `<a>` click under a number of situations
|
||||
|
||||
In other words, the router will only try to do a client-side navigation when it’s pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior.
|
||||
|
||||
> This also means that if you need to opt out of client-side routing, you can do so easily. For example, if you have a link to another page on the same domain, but which isn’t part of your Leptos app, you can just use `<a rel="external">` to tell the router it isn’t something it can handle.
|
||||
|
||||
The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things:
|
||||
|
||||
1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears.
|
||||
@@ -53,8 +55,9 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=ContactList
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<div class="tab">
|
||||
"(Contact Info)"
|
||||
|
||||
@@ -80,7 +80,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
<h1><code>"<Form/>"</code></h1>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=FormExample
|
||||
<Route path="" view=FormExample/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -113,7 +113,7 @@ Server functions are a cool technology, but it’s very important to remember. *
|
||||
|
||||
So far, everything I’ve said is actually framework agnostic. (And in fact, the Leptos server function crate has been integrated into Dioxus as well!) Server functions are simply a way of defining a function-like RPC call that leans on Web standards like HTTP requests and URL encoding.
|
||||
|
||||
But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](../async/README.md). So you can easily integrate your server functions with the rest of your applications:
|
||||
But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](https://leptos-rs.github.io/leptos/async/index.html). So you can easily integrate your server functions with the rest of your applications:
|
||||
|
||||
- Create **resources** that call the server function to load data from the server
|
||||
- Read these resources under `<Suspense/>` or `<Transition/>` to enable streaming SSR and fallback states while data loads.
|
||||
|
||||
@@ -6,9 +6,9 @@ The server functions we looked at in the last chapter showed how to run code on
|
||||
|
||||
We call Leptos a “full-stack” framework, but “full-stack” is always a misnomer (after all, it never means everything from the browser to your power company.) For us, “full stack” means that your Leptos app can run in the browser, and can run on the server, and can integrate the two, drawing together the unique features available in each; as we’ve seen in the book so far, a button click on the browser can drive a database read on the server, both written in the same Rust module. But Leptos itself doesn’t provide the server (or the database, or the operating system, or the firmware, or the electrical cables...)
|
||||
|
||||
Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_actix/latest/leptos_axum/)). We’ve built integrations with each server’s router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls.
|
||||
Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/)). We’ve built integrations with each server’s router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls.
|
||||
|
||||
> If haven’t seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, now’s a good time to check them out.
|
||||
> If you haven’t seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, now’s a good time to check them out.
|
||||
|
||||
## Using Extractors
|
||||
|
||||
@@ -43,7 +43,7 @@ pub async fn actix_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
|
||||
## Axum Extractors
|
||||
|
||||
The syntax for the `leptos_axum::extract` function is very similar. (**Note**: This is available on the git main branch, but has not been released as of writing.) Note that Axum extractors return a `Result`, so you’ll need to add something to handle the error case.
|
||||
The syntax for the [`leptos_axum::extract`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract.html) function is very similar. (**Note**: This is available on the git main branch, but has not been released as of writing.) Note that Axum extractors return a `Result`, so you’ll need to add something to handle the error case.
|
||||
|
||||
```rust
|
||||
#[server(AxumExtract, "/api")]
|
||||
|
||||
@@ -8,7 +8,12 @@ If you’ve ever listened to streaming music or watched a video online, I’m su
|
||||
|
||||
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.
|
||||
Leptos supports all four different modes of rendering HTML that includes asynchronous data:
|
||||
|
||||
1. [Synchronous Rendering](#synchronous-rendering)
|
||||
1. [Async Rendering](#async-rendering)
|
||||
1. [In-Order streaming](#in-order-streaming)
|
||||
1. [Out-of-Order Streaming](#out-of-order-streaming)
|
||||
|
||||
## Synchronous Rendering
|
||||
|
||||
@@ -79,7 +84,7 @@ Because it offers the best blend of performance characteristics, Leptos defaults
|
||||
```rust
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=HomePage
|
||||
<Route path="" view=HomePage/>
|
||||
|
||||
// We'll load the posts with async rendering, so they can set
|
||||
// the title and metadata *after* loading the data
|
||||
|
||||
@@ -139,7 +139,7 @@ view! { cx,
|
||||
Remember—and this is _very important_—only functions are reactive. This means that
|
||||
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||
in a function, telling the framework to update the view every time `count` changes.
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
`{count()}` accesses the value of `count` once, and passes an `i32` into the view,
|
||||
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
|
||||
|
||||
Let’s make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Let’s replace “set this value to 3” with “increment this value by 1”:
|
||||
|
||||
@@ -219,9 +219,25 @@ where
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
any value that implements this `Fn()` trait.
|
||||
|
||||
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
|
||||
> or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
> a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
This generic can also be specified inline:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar<F: Fn() -> i32 + 'static>(
|
||||
cx: Scope,
|
||||
#[prop(default = 100)] max: u16,
|
||||
progress: F,
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Note that generic component props _can’t_ be specified with an `impl` yet (`progress: impl Fn() -> i32 + 'static,`), in part because they’re actually used to generate a `struct ProgressBarProps`, and struct fields cannot be `impl` types. The `#[component]` macro may be further improved in the future to allow inline `impl` generic props.
|
||||
|
||||
### `into` Props
|
||||
|
||||
@@ -271,6 +287,81 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Generic Props
|
||||
|
||||
Note that you can’t specify optional generic props for a component. Let’s see what would happen if you try:
|
||||
|
||||
```rust,compile_fail
|
||||
#[component]
|
||||
fn ProgressBar<F: Fn() -> i32 + 'static>(
|
||||
cx: Scope,
|
||||
#[prop(optional)] progress: Option<F>,
|
||||
) -> impl IntoView {
|
||||
progress.map(|progress| {
|
||||
view! { cx,
|
||||
<progress
|
||||
max=100
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<ProgressBar/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rust helpfully gives the error
|
||||
|
||||
```
|
||||
xx | <ProgressBar/>
|
||||
| ^^^^^^^^^^^ cannot infer type of the type parameter `F` declared on the function `ProgressBar`
|
||||
|
|
||||
help: consider specifying the generic argument
|
||||
|
|
||||
xx | <ProgressBar::<F>/>
|
||||
| +++++
|
||||
```
|
||||
|
||||
There are just two problems:
|
||||
|
||||
1. Leptos’s view macro doesn’t support specifying a generic on a component with this turbofish syntax.
|
||||
2. Even if you could, specifying the correct type here is not possible; closures and functions in general are unnameable types. The compiler can display them with a shorthand, but you can’t specify them.
|
||||
|
||||
However, you can get around this by providing a concrete type using `Box<dyn _>` or `&dyn _`:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
#[prop(optional)] progress: Option<Box<dyn Fn() -> i32>>,
|
||||
) -> impl IntoView {
|
||||
progress.map(|progress| {
|
||||
view! { cx,
|
||||
<progress
|
||||
max=100
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<ProgressBar/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because the Rust compiler now knows the concrete type of the prop, and therefore its size in memory even in the `None` case, this compiles fine.
|
||||
|
||||
> In this particular case, `&dyn Fn() -> i32` will cause lifetime issues, but in other cases, it may be a possibility.
|
||||
|
||||
## Documenting Components
|
||||
|
||||
This is one of the least essential but most important sections of this book.
|
||||
|
||||
@@ -115,11 +115,11 @@ Calling it like this will create a list:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<WrappedChildren>
|
||||
<WrapsChildren>
|
||||
"A"
|
||||
"B"
|
||||
"C"
|
||||
</WrappedChildren>
|
||||
</WrapsChildren>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -5,32 +5,34 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
|
||||
CARGO_MAKE_WORKSPACE_EMULATION = true
|
||||
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"counter",
|
||||
"counter_isomorphic",
|
||||
"counters",
|
||||
"counters_stable",
|
||||
"counter_without_macros",
|
||||
"error_boundary",
|
||||
"errors_axum",
|
||||
"fetch",
|
||||
"hackernews",
|
||||
"hackernews_axum",
|
||||
"js-framework-benchmark",
|
||||
"leptos-tailwind-axum",
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"session_auth_axum",
|
||||
"slots",
|
||||
"ssr_modes",
|
||||
"ssr_modes_axum",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
"animated_show",
|
||||
"counter",
|
||||
"counter_isomorphic",
|
||||
"counters",
|
||||
"counters_stable",
|
||||
"counter_url_query",
|
||||
"counter_without_macros",
|
||||
"error_boundary",
|
||||
"errors_axum",
|
||||
"fetch",
|
||||
"hackernews",
|
||||
"hackernews_axum",
|
||||
"js-framework-benchmark",
|
||||
"leptos-tailwind-axum",
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"session_auth_axum",
|
||||
"slots",
|
||||
"ssr_modes",
|
||||
"ssr_modes_axum",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
]
|
||||
|
||||
[tasks.gen-members]
|
||||
@@ -45,3 +47,65 @@ grep -v gtk |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
|
||||
'''
|
||||
|
||||
[tasks.test-runner-report]
|
||||
workspace = false
|
||||
description = "report ci test runners for each example - OPTION: [all]"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
ITALIC="\e[3m"
|
||||
YELLOW="\e[0;33m"
|
||||
RESET="\e[0m"
|
||||
|
||||
echo
|
||||
echo "${YELLOW}Test Runner Report${RESET}"
|
||||
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
|
||||
echo
|
||||
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
|
||||
sed 's%./%%' |
|
||||
sed 's%/Makefile.toml%%' |
|
||||
grep -v Makefile.toml |
|
||||
sort -u)
|
||||
|
||||
start_path=$(pwd)
|
||||
|
||||
for path in $makefile_paths; do
|
||||
cd $path
|
||||
|
||||
test_runner=
|
||||
|
||||
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
|
||||
if [ $test_count -gt 0 ]; then
|
||||
test_runner="-C"
|
||||
fi
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"wasm-test.toml"*)
|
||||
test_runner=$test_runner"-W"
|
||||
;;
|
||||
*"playwright-test.toml"*)
|
||||
test_runner=$test_runner"-P"
|
||||
;;
|
||||
*"cargo-leptos-test.toml"*)
|
||||
test_runner=$test_runner"-L"
|
||||
;;
|
||||
esac
|
||||
done <"./Makefile.toml"
|
||||
|
||||
if [ ! -z "$1" ]; then
|
||||
# Show all examples
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
elif [ ! -z $test_runner ]; then
|
||||
# Filter out examples that do not run tests in `ci`
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
fi
|
||||
|
||||
cd ${start_path}
|
||||
done
|
||||
echo
|
||||
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
|
||||
echo
|
||||
'''
|
||||
|
||||
14
examples/animated_show/Cargo.toml
Normal file
14
examples/animated_show/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "animated-show"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
1
examples/animated_show/Makefile.toml
Normal file
1
examples/animated_show/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
9
examples/animated_show/README.md
Normal file
9
examples/animated_show/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# `<AnimatedShow>` combined with CSS animations
|
||||
|
||||
This is a very simple example of the `<AnimatedShow>` component.
|
||||
|
||||
This component is an extension for the `<Show>` component and it will not take in a fallback, but it will unmount the
|
||||
component from the DOM after a given duration. This makes it possible to have really easy unmount animations with just
|
||||
CSS.
|
||||
|
||||
Just execute `trunk serve` to start the demo.
|
||||
42
examples/animated_show/index.html
Normal file
42
examples/animated_show/index.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
<style>
|
||||
.hover-me {
|
||||
width: 100px;
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border: 1px solid grey;
|
||||
}
|
||||
.here-i-am {
|
||||
width: 100px;
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
background: black;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
.fade-in-1000 {
|
||||
animation: 1000ms fade-in forwards;
|
||||
}
|
||||
.fade-out-1000 {
|
||||
animation: 1000ms fade-out forwards;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
BIN
examples/animated_show/public/favicon.ico
Normal file
BIN
examples/animated_show/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
34
examples/animated_show/src/lib.rs
Normal file
34
examples/animated_show/src/lib.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use core::time::Duration;
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let show = create_rw_signal(cx, false);
|
||||
|
||||
// the CSS classes in this example are just written directly inside the `index.html`
|
||||
view! { cx,
|
||||
<div
|
||||
class="hover-me"
|
||||
on:mouseenter=move |_| show.set(true)
|
||||
on:mouseleave=move |_| show.set(false)
|
||||
>
|
||||
"Hover Me"
|
||||
</div>
|
||||
|
||||
<AnimatedShow
|
||||
when=show
|
||||
// optional CSS class which will be applied if `when == true`
|
||||
show_class="fade-in-1000"
|
||||
// optional CSS class which will be applied if `when == false` and before the
|
||||
// `hide_delay` starts -> makes CSS unmount animations really easy
|
||||
hide_class="fade-out-1000"
|
||||
// the given unmount delay which should match your unmount animation duration
|
||||
hide_delay=Duration::from_millis(1000)
|
||||
>
|
||||
// provide any `Children` inside here
|
||||
<div class="here-i-am">
|
||||
"Here I Am!"
|
||||
</div>
|
||||
</AnimatedShow>
|
||||
}
|
||||
}
|
||||
12
examples/animated_show/src/main.rs
Normal file
12
examples/animated_show/src/main.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use animated_show::App;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
<App />
|
||||
}
|
||||
})
|
||||
}
|
||||
11
examples/cargo-make/compile.toml
Normal file
11
examples/cargo-make/compile.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[tasks.build]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -1,4 +1,5 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/compile.toml" },
|
||||
{ path = "../cargo-make/clean.toml" },
|
||||
{ path = "../cargo-make/lint.toml" },
|
||||
{ path = "../cargo-make/node.toml" },
|
||||
@@ -9,9 +10,6 @@ extend = [
|
||||
[tasks.ci]
|
||||
dependencies = ["prepare", "lint", "build", "test-flow", "integration-test"]
|
||||
|
||||
[tasks.ci-clean]
|
||||
dependencies = ["ci", "clean"]
|
||||
|
||||
[tasks.prepare]
|
||||
dependencies = ["setup-node"]
|
||||
|
||||
@@ -20,6 +18,17 @@ dependencies = ["check-style"]
|
||||
|
||||
[tasks.integration-test]
|
||||
|
||||
# Support Local Runs
|
||||
|
||||
[tasks.ci-clean]
|
||||
dependencies = ["ci", "clean"]
|
||||
|
||||
[tasks.check-clean]
|
||||
dependencies = ["check", "clean"]
|
||||
|
||||
[tasks.build-clean]
|
||||
dependencies = ["build", "clean"]
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.verify-flow]
|
||||
|
||||
@@ -2,13 +2,3 @@ extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
20
examples/counter_url_query/Cargo.toml
Normal file
20
examples/counter_url_query/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "counter_url_query"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
web-sys = "0.3"
|
||||
1
examples/counter_url_query/Makefile.toml
Normal file
1
examples/counter_url_query/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
7
examples/counter_url_query/README.md
Normal file
7
examples/counter_url_query/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Leptos Query Counter Example
|
||||
|
||||
This example creates a simple counter whose state is persisted and synced in the url with query params.
|
||||
|
||||
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.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
8
examples/counter_url_query/index.html
Normal file
8
examples/counter_url_query/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
BIN
examples/counter_url_query/public/favicon.ico
Normal file
BIN
examples/counter_url_query/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
39
examples/counter_url_query/src/lib.rs
Normal file
39
examples/counter_url_query/src/lib.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
/// A simple counter component.
|
||||
///
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleQueryCounter(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_query_signal::<i32>(cx, "count");
|
||||
let clear = move |_| set_count(None);
|
||||
let decrement = move |_| set_count(Some(count().unwrap_or(0) - 1));
|
||||
let increment = move |_| set_count(Some(count().unwrap_or(0) + 1));
|
||||
|
||||
let (msg, set_msg) = create_query_signal::<String>(cx, "message");
|
||||
let update_msg = move |ev| {
|
||||
let new_msg = event_target_value(&ev);
|
||||
if new_msg.is_empty() {
|
||||
set_msg(None);
|
||||
} else {
|
||||
set_msg(Some(new_msg));
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
<span>"Value: " {move || count().unwrap_or(0)} "!"</span>
|
||||
<button on:click=increment>"+1"</button>
|
||||
|
||||
<br />
|
||||
|
||||
<input
|
||||
prop:value=move || msg().unwrap_or_default()
|
||||
on:input=update_msg
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
17
examples/counter_url_query/src/main.rs
Normal file
17
examples/counter_url_query/src/main.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use counter_url_query::SimpleQueryCounter;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="" view=SimpleQueryCounter />
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,11 +4,13 @@ extend = [
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["+stable", "build-all-features"]
|
||||
args = ["build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["+stable", "check-all-features"]
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -2,13 +2,3 @@ extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -6,6 +6,13 @@ extend = [
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["+stable", "build-all-features"]
|
||||
args = ["build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -4,13 +4,15 @@ extend = [
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
args = ["build-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
args = ["check-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.pre-clippy]
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -3,13 +3,3 @@ extend = [
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -2,13 +2,3 @@ extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = ["check-debug", "check-release"]
|
||||
|
||||
[tasks.check-debug]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-release]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features", "--release"]
|
||||
args = ["check-all-features", "--release"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -3,13 +3,3 @@ extend = { path = "../cargo-make/main.toml" }
|
||||
[tasks.setup-node]
|
||||
env = { SETUP_NODE = false }
|
||||
condition = { env_true = ["SETUP_NODE"] }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -46,6 +46,7 @@ ssr = [
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
"server_fn/ssr",
|
||||
]
|
||||
nightly = [
|
||||
"leptos_dom/nightly",
|
||||
|
||||
111
leptos/src/animated_show.rs
Normal file
111
leptos/src/animated_show.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use crate::Show;
|
||||
use core::time::Duration;
|
||||
use leptos::component;
|
||||
use leptos_dom::{helpers::TimeoutHandle, Fragment, IntoView};
|
||||
use leptos_macro::view;
|
||||
use leptos_reactive::{
|
||||
create_effect, on_cleanup, signal_prelude::*, store_value, Scope,
|
||||
StoredValue,
|
||||
};
|
||||
|
||||
/// A component that will show its children when the `when` condition is `true`.
|
||||
/// Additionally, you need to specify a `hide_delay`. If the `when` condition changes to `false`,
|
||||
/// the unmounting of the children will be delayed by the specified Duration.
|
||||
/// If you provide the optional `show_class` and `hide_class`, you can create very easy mount /
|
||||
/// unmount animations.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use core::time::Duration;
|
||||
/// # use leptos::*;
|
||||
/// # #[component]
|
||||
/// # pub fn App(cx: Scope) -> impl IntoView {
|
||||
/// let show = create_rw_signal(cx, false);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <div
|
||||
/// class="hover-me"
|
||||
/// on:mouseenter=move |_| show.set(true)
|
||||
/// on:mouseleave=move |_| show.set(false)
|
||||
/// >
|
||||
/// "Hover Me"
|
||||
/// </div>
|
||||
///
|
||||
/// <AnimatedShow
|
||||
/// when=show
|
||||
/// show_class="fade-in-1000"
|
||||
/// hide_class="fade-out-1000"
|
||||
/// hide_delay=Duration::from_millis(1000)
|
||||
/// >
|
||||
/// <div class="here-i-am">
|
||||
/// "Here I Am!"
|
||||
/// </div>
|
||||
/// </AnimatedShow>
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
)]
|
||||
#[component]
|
||||
pub fn AnimatedShow(
|
||||
/// The scope the component is running in
|
||||
cx: Scope,
|
||||
/// The components Show wraps
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
/// If the component should show or not
|
||||
#[prop(into)]
|
||||
when: MaybeSignal<bool>,
|
||||
/// Optional CSS class to apply if `when == true`
|
||||
#[prop(optional)]
|
||||
show_class: &'static str,
|
||||
/// Optional CSS class to apply if `when == false`
|
||||
#[prop(optional)]
|
||||
hide_class: &'static str,
|
||||
/// The timeout after which the component will be unmounted if `when == false`
|
||||
hide_delay: Duration,
|
||||
) -> impl IntoView {
|
||||
let handle: StoredValue<Option<TimeoutHandle>> = store_value(cx, None);
|
||||
let cls = create_rw_signal(
|
||||
cx,
|
||||
if when.get_untracked() {
|
||||
show_class
|
||||
} else {
|
||||
hide_class
|
||||
},
|
||||
);
|
||||
let show = create_rw_signal(cx, when.get_untracked());
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
if when.get() {
|
||||
// clear any possibly active timer
|
||||
if let Some(h) = handle.get_value() {
|
||||
h.clear();
|
||||
}
|
||||
|
||||
cls.set(show_class);
|
||||
show.set(true);
|
||||
} else {
|
||||
cls.set(hide_class);
|
||||
|
||||
let h = leptos_dom::helpers::set_timeout_with_handle(
|
||||
move || show.set(false),
|
||||
hide_delay,
|
||||
)
|
||||
.expect("set timeout in AnimatedShow");
|
||||
handle.set_value(Some(h));
|
||||
}
|
||||
});
|
||||
|
||||
on_cleanup(cx, move || {
|
||||
if let Some(Some(h)) = handle.try_get_value() {
|
||||
h.clear();
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<Show when=move || show.get() fallback=|_| ()>
|
||||
<div class=move || cls.get()>{children(cx)}</div>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -156,18 +156,10 @@ pub mod ssr {
|
||||
pub use leptos_dom::{ssr::*, ssr_in_order::*};
|
||||
}
|
||||
pub use leptos_dom::{
|
||||
self, create_node_ref, debug_warn, document, error, ev,
|
||||
helpers::{
|
||||
event_target, event_target_checked, event_target_value,
|
||||
request_animation_frame, request_animation_frame_with_handle,
|
||||
request_idle_callback, request_idle_callback_with_handle, set_interval,
|
||||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener, window_event_listener_untyped,
|
||||
},
|
||||
html, log, math, mount_to, mount_to_body, nonce, svg, warn, window,
|
||||
Attribute, Class, CollectView, Errors, Fragment, HtmlElement,
|
||||
IntoAttribute, IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef,
|
||||
Property, View,
|
||||
self, create_node_ref, debug_warn, document, error, ev, helpers::*, html,
|
||||
log, math, mount_to, mount_to_body, nonce, svg, warn, window, Attribute,
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
|
||||
/// Types to make it easier to handle errors in your application.
|
||||
@@ -189,12 +181,14 @@ pub use typed_builder;
|
||||
pub use {leptos_macro::template, wasm_bindgen, web_sys};
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
mod animated_show;
|
||||
mod for_loop;
|
||||
mod show;
|
||||
pub use animated_show::*;
|
||||
pub use for_loop::*;
|
||||
pub use show::*;
|
||||
mod suspense_component;
|
||||
pub use suspense_component::*;
|
||||
mod suspense_component;
|
||||
mod text_prop;
|
||||
mod transition;
|
||||
pub use text_prop::TextProp;
|
||||
|
||||
@@ -72,9 +72,6 @@ where
|
||||
let current_id = HydrationCtx::next_component();
|
||||
|
||||
let child = DynChild::new({
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let current_id = current_id;
|
||||
|
||||
let children = Rc::new(orig_children(cx).into_view(cx));
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let orig_children = Rc::clone(&orig_children);
|
||||
|
||||
@@ -222,16 +222,31 @@ where
|
||||
// node
|
||||
let ret = if let Some(prev_t) = prev_t {
|
||||
// Here, our child is also a text node
|
||||
if let Some(new_t) = new_child.get_text() {
|
||||
|
||||
// nb: the match/ownership gymnastics here
|
||||
// are so that, if we can reuse the text node,
|
||||
// we can take ownership of new_t so we don't clone
|
||||
// the contents, which in O(n) on the length of the text
|
||||
if matches!(new_child, View::Text(_)) {
|
||||
if !was_child_moved && child != new_child {
|
||||
let mut new_t = match new_child {
|
||||
View::Text(t) => t,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
prev_t
|
||||
.unchecked_ref::<web_sys::Text>()
|
||||
.set_data(&new_t.content);
|
||||
|
||||
// replace new_t's text node with the prev node
|
||||
// see discussion: https://github.com/leptos-rs/leptos/pull/1472
|
||||
new_t.node = prev_t.clone();
|
||||
|
||||
let new_child = View::Text(new_t);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(Some(prev_t), disposer)
|
||||
} else {
|
||||
let new_t = new_child.as_text().unwrap();
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
|
||||
@@ -377,6 +377,9 @@ where
|
||||
|
||||
let component = EachRepr::default();
|
||||
|
||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
let opening = component.opening.node.clone().unchecked_into();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let (children, closing) =
|
||||
(component.children.clone(), component.closing.node.clone());
|
||||
@@ -387,7 +390,11 @@ where
|
||||
move |prev_hash_run: Option<HashRun<FxIndexSet<K>>>| {
|
||||
let mut children_borrow = children.borrow_mut();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[cfg(all(
|
||||
not(debug_assertions),
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let opening = if let Some(Some(child)) = children_borrow.get(0)
|
||||
{
|
||||
// correctly remove opening <!--<EachItem/>-->
|
||||
|
||||
@@ -1073,7 +1073,6 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
let id = *element.hydration_id();
|
||||
|
||||
let mut element = Element::new(element);
|
||||
let children = children;
|
||||
|
||||
if attrs.iter_mut().any(|(name, _)| name == "id") {
|
||||
attrs.push(("leptos-hk".into(), format!("_{id}").into()));
|
||||
|
||||
@@ -6,6 +6,9 @@ mod hydration {
|
||||
use std::{cell::RefCell, collections::HashMap};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// See ["createTreeWalker"](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker)
|
||||
const FILTER_SHOW_COMMENT: u32 = 0b10000000;
|
||||
|
||||
// We can tell if we start in hydration mode by checking to see if the
|
||||
// id "_0-1" is present in the DOM. If it is, we know we are hydrating from
|
||||
// the server, if not, we are starting off in CSR
|
||||
@@ -14,7 +17,7 @@ mod hydration {
|
||||
let document = crate::document();
|
||||
let body = document.body().unwrap();
|
||||
let walker = document
|
||||
.create_tree_walker_with_what_to_show(&body, 128)
|
||||
.create_tree_walker_with_what_to_show(&body, FILTER_SHOW_COMMENT)
|
||||
.unwrap();
|
||||
let mut map = HashMap::new();
|
||||
while let Ok(Some(node)) = walker.next_node() {
|
||||
@@ -34,7 +37,7 @@ mod hydration {
|
||||
let document = crate::document();
|
||||
let body = document.body().unwrap();
|
||||
let walker = document
|
||||
.create_tree_walker_with_what_to_show(&body, 128)
|
||||
.create_tree_walker_with_what_to_show(&body, FILTER_SHOW_COMMENT)
|
||||
.unwrap();
|
||||
let mut map = HashMap::new();
|
||||
while let Ok(Some(node)) = walker.next_node() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::incorrect_clone_impl_on_copy_type)]
|
||||
#![deny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
@@ -38,7 +37,7 @@ pub use hydration::{HydrationCtx, HydrationKey};
|
||||
use leptos_reactive::Scope;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
pub use logging::*;
|
||||
pub use macro_helpers::*;
|
||||
@@ -212,6 +211,20 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoView for MaybeProp<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", name = "MaybeSignal<T>", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects an iterator or collection into a [`View`].
|
||||
pub trait CollectView {
|
||||
/// Collects an iterator or collection into a [`View`].
|
||||
@@ -273,8 +286,12 @@ cfg_if! {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
let attrs =
|
||||
self.attrs.iter().map(|(n, v)| format!(" {n}=\"{v}\"")).collect::<String>();
|
||||
let attrs = self.attrs.iter().fold(String::new(), |mut output, (n, v)| {
|
||||
// can safely ignore output
|
||||
// see https://rust-lang.github.io/rust-clippy/master/index.html#/format_collect
|
||||
let _ = write!(output, " {n}=\"{v}\"");
|
||||
output
|
||||
});
|
||||
|
||||
if self.is_void {
|
||||
write!(f, "<{}{attrs} />", self.name)
|
||||
|
||||
@@ -147,7 +147,7 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
|
||||
|
||||
impl<T: ElementDescriptor> Clone for NodeRef<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -405,11 +405,14 @@ impl View {
|
||||
self,
|
||||
dont_escape_text: bool,
|
||||
) -> Cow<'static, str> {
|
||||
println!("render_to_string_helper {:?}", self);
|
||||
match self {
|
||||
View::Text(node) => {
|
||||
if dont_escape_text {
|
||||
println!("don't escape {:?}", node.content);
|
||||
node.content
|
||||
} else {
|
||||
println!("encode_safe {:?}", node.content);
|
||||
html_escape::encode_safe(&node.content).to_string().into()
|
||||
}
|
||||
}
|
||||
@@ -492,9 +495,17 @@ impl View {
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{}", t.content).into()
|
||||
format!(
|
||||
"<!>{}",
|
||||
html_escape::encode_safe(
|
||||
&t.content
|
||||
)
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
t.content
|
||||
html_escape::encode_safe(&t.content)
|
||||
.to_string()
|
||||
.into()
|
||||
}
|
||||
} else {
|
||||
child.render_to_string_helper(
|
||||
|
||||
@@ -438,12 +438,16 @@ impl View {
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!>{}",
|
||||
content
|
||||
html_escape::encode_safe(
|
||||
&content
|
||||
)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(content)
|
||||
StreamChunk::Sync(html_escape::encode_safe(
|
||||
&content
|
||||
).to_string().into())
|
||||
},
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,7 @@ syn = { version = "2", features = [
|
||||
"printing",
|
||||
] }
|
||||
quote = "1"
|
||||
rstml = "0.10.6"
|
||||
rstml = "0.11.0"
|
||||
proc-macro2 = { version = "1", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12"
|
||||
walkdir = "2"
|
||||
|
||||
@@ -21,7 +21,7 @@ proc-macro-error = "1"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
rstml = "0.10.6"
|
||||
rstml = "0.11.0"
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
@@ -35,10 +35,9 @@ trybuild = "1"
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
csr = []
|
||||
hydrate = []
|
||||
ssr = []
|
||||
ssr = ["server_fn_macro/ssr"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = []
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ pub fn TestComponent(
|
||||
/// # use example::TestComponent;
|
||||
/// <TestComponent key="hello"/>
|
||||
#[prop(optional)]
|
||||
another:usize,
|
||||
another: usize,
|
||||
/// rust unclosed
|
||||
/// ```view
|
||||
/// use example::TestComponent;
|
||||
@@ -38,3 +38,22 @@ pub fn TestComponent(
|
||||
_ = (key, another, and_another);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TestMutCallback<'a, F>(
|
||||
cx: Scope,
|
||||
mut callback: F,
|
||||
value: &'a str,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: FnMut(u32) + 'static,
|
||||
{
|
||||
let value = value.to_owned();
|
||||
view! { cx,
|
||||
<button on:click=move |_| {
|
||||
callback(5);
|
||||
}>
|
||||
{value}
|
||||
</button>
|
||||
<TestComponent key="test"/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +284,8 @@ impl ToTokens for Model {
|
||||
) #ret #(+ #lifetimes)*
|
||||
#where_clause
|
||||
{
|
||||
// allowed for lifetimes that are needed for props struct
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
#body
|
||||
|
||||
#destructure_props
|
||||
@@ -589,12 +591,14 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
quote!()
|
||||
};
|
||||
|
||||
let PatIdent { ident, by_ref, .. } = &name;
|
||||
|
||||
quote! {
|
||||
#docs
|
||||
#builder_docs
|
||||
#builder_attrs
|
||||
#allow_missing_docs
|
||||
#vis #name: #ty,
|
||||
#vis #by_ref #ident: #ty,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -604,7 +608,12 @@ fn prop_names(props: &[Prop]) -> TokenStream {
|
||||
props
|
||||
.iter()
|
||||
.filter(|Prop { ty, .. }| !is_valid_scope_type(ty))
|
||||
.map(|Prop { name, .. }| quote! { #name, })
|
||||
.map(|Prop { name, .. }| {
|
||||
// fields like mutability are removed because unneeded
|
||||
// in the contexts in which this is used
|
||||
let ident = &name.ident;
|
||||
quote! { #ident, }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -169,14 +169,26 @@ mod template;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// Class names can include dashes, but cannot (at the moment) include a dash-separated segment of only numbers.
|
||||
/// Class names can include dashes, and since leptos v0.5.0 can include a dash-separated segment of only numbers.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (count, set_count) = create_signal(cx, 2);
|
||||
/// view! { cx, <div class:hidden-div-25={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// Class names cannot include special symbols.
|
||||
/// ```rust,compile_fail
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (count, set_count) = create_signal(cx, 2);
|
||||
/// // `hidden-div-25` is invalid at the moment
|
||||
/// view! { cx, <div class:hidden-div-25={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// // class:hidden-[div]-25 is invalid attribute name
|
||||
/// view! { cx, <div class:hidden-[div]-25={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
@@ -921,8 +933,8 @@ pub fn params_derive(
|
||||
}
|
||||
|
||||
pub(crate) fn attribute_value(attr: &KeyedAttribute) -> &syn::Expr {
|
||||
match &attr.possible_value {
|
||||
Some(value) => &value.value,
|
||||
match attr.value() {
|
||||
Some(value) => value,
|
||||
None => abort!(attr.key, "attribute should have value"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,32 +20,19 @@ enum TagType {
|
||||
Math,
|
||||
}
|
||||
|
||||
// Keep list alphabetized for binary search
|
||||
const TYPED_EVENTS: [&str; 126] = [
|
||||
"afterprint",
|
||||
"beforeprint",
|
||||
"beforeunload",
|
||||
"gamepadconnected",
|
||||
"gamepaddisconnected",
|
||||
"hashchange",
|
||||
"languagechange",
|
||||
"message",
|
||||
"messageerror",
|
||||
"offline",
|
||||
"online",
|
||||
"pagehide",
|
||||
"pageshow",
|
||||
"popstate",
|
||||
"rejectionhandled",
|
||||
"storage",
|
||||
"unhandledrejection",
|
||||
"unload",
|
||||
"DOMContentLoaded",
|
||||
"abort",
|
||||
"afterprint",
|
||||
"animationcancel",
|
||||
"animationend",
|
||||
"animationiteration",
|
||||
"animationstart",
|
||||
"auxclick",
|
||||
"beforeinput",
|
||||
"beforeprint",
|
||||
"beforeunload",
|
||||
"blur",
|
||||
"canplay",
|
||||
"canplaythrough",
|
||||
@@ -56,8 +43,12 @@ const TYPED_EVENTS: [&str; 126] = [
|
||||
"compositionstart",
|
||||
"compositionupdate",
|
||||
"contextmenu",
|
||||
"copy",
|
||||
"cuechange",
|
||||
"cut",
|
||||
"dblclick",
|
||||
"devicemotion",
|
||||
"deviceorientation",
|
||||
"drag",
|
||||
"dragend",
|
||||
"dragenter",
|
||||
@@ -73,17 +64,25 @@ const TYPED_EVENTS: [&str; 126] = [
|
||||
"focusin",
|
||||
"focusout",
|
||||
"formdata",
|
||||
"fullscreenchange",
|
||||
"fullscreenerror",
|
||||
"gamepadconnected",
|
||||
"gamepaddisconnected",
|
||||
"gotpointercapture",
|
||||
"hashchange",
|
||||
"input",
|
||||
"invalid",
|
||||
"keydown",
|
||||
"keypress",
|
||||
"keyup",
|
||||
"languagechange",
|
||||
"load",
|
||||
"loadeddata",
|
||||
"loadedmetadata",
|
||||
"loadstart",
|
||||
"lostpointercapture",
|
||||
"message",
|
||||
"messageerror",
|
||||
"mousedown",
|
||||
"mouseenter",
|
||||
"mouseleave",
|
||||
@@ -91,6 +90,12 @@ const TYPED_EVENTS: [&str; 126] = [
|
||||
"mouseout",
|
||||
"mouseover",
|
||||
"mouseup",
|
||||
"offline",
|
||||
"online",
|
||||
"orientationchange",
|
||||
"pagehide",
|
||||
"pageshow",
|
||||
"paste",
|
||||
"pause",
|
||||
"play",
|
||||
"playing",
|
||||
@@ -98,12 +103,17 @@ const TYPED_EVENTS: [&str; 126] = [
|
||||
"pointerdown",
|
||||
"pointerenter",
|
||||
"pointerleave",
|
||||
"pointerlockchange",
|
||||
"pointerlockerror",
|
||||
"pointermove",
|
||||
"pointerout",
|
||||
"pointerover",
|
||||
"pointerup",
|
||||
"popstate",
|
||||
"progress",
|
||||
"ratechange",
|
||||
"readystatechange",
|
||||
"rejectionhandled",
|
||||
"reset",
|
||||
"resize",
|
||||
"scroll",
|
||||
@@ -115,6 +125,7 @@ const TYPED_EVENTS: [&str; 126] = [
|
||||
"selectstart",
|
||||
"slotchange",
|
||||
"stalled",
|
||||
"storage",
|
||||
"submit",
|
||||
"suspend",
|
||||
"timeupdate",
|
||||
@@ -127,6 +138,9 @@ const TYPED_EVENTS: [&str; 126] = [
|
||||
"transitionend",
|
||||
"transitionrun",
|
||||
"transitionstart",
|
||||
"unhandledrejection",
|
||||
"unload",
|
||||
"visibilitychange",
|
||||
"volumechange",
|
||||
"waiting",
|
||||
"webkitanimationend",
|
||||
@@ -134,21 +148,10 @@ const TYPED_EVENTS: [&str; 126] = [
|
||||
"webkitanimationstart",
|
||||
"webkittransitionend",
|
||||
"wheel",
|
||||
"DOMContentLoaded",
|
||||
"devicemotion",
|
||||
"deviceorientation",
|
||||
"orientationchange",
|
||||
"copy",
|
||||
"cut",
|
||||
"paste",
|
||||
"fullscreenchange",
|
||||
"fullscreenerror",
|
||||
"pointerlockchange",
|
||||
"pointerlockerror",
|
||||
"readystatechange",
|
||||
"visibilitychange",
|
||||
];
|
||||
|
||||
const CUSTOM_EVENT: &str = "Custom";
|
||||
|
||||
pub(crate) fn render_view(
|
||||
cx: &Ident,
|
||||
nodes: &[Node],
|
||||
@@ -348,7 +351,7 @@ fn root_element_to_tokens_ssr(
|
||||
// We can use open_tag.span(), to provide simmilar(to name span) diagnostic
|
||||
// in case of expansion error, but it will also higlight "<" token.
|
||||
let typed_element_name = if is_custom_element {
|
||||
Ident::new("Custom", Span::call_site())
|
||||
Ident::new(CUSTOM_EVENT, Span::call_site())
|
||||
} else {
|
||||
let camel_cased = camel_case_tag_name(
|
||||
tag_name
|
||||
@@ -1239,7 +1242,7 @@ fn attribute_to_tokens(
|
||||
};
|
||||
let undelegated_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => parts.last().and_then(|last| {
|
||||
if last == "undelegated" {
|
||||
if last.to_string() == "undelegated" {
|
||||
Some(last)
|
||||
} else {
|
||||
None
|
||||
@@ -1260,9 +1263,8 @@ fn attribute_to_tokens(
|
||||
let event_type = if is_custom {
|
||||
event_type
|
||||
} else if let Some(ev_name) = event_name_ident {
|
||||
let span = ev_name.span();
|
||||
quote_spanned! {
|
||||
span => #ev_name
|
||||
quote! {
|
||||
#ev_name
|
||||
}
|
||||
} else {
|
||||
event_type
|
||||
@@ -1270,9 +1272,8 @@ fn attribute_to_tokens(
|
||||
|
||||
let event_type = if is_force_undelegated {
|
||||
let undelegated = if let Some(undelegated) = undelegated_ident {
|
||||
let span = undelegated.span();
|
||||
quote_spanned! {
|
||||
span => #undelegated
|
||||
quote! {
|
||||
#undelegated
|
||||
}
|
||||
} else {
|
||||
quote! { undelegated }
|
||||
@@ -1380,12 +1381,10 @@ fn attribute_to_tokens(
|
||||
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 (event_type, is_custom) = TYPED_EVENTS
|
||||
.binary_search(&name)
|
||||
.map(|_| (name, false))
|
||||
.unwrap_or((CUSTOM_EVENT, true));
|
||||
|
||||
let Ok(event_type) = event_type.parse::<TokenStream>() else {
|
||||
abort!(event_type, "couldn't parse event name");
|
||||
@@ -1713,8 +1712,10 @@ pub(crate) fn component_to_tokens(
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
IdeTagHelper::add_component_completion(&mut component, node);
|
||||
// (Temporarily?) removed
|
||||
// See note on the function itself below.
|
||||
/* #[cfg(debug_assertions)]
|
||||
IdeTagHelper::add_component_completion(cx, &mut component, node); */
|
||||
|
||||
if events.is_empty() {
|
||||
component
|
||||
@@ -1743,10 +1744,9 @@ pub(crate) fn event_from_attribute_node(
|
||||
let (name, name_undelegated) = parse_event(&event_name);
|
||||
|
||||
let event_type = TYPED_EVENTS
|
||||
.iter()
|
||||
.find(|e| **e == name)
|
||||
.copied()
|
||||
.unwrap_or("Custom");
|
||||
.binary_search(&name)
|
||||
.map(|_| (name))
|
||||
.unwrap_or(CUSTOM_EVENT);
|
||||
|
||||
let Ok(event_type) = event_type.parse::<TokenStream>() else {
|
||||
abort!(attr.key, "couldn't parse event name");
|
||||
@@ -1833,23 +1833,13 @@ fn is_custom_element(tag: &str) -> bool {
|
||||
fn is_self_closing(node: &NodeElement) -> bool {
|
||||
// self-closing tags
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
||||
matches!(
|
||||
node.name().to_string().as_str(),
|
||||
"area"
|
||||
| "base"
|
||||
| "br"
|
||||
| "col"
|
||||
| "embed"
|
||||
| "hr"
|
||||
| "img"
|
||||
| "input"
|
||||
| "link"
|
||||
| "meta"
|
||||
| "param"
|
||||
| "source"
|
||||
| "track"
|
||||
| "wbr"
|
||||
)
|
||||
// Keep list alphabetized for binary search
|
||||
[
|
||||
"area", "base", "br", "col", "embed", "hr", "img", "input", "link",
|
||||
"meta", "param", "source", "track", "wbr",
|
||||
]
|
||||
.binary_search(&node.name().to_string().as_str())
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn camel_case_tag_name(tag_name: &str) -> String {
|
||||
@@ -1865,109 +1855,113 @@ fn camel_case_tag_name(tag_name: &str) -> String {
|
||||
}
|
||||
|
||||
fn is_svg_element(tag: &str) -> bool {
|
||||
matches!(
|
||||
tag,
|
||||
"animate"
|
||||
| "animateMotion"
|
||||
| "animateTransform"
|
||||
| "circle"
|
||||
| "clipPath"
|
||||
| "defs"
|
||||
| "desc"
|
||||
| "discard"
|
||||
| "ellipse"
|
||||
| "feBlend"
|
||||
| "feColorMatrix"
|
||||
| "feComponentTransfer"
|
||||
| "feComposite"
|
||||
| "feConvolveMatrix"
|
||||
| "feDiffuseLighting"
|
||||
| "feDisplacementMap"
|
||||
| "feDistantLight"
|
||||
| "feDropShadow"
|
||||
| "feFlood"
|
||||
| "feFuncA"
|
||||
| "feFuncB"
|
||||
| "feFuncG"
|
||||
| "feFuncR"
|
||||
| "feGaussianBlur"
|
||||
| "feImage"
|
||||
| "feMerge"
|
||||
| "feMergeNode"
|
||||
| "feMorphology"
|
||||
| "feOffset"
|
||||
| "fePointLight"
|
||||
| "feSpecularLighting"
|
||||
| "feSpotLight"
|
||||
| "feTile"
|
||||
| "feTurbulence"
|
||||
| "filter"
|
||||
| "foreignObject"
|
||||
| "g"
|
||||
| "hatch"
|
||||
| "hatchpath"
|
||||
| "image"
|
||||
| "line"
|
||||
| "linearGradient"
|
||||
| "marker"
|
||||
| "mask"
|
||||
| "metadata"
|
||||
| "mpath"
|
||||
| "path"
|
||||
| "pattern"
|
||||
| "polygon"
|
||||
| "polyline"
|
||||
| "radialGradient"
|
||||
| "rect"
|
||||
| "set"
|
||||
| "stop"
|
||||
| "svg"
|
||||
| "switch"
|
||||
| "symbol"
|
||||
| "text"
|
||||
| "textPath"
|
||||
| "tspan"
|
||||
| "use"
|
||||
| "use_"
|
||||
| "view"
|
||||
)
|
||||
// Keep list alphabetized for binary search
|
||||
[
|
||||
"animate",
|
||||
"animateMotion",
|
||||
"animateTransform",
|
||||
"circle",
|
||||
"clipPath",
|
||||
"defs",
|
||||
"desc",
|
||||
"discard",
|
||||
"ellipse",
|
||||
"feBlend",
|
||||
"feColorMatrix",
|
||||
"feComponentTransfer",
|
||||
"feComposite",
|
||||
"feConvolveMatrix",
|
||||
"feDiffuseLighting",
|
||||
"feDisplacementMap",
|
||||
"feDistantLight",
|
||||
"feDropShadow",
|
||||
"feFlood",
|
||||
"feFuncA",
|
||||
"feFuncB",
|
||||
"feFuncG",
|
||||
"feFuncR",
|
||||
"feGaussianBlur",
|
||||
"feImage",
|
||||
"feMerge",
|
||||
"feMergeNode",
|
||||
"feMorphology",
|
||||
"feOffset",
|
||||
"fePointLight",
|
||||
"feSpecularLighting",
|
||||
"feSpotLight",
|
||||
"feTile",
|
||||
"feTurbulence",
|
||||
"filter",
|
||||
"foreignObject",
|
||||
"g",
|
||||
"hatch",
|
||||
"hatchpath",
|
||||
"image",
|
||||
"line",
|
||||
"linearGradient",
|
||||
"marker",
|
||||
"mask",
|
||||
"metadata",
|
||||
"mpath",
|
||||
"path",
|
||||
"pattern",
|
||||
"polygon",
|
||||
"polyline",
|
||||
"radialGradient",
|
||||
"rect",
|
||||
"set",
|
||||
"stop",
|
||||
"svg",
|
||||
"switch",
|
||||
"symbol",
|
||||
"text",
|
||||
"textPath",
|
||||
"tspan",
|
||||
"use",
|
||||
"use_",
|
||||
"view",
|
||||
]
|
||||
.binary_search(&tag)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn is_math_ml_element(tag: &str) -> bool {
|
||||
matches!(
|
||||
tag,
|
||||
"math"
|
||||
| "mi"
|
||||
| "mn"
|
||||
| "mo"
|
||||
| "ms"
|
||||
| "mspace"
|
||||
| "mtext"
|
||||
| "menclose"
|
||||
| "merror"
|
||||
| "mfenced"
|
||||
| "mfrac"
|
||||
| "mpadded"
|
||||
| "mphantom"
|
||||
| "mroot"
|
||||
| "mrow"
|
||||
| "msqrt"
|
||||
| "mstyle"
|
||||
| "mmultiscripts"
|
||||
| "mover"
|
||||
| "mprescripts"
|
||||
| "msub"
|
||||
| "msubsup"
|
||||
| "msup"
|
||||
| "munder"
|
||||
| "munderover"
|
||||
| "mtable"
|
||||
| "mtd"
|
||||
| "mtr"
|
||||
| "maction"
|
||||
| "annotation"
|
||||
| "semantics"
|
||||
)
|
||||
// Keep list alphabetized for binary search
|
||||
[
|
||||
"annotation",
|
||||
"maction",
|
||||
"math",
|
||||
"menclose",
|
||||
"merror",
|
||||
"mfenced",
|
||||
"mfrac",
|
||||
"mi",
|
||||
"mmultiscripts",
|
||||
"mn",
|
||||
"mo",
|
||||
"mover",
|
||||
"mpadded",
|
||||
"mphantom",
|
||||
"mprescripts",
|
||||
"mroot",
|
||||
"mrow",
|
||||
"ms",
|
||||
"mspace",
|
||||
"msqrt",
|
||||
"mstyle",
|
||||
"msub",
|
||||
"msubsup",
|
||||
"msup",
|
||||
"mtable",
|
||||
"mtd",
|
||||
"mtext",
|
||||
"mtr",
|
||||
"munder",
|
||||
"munderover",
|
||||
"semantics",
|
||||
]
|
||||
.binary_search(&tag)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn is_ambiguous_element(tag: &str) -> bool {
|
||||
@@ -2118,6 +2112,19 @@ impl IdeTagHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/* This has been (temporarily?) removed.
|
||||
* Its purpose was simply to add syntax highlighting and IDE hints for
|
||||
* component closing tags in debug mode by associating the closing tag
|
||||
* ident with the component function.
|
||||
*
|
||||
* Doing this in a way that correctly inferred types, however, required
|
||||
* duplicating the entire component constructor.
|
||||
*
|
||||
* In view trees with many nested components, this led to a massive explosion
|
||||
* in compile times.
|
||||
*
|
||||
* See https://github.com/leptos-rs/leptos/issues/1283
|
||||
*
|
||||
/// Add completion to the closing tag of the component.
|
||||
///
|
||||
/// In order to ensure that generics are passed through correctly in the
|
||||
@@ -2134,22 +2141,21 @@ impl IdeTagHelper {
|
||||
/// ```
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn add_component_completion(
|
||||
cx: &Ident,
|
||||
component: &mut TokenStream,
|
||||
node: &NodeElement,
|
||||
) {
|
||||
// emit ide helper info
|
||||
if node.close_tag.is_some() {
|
||||
let constructor = component.clone();
|
||||
if let Some(close_tag) = node.close_tag.as_ref().map(|c| &c.name) {
|
||||
*component = quote! {
|
||||
if false {
|
||||
#[allow(unreachable_code)]
|
||||
#constructor
|
||||
} else {
|
||||
#component
|
||||
{
|
||||
let #close_tag = |cx| #component;
|
||||
#close_tag(#cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// Returns `syn::Path`-like `TokenStream` to the fn in docs.
|
||||
/// If tag name is `Component` returns `None`.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::incorrect_clone_impl_on_copy_type)]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||
|
||||
@@ -179,13 +179,7 @@ where
|
||||
T: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -734,19 +734,8 @@ where
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
source_ty: PhantomData,
|
||||
out_ty: PhantomData,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -370,7 +370,7 @@ fn push_cleanup(cx: Scope, cleanup_fn: Box<dyn FnOnce()>) {
|
||||
let cleanups = cleanups
|
||||
.entry(cx.id)
|
||||
.expect("trying to clean up a Scope that has already been disposed")
|
||||
.or_insert_with(Default::default);
|
||||
.or_default();
|
||||
cleanups.push(cleanup_fn);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2059,11 +2059,11 @@ impl NodeId {
|
||||
debug_warn!(
|
||||
"[Signal::update] You’re trying to update a \
|
||||
Signal<{}> (defined at {defined_at}) that has \
|
||||
already been disposed of. This is probably \
|
||||
either a logic error in a component that creates \
|
||||
and disposes of scopes. If it does cause cause \
|
||||
any issues, it is safe to ignore this warning, \
|
||||
which occurs only in debug mode.",
|
||||
already been disposed of. This is probably a \
|
||||
logic error in a component that creates and \
|
||||
disposes of scopes. If it does not cause any \
|
||||
issues, it is safe to ignore this warning, which \
|
||||
occurs only in debug mode.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
}
|
||||
@@ -2106,11 +2106,11 @@ impl NodeId {
|
||||
debug_warn!(
|
||||
"[Signal::update] You’re trying to update a \
|
||||
Signal<{}> (defined at {defined_at}) that has \
|
||||
already been disposed of. This is probably \
|
||||
either a logic error in a component that creates \
|
||||
and disposes of scopes. If it does cause cause \
|
||||
any issues, it is safe to ignore this warning, \
|
||||
which occurs only in debug mode.",
|
||||
already been disposed of. This is probably a \
|
||||
logic error in a component that creates and \
|
||||
disposes of scopes. If it does not cause any \
|
||||
issues, it is safe to ignore this warning, which \
|
||||
occurs only in debug mode.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,11 +71,7 @@ where
|
||||
|
||||
impl<T> Clone for Signal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,13 +432,7 @@ where
|
||||
|
||||
impl<T> Clone for SignalTypes<T> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::ReadSignal(arg0) => Self::ReadSignal(*arg0),
|
||||
Self::Memo(arg0) => Self::Memo(*arg0),
|
||||
Self::DerivedSignal(arg0, arg1) => {
|
||||
Self::DerivedSignal(*arg0, *arg1)
|
||||
}
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,6 +556,15 @@ impl<T: Default> Default for MaybeSignal<T> {
|
||||
/// # });
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::get()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn get(&self) -> T {
|
||||
match self {
|
||||
Self::Static(t) => t.clone(),
|
||||
@@ -573,6 +572,15 @@ impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::try_get()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_get(&self) -> Option<T> {
|
||||
match self {
|
||||
Self::Static(t) => Some(t.clone()),
|
||||
@@ -650,6 +658,15 @@ impl<T> SignalWith<T> for MaybeSignal<T> {
|
||||
}
|
||||
|
||||
impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::with_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
match self {
|
||||
Self::Static(t) => f(t),
|
||||
@@ -657,6 +674,15 @@ impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::try_with_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
match self {
|
||||
Self::Static(t) => Some(f(t)),
|
||||
@@ -666,6 +692,15 @@ impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::get_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> T {
|
||||
match self {
|
||||
Self::Static(t) => t.clone(),
|
||||
@@ -673,6 +708,15 @@ impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::try_get_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_get_untracked(&self) -> Option<T> {
|
||||
match self {
|
||||
Self::Static(t) => Some(t.clone()),
|
||||
@@ -682,6 +726,15 @@ impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalStream<T> for MaybeSignal<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::to_stream()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn to_stream(
|
||||
&self,
|
||||
cx: Scope,
|
||||
@@ -774,4 +827,400 @@ impl From<&str> for MaybeSignal<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapping type for an optional component prop, which can either be a signal or a
|
||||
/// non-reactive value, and which may or may not have a value. In other words, this is
|
||||
/// an `Option<MaybeSignal<Option<T>>>` that automatically flattens its getters.
|
||||
///
|
||||
/// This creates an extremely flexible type for component libraries, etc.
|
||||
///
|
||||
/// ## Core Trait Implementations
|
||||
/// - [`.get()`](#impl-SignalGet<T>-for-MaybeProp<T>) (or calling the signal as a function) clones the current
|
||||
/// value of the signal. If you call it within an effect, it will cause that effect
|
||||
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
|
||||
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-MaybeProp<T>) clones the value of the signal
|
||||
/// without reactively tracking it.
|
||||
/// - [`.with()`](#impl-SignalWith<T>-for-MaybeProp<T>) allows you to reactively access the signal’s value without
|
||||
/// cloning by applying a callback function.
|
||||
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeProp<T>) allows you to access the signal’s
|
||||
/// value without reactively tracking it.
|
||||
/// - [`.to_stream()`](#impl-SignalStream<T>-for-MaybeProp<T>) converts the signal to an `async` stream of values.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (count, set_count) = create_signal(cx, Some(2));
|
||||
/// let double = |n| n * 2;
|
||||
/// let double_count = MaybeProp::derive(cx, move || count.get().map(double));
|
||||
/// let memoized_double_count =
|
||||
/// create_memo(cx, move |_| count.get().map(double));
|
||||
/// let static_value = 5;
|
||||
///
|
||||
/// // this function takes either a reactive or non-reactive value
|
||||
/// fn above_3(arg: &MaybeProp<i32>) -> bool {
|
||||
/// // ✅ calling the signal clones and returns the value
|
||||
/// // it is a shorthand for arg.get()q
|
||||
/// arg.get().map(|arg| arg > 3).unwrap_or(false)
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&None::<i32>.into()), false);
|
||||
/// assert_eq!(above_3(&static_value.into()), true);
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
/// assert_eq!(above_3(&double_count), true);
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MaybeProp<T: 'static>(Option<MaybeSignal<Option<T>>>);
|
||||
|
||||
impl<T: Copy> Copy for MaybeProp<T> {}
|
||||
|
||||
impl<T> Default for MaybeProp<T> {
|
||||
fn default() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (count, set_count) = create_signal(cx, Some(2));
|
||||
/// let double = |n| n * 2;
|
||||
/// let double_count = MaybeProp::derive(cx, move || count.get().map(double));
|
||||
/// let memoized_double_count =
|
||||
/// create_memo(cx, move |_| count.get().map(double));
|
||||
/// let static_value = 5;
|
||||
///
|
||||
/// // this function takes either a reactive or non-reactive value
|
||||
/// fn above_3(arg: &MaybeProp<i32>) -> bool {
|
||||
/// // ✅ calling the signal clones and returns the value
|
||||
/// // it is a shorthand for arg.get()q
|
||||
/// arg.get().map(|arg| arg > 3).unwrap_or(false)
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&None::<i32>.into()), false);
|
||||
/// assert_eq!(above_3(&static_value.into()), true);
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
/// assert_eq!(above_3(&double_count), true);
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// # });
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<Option<T>> for MaybeProp<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::get()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn get(&self) -> Option<T> {
|
||||
self.0.as_ref().and_then(|s| s.get())
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::try_get()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_get(&self) -> Option<Option<T>> {
|
||||
self.0.as_ref().and_then(|s| s.try_get())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (name, set_name) = create_signal(cx, Some("Alice".to_string()));
|
||||
/// let name_upper = MaybeProp::derive(cx, move || {
|
||||
/// name.with(|n| n.as_ref().map(|n| n.to_uppercase()))
|
||||
/// });
|
||||
/// let memoized_lower = create_memo(cx, move |_| {
|
||||
/// name.with(|n| n.as_ref().map(|n| n.to_lowercase()))
|
||||
/// });
|
||||
/// let static_value: MaybeProp<String> = "Bob".to_string().into();
|
||||
///
|
||||
/// // this function takes any kind of wrapped signal
|
||||
/// fn current_len_inefficient(arg: &MaybeProp<String>) -> usize {
|
||||
/// // ❌ unnecessarily clones the string
|
||||
/// arg.get().map(|n| n.len()).unwrap_or(0)
|
||||
/// }
|
||||
///
|
||||
/// fn current_len(arg: &MaybeProp<String>) -> usize {
|
||||
/// // ✅ gets the length without cloning the `String`
|
||||
/// arg.with(|value| value.len()).unwrap_or(0)
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(current_len(&None::<String>.into()), 0);
|
||||
/// assert_eq!(current_len(&name_upper), 5);
|
||||
/// assert_eq!(current_len(&memoized_lower.into()), 5);
|
||||
/// assert_eq!(current_len(&static_value), 3);
|
||||
///
|
||||
/// assert_eq!(name.get(), Some("Alice".to_string()));
|
||||
/// assert_eq!(name_upper.get(), Some("ALICE".to_string()));
|
||||
/// assert_eq!(memoized_lower.get(), Some("alice".to_string()));
|
||||
/// assert_eq!(static_value.get(), Some("Bob".to_string()));
|
||||
/// # });
|
||||
/// ```
|
||||
impl<T> MaybeProp<T> {
|
||||
/// Applies a function to the current value, returning the result.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::with()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.with(|value| value.as_ref().map(f)))
|
||||
}
|
||||
|
||||
/// Applies a function to the current value, returning the result. Returns `None`
|
||||
/// if the value has already been disposed.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::try_with()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.try_with(|value| value.as_ref().map(f)))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Applies a function to the current value, returning the result, without
|
||||
/// causing the current reactive scope to track changes.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::with_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0.as_ref().and_then(|inner| {
|
||||
inner.with_untracked(|value| value.as_ref().map(f))
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies a function to the current value, returning the result, without
|
||||
/// causing the current reactive scope to track changes. Returns `None` if
|
||||
/// the value has already been disposed.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::try_with_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.and_then(|inner| {
|
||||
inner.try_with_untracked(|value| value.as_ref().map(f))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<Option<T>> for MaybeProp<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::get_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> Option<T> {
|
||||
self.0.as_ref().and_then(|inner| inner.get_untracked())
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::try_get_untracked()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn try_get_untracked(&self) -> Option<Option<T>> {
|
||||
self.0.as_ref().and_then(|inner| inner.try_get_untracked())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalStream<Option<T>> for MaybeProp<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::to_stream()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
fn to_stream(
|
||||
&self,
|
||||
cx: Scope,
|
||||
) -> std::pin::Pin<Box<dyn futures::Stream<Item = Option<T>>>> {
|
||||
match &self.0 {
|
||||
None => Box::pin(futures::stream::once(async move { None })),
|
||||
Some(MaybeSignal::Static(t)) => {
|
||||
let t = t.clone();
|
||||
|
||||
let stream = futures::stream::once(async move { t });
|
||||
|
||||
Box::pin(stream)
|
||||
}
|
||||
Some(MaybeSignal::Dynamic(s)) => s.to_stream(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeProp<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
/// Wraps a derived signal, i.e., any computation that accesses one or more
|
||||
/// reactive signals.
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (count, set_count) = create_signal(cx, Some(2));
|
||||
/// let double_count = Signal::derive(cx, move || count.get().map(|n| n * 2));
|
||||
///
|
||||
/// // this function takes any kind of wrapped signal
|
||||
/// fn above_3(arg: &MaybeProp<i32>) -> bool {
|
||||
/// arg.get().unwrap_or(0) > 3
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
/// assert_eq!(above_3(&double_count.into()), true);
|
||||
/// assert_eq!(above_3(&2.into()), false);
|
||||
/// # });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeProp::derive()",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn derive(
|
||||
cx: Scope,
|
||||
derived_signal: impl Fn() -> Option<T> + 'static,
|
||||
) -> Self {
|
||||
Self(Some(MaybeSignal::derive(cx, derived_signal)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MaybeProp<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self(Some(MaybeSignal::from(Some(value))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<T>> for MaybeProp<T> {
|
||||
fn from(value: Option<T>) -> Self {
|
||||
Self(Some(MaybeSignal::from(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<MaybeSignal<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: MaybeSignal<Option<T>>) -> Self {
|
||||
Self(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<MaybeSignal<Option<T>>>> for MaybeProp<T> {
|
||||
fn from(value: Option<MaybeSignal<Option<T>>>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ReadSignal<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: ReadSignal<Option<T>>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RwSignal<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: RwSignal<Option<T>>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Memo<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: Memo<Option<T>>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Signal<Option<T>>> for MaybeProp<T> {
|
||||
fn from(value: Signal<Option<T>>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for MaybeProp<String> {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(Some(MaybeSignal::from(Some(value.to_string()))))
|
||||
}
|
||||
}
|
||||
|
||||
impl_get_fn_traits![Signal, MaybeSignal];
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<T: Clone> FnOnce<()> for MaybeProp<T> {
|
||||
type Output = Option<T>;
|
||||
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<T: Clone> FnMut<()> for MaybeProp<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<T: Clone> Fn<()> for MaybeProp<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +63,7 @@ where
|
||||
|
||||
impl<T> Clone for SignalSetter<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,11 +227,7 @@ where
|
||||
|
||||
impl<T> Clone for SignalSetterTypes<T> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Write(arg0) => Self::Write(*arg0),
|
||||
Self::Mapped(cx, f) => Self::Mapped(*cx, *f),
|
||||
Self::Default => Self::Default,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,73 @@ use std::future::Future;
|
||||
|
||||
/// Spawns and runs a thread-local [`Future`] in a platform-independent way.
|
||||
///
|
||||
/// This can be used to interface with any `async` code.
|
||||
/// This can be used to interface with any `async` code by spawning a task
|
||||
/// to run a `Future`.
|
||||
///
|
||||
/// ## Limitations
|
||||
///
|
||||
/// You should not use `spawn_local` to synchronize `async` code with a
|
||||
/// signal’s value during server rendering. The server response will not
|
||||
/// be notified to wait for the spawned task to complete, creating a race
|
||||
/// condition between the response and your task. Instead, use
|
||||
/// [`create_resource`](crate::create_resource) and `<Suspense/>` to coordinate
|
||||
/// asynchronous work with the rendering process.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # #[cfg(not(any(feature = "csr", feature = "serde-lite", feature = "miniserde", feature = "rkyv")))]
|
||||
/// # {
|
||||
///
|
||||
/// async fn get_user(user: String) -> Result<String, ServerFnError> {
|
||||
/// Ok(format!("this user is {user}"))
|
||||
/// }
|
||||
///
|
||||
/// // ❌ Write into a signal from `spawn_local` on the serevr
|
||||
/// #[component]
|
||||
/// fn UserBad(cx: Scope) -> impl IntoView {
|
||||
/// let signal = create_rw_signal(cx, String::new());
|
||||
///
|
||||
/// // ❌ If the rest of the response is already complete,
|
||||
/// // `signal` will no longer exist when `get_user` resolves
|
||||
/// #[cfg(feature = "ssr")]
|
||||
/// spawn_local(async move {
|
||||
/// let user_res = get_user("user".into()).await.unwrap_or_default();
|
||||
/// signal.set(user_res);
|
||||
/// });
|
||||
///
|
||||
/// view!{cx,
|
||||
/// <p>
|
||||
/// "This will be empty (hopefully the client will render it) -> "
|
||||
/// {move || signal.get()}
|
||||
/// </p>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // ✅ Use a resource and suspense
|
||||
/// #[component]
|
||||
/// fn UserGood(cx: Scope) -> impl IntoView {
|
||||
/// // new resource with no dependencies (it will only called once)
|
||||
/// let user = create_resource(cx, || (), |_| async { get_user("john".into()).await });
|
||||
/// view!{cx,
|
||||
/// // handles the loading
|
||||
/// <Suspense fallback=move || view! {cx, <p>"Loading User"</p> }>
|
||||
/// // handles the error from the resource
|
||||
/// <ErrorBoundary fallback=|cx, _| {view! {cx, <p>"Something went wrong"</p>}}>
|
||||
/// {move || {
|
||||
/// user.read(cx).map(move |x| {
|
||||
/// // the resource has a result
|
||||
/// x.map(move |y| {
|
||||
/// // successful call from the server fn
|
||||
/// view!{cx, <p>"User result filled in server and client: "{y}</p>}
|
||||
/// })
|
||||
/// })
|
||||
/// }}
|
||||
/// </ErrorBoundary>
|
||||
/// </Suspense>
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn spawn_local<F>(fut: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
|
||||
@@ -33,11 +33,7 @@ where
|
||||
|
||||
impl<T> Clone for StoredValue<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: self.ty,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
server_fn = { workspace = true }
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -21,11 +22,11 @@ inventory = "0.3"
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
csr = ["leptos_reactive/csr"]
|
||||
csr = ["leptos_reactive/csr", "leptos_macro/csr"]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
hydrate = ["leptos_reactive/hydrate"]
|
||||
hydrate = ["leptos_reactive/hydrate", "leptos_macro/hydrate"]
|
||||
rustls = ["server_fn/rustls"]
|
||||
ssr = ["leptos_reactive/ssr", "server_fn/ssr"]
|
||||
ssr = ["leptos_reactive/ssr", "server_fn/ssr", "leptos_macro/ssr"]
|
||||
nightly = ["leptos_reactive/nightly", "server_fn/nightly"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -187,7 +187,7 @@ where
|
||||
O: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::incorrect_clone_impl_on_copy_type)]
|
||||
#![deny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ where
|
||||
O: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,12 +178,7 @@ where
|
||||
|
||||
impl<I, O> Clone for Submission<I, O> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
input: self.input,
|
||||
value: self.value,
|
||||
pending: self.pending,
|
||||
canceled: self.canceled,
|
||||
}
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.4.5"
|
||||
version = "0.4.7"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -125,6 +125,7 @@ pub fn Title(
|
||||
match document().query_selector("title") {
|
||||
Ok(Some(title)) => title.unchecked_into(),
|
||||
_ => {
|
||||
let el_ref = meta.title.el.clone();
|
||||
let el = document().create_element("title").unwrap_throw();
|
||||
let head = document().head().unwrap_throw();
|
||||
head.append_child(el.unchecked_ref())
|
||||
@@ -134,6 +135,7 @@ pub fn Title(
|
||||
let el = el.clone();
|
||||
move || {
|
||||
_ = head.remove_child(&el);
|
||||
*el_ref.borrow_mut() = None;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.4.5"
|
||||
version = "0.4.7"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{use_navigate, use_resolved_path, ToHref, Url};
|
||||
use crate::{use_navigate, use_resolved_path, NavigateOptions, ToHref, Url};
|
||||
use leptos::{html::form, *};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{error::Error, rc::Rc};
|
||||
@@ -54,6 +54,9 @@ pub fn Form<A>(
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
#[prop(optional)]
|
||||
node_ref: Option<NodeRef<html::Form>>,
|
||||
/// Sets whether the page should be scrolled to the top when the form is submitted.
|
||||
#[prop(optional)]
|
||||
noscroll: bool,
|
||||
/// Arbitrary attributes to add to the `<form>`
|
||||
#[prop(optional, into)]
|
||||
attributes: Option<MaybeSignal<AdditionalAttributes>>,
|
||||
@@ -76,6 +79,7 @@ where
|
||||
class: Option<Attribute>,
|
||||
children: Children,
|
||||
node_ref: Option<NodeRef<html::Form>>,
|
||||
noscroll: bool,
|
||||
attributes: Option<MaybeSignal<AdditionalAttributes>>,
|
||||
) -> HtmlElement<html::Form> {
|
||||
let action_version = version;
|
||||
@@ -85,6 +89,10 @@ where
|
||||
return;
|
||||
}
|
||||
let navigate = use_navigate(cx);
|
||||
let navigate_options = NavigateOptions {
|
||||
scroll: !noscroll,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (form, method, action, enctype) =
|
||||
extract_form_attributes(&ev);
|
||||
@@ -167,7 +175,7 @@ where
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
Default::default(),
|
||||
navigate_options,
|
||||
) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
@@ -248,7 +256,7 @@ where
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
Default::default(),
|
||||
navigate_options,
|
||||
) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
@@ -267,11 +275,8 @@ where
|
||||
else {
|
||||
let params =
|
||||
params.to_string().as_string().unwrap_or_default();
|
||||
if navigate(
|
||||
&format!("{action}?{params}"),
|
||||
Default::default(),
|
||||
)
|
||||
.is_ok()
|
||||
if navigate(&format!("{action}?{params}"), navigate_options)
|
||||
.is_ok()
|
||||
{
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
@@ -318,6 +323,7 @@ where
|
||||
class,
|
||||
children,
|
||||
node_ref,
|
||||
noscroll,
|
||||
attributes,
|
||||
)
|
||||
}
|
||||
@@ -364,6 +370,9 @@ pub fn ActionForm<I, O>(
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
#[prop(optional)]
|
||||
node_ref: Option<NodeRef<html::Form>>,
|
||||
/// Sets whether the page should be scrolled to the top when navigating.
|
||||
#[prop(optional)]
|
||||
noscroll: bool,
|
||||
/// Arbitrary attributes to add to the `<form>`
|
||||
#[prop(optional, into)]
|
||||
attributes: Option<MaybeSignal<AdditionalAttributes>>,
|
||||
@@ -513,6 +522,7 @@ where
|
||||
.on_error(on_error)
|
||||
.method("post")
|
||||
.class(class)
|
||||
.noscroll(noscroll)
|
||||
.children(children)
|
||||
.build();
|
||||
props.error = error;
|
||||
|
||||
@@ -42,19 +42,21 @@ impl ParamsMap {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
|
||||
/// Converts the map to a query string.
|
||||
pub fn to_query_string(&self) -> String {
|
||||
use crate::history::url::escape;
|
||||
let mut buf = String::from("?");
|
||||
for (k, v) in &self.0 {
|
||||
buf.push_str(&escape(k));
|
||||
buf.push('=');
|
||||
buf.push_str(&escape(v));
|
||||
buf.push('&');
|
||||
}
|
||||
if buf.len() > 1 {
|
||||
buf.pop();
|
||||
let mut buf = String::new();
|
||||
if !self.0.is_empty() {
|
||||
buf.push('?');
|
||||
for (k, v) in &self.0 {
|
||||
buf.push_str(&escape(k));
|
||||
buf.push('=');
|
||||
buf.push_str(&escape(v));
|
||||
buf.push('&');
|
||||
}
|
||||
if buf.len() > 1 {
|
||||
buf.pop();
|
||||
}
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
@@ -3,7 +3,82 @@ use crate::{
|
||||
RouteContext, RouterContext,
|
||||
};
|
||||
use leptos::{create_memo, signal_prelude::*, use_context, Memo, Scope};
|
||||
use std::rc::Rc;
|
||||
use std::{borrow::Cow, rc::Rc, str::FromStr};
|
||||
|
||||
/// Constructs a signal synchronized with a specific URL query parameter.
|
||||
///
|
||||
/// The function creates a bidirectional sync mechanism between the state encapsulated in a signal and a URL query parameter.
|
||||
/// This means that any change to the state will update the URL, and vice versa, making the function especially useful
|
||||
/// for maintaining state consistency across page reloads.
|
||||
///
|
||||
/// The `key` argument is the unique identifier for the query parameter to be synced with the state.
|
||||
/// It is important to note that only one state can be tied to a specific key at any given time.
|
||||
///
|
||||
/// The function operates with types that can be parsed from and formatted into strings, denoted by `T`.
|
||||
/// If the parsing fails for any reason, the function treats the value as `None`.
|
||||
/// The URL parameter can be cleared by setting the signal to `None`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use leptos::*;
|
||||
/// use leptos_router::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn SimpleQueryCounter(cx: Scope) -> impl IntoView {
|
||||
/// let (count, set_count) = create_query_signal::<i32>(cx, "count");
|
||||
/// let clear = move |_| set_count.set(None);
|
||||
/// let decrement =
|
||||
/// move |_| set_count.set(Some(count.get().unwrap_or(0) - 1));
|
||||
/// let increment =
|
||||
/// move |_| set_count.set(Some(count.get().unwrap_or(0) + 1));
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <div>
|
||||
/// <button on:click=clear>"Clear"</button>
|
||||
/// <button on:click=decrement>"-1"</button>
|
||||
/// <span>"Value: " {move || count.get().unwrap_or(0)} "!"</span>
|
||||
/// <button on:click=increment>"+1"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn create_query_signal<T>(
|
||||
cx: Scope,
|
||||
key: impl Into<Cow<'static, str>>,
|
||||
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
|
||||
where
|
||||
T: FromStr + ToString + PartialEq,
|
||||
{
|
||||
let key = key.into();
|
||||
let query_map = use_query_map(cx);
|
||||
let navigate = use_navigate(cx);
|
||||
let route = use_route(cx);
|
||||
|
||||
let get = create_memo(cx, {
|
||||
let key = key.clone();
|
||||
move |_| {
|
||||
query_map
|
||||
.with(|map| map.get(&key).and_then(|value| value.parse().ok()))
|
||||
}
|
||||
});
|
||||
|
||||
let set = SignalSetter::map(cx, move |value: Option<T>| {
|
||||
let mut new_query_map = query_map.get();
|
||||
match value {
|
||||
Some(value) => {
|
||||
new_query_map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
None => {
|
||||
new_query_map.remove(&key);
|
||||
}
|
||||
}
|
||||
let qs = new_query_map.to_query_string();
|
||||
let path = route.path();
|
||||
let new_url = format!("{path}{qs}");
|
||||
let _ = navigate(&new_url, NavigateOptions::default());
|
||||
});
|
||||
|
||||
(get, set)
|
||||
}
|
||||
|
||||
/// Returns the current [RouterContext], containing information about the router's state.
|
||||
pub fn use_router(cx: Scope) -> RouterContext {
|
||||
|
||||
@@ -5,7 +5,7 @@ use thiserror::Error;
|
||||
/// This is a result type into which any error can be converted,
|
||||
/// and which can be used directly in your `view`.
|
||||
///
|
||||
/// All errors will be stored as [`Error`].
|
||||
/// All errors will be stored as [`struct@Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
@@ -110,7 +110,7 @@ where
|
||||
|
||||
/// Type for errors that can occur when using server functions.
|
||||
///
|
||||
/// Unlike [`ServerFnErrorErr`], this implements [`std::error::Error`]. This means
|
||||
/// Unlike [`ServerFnError`], this implements [`std::error::Error`]. This means
|
||||
/// it can be used in situations in which the `Error` trait is required, but it’s
|
||||
/// not possible to create a blanket implementation that converts other errors into
|
||||
/// this type.
|
||||
|
||||
@@ -19,3 +19,4 @@ const_format = "0.2.30"
|
||||
|
||||
[features]
|
||||
nightly = []
|
||||
ssr = []
|
||||
|
||||
@@ -217,6 +217,61 @@ pub fn server_macro_impl(
|
||||
.map(|(doc, span)| quote_spanned!(*span=> #[doc = #doc]))
|
||||
.collect::<TokenStream2>();
|
||||
|
||||
let inventory = if cfg!(feature = "ssr") {
|
||||
quote! {
|
||||
#server_fn_path::inventory::submit! {
|
||||
#trait_obj_wrapper::from_generic_server_fn(#server_fn_path::ServerFnTraitObj::new(
|
||||
#struct_name::PREFIX,
|
||||
#struct_name::URL,
|
||||
#struct_name::ENCODING,
|
||||
<#struct_name as #server_fn_path::ServerFn<#server_ctx_path>>::call_from_bytes,
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let call_fn = if cfg!(feature = "ssr") {
|
||||
quote! {
|
||||
fn call_fn(self, cx: #server_ctx_path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, #server_fn_path::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_2),*).await })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
fn call_fn_client(self, cx: #server_ctx_path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, #server_fn_path::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names_3),* } = self;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_4),*).await })
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let func = if cfg!(feature = "ssr") {
|
||||
quote! {
|
||||
#docs
|
||||
#vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty {
|
||||
#block
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#docs
|
||||
#[allow(unused_variables)]
|
||||
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
|
||||
#server_fn_path::call_server_fn(
|
||||
&{
|
||||
let prefix = #struct_name::PREFIX.to_string();
|
||||
prefix + "/" + #struct_name::URL
|
||||
},
|
||||
#struct_name { #(#field_names_5),* },
|
||||
#encoding
|
||||
).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(quote::quote! {
|
||||
#args_docs
|
||||
#docs
|
||||
@@ -241,15 +296,7 @@ pub fn server_macro_impl(
|
||||
const ENCODING: #server_fn_path::Encoding = #encoding;
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#server_fn_path::inventory::submit! {
|
||||
#trait_obj_wrapper::from_generic_server_fn(#server_fn_path::ServerFnTraitObj::new(
|
||||
#struct_name::PREFIX,
|
||||
#struct_name::URL,
|
||||
#struct_name::ENCODING,
|
||||
<#struct_name as #server_fn_path::ServerFn<#server_ctx_path>>::call_from_bytes,
|
||||
))
|
||||
}
|
||||
#inventory
|
||||
|
||||
impl #server_fn_path::ServerFn<#server_ctx_path> for #struct_name {
|
||||
type Output = #output_ty;
|
||||
@@ -266,38 +313,10 @@ pub fn server_macro_impl(
|
||||
Self::ENCODING
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn call_fn(self, cx: #server_ctx_path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, #server_fn_path::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_2),*).await })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn call_fn_client(self, cx: #server_ctx_path) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, #server_fn_path::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names_3),* } = self;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_4),*).await })
|
||||
}
|
||||
#call_fn
|
||||
}
|
||||
|
||||
#docs
|
||||
#[cfg(feature = "ssr")]
|
||||
#vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty {
|
||||
#block
|
||||
}
|
||||
|
||||
#docs
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#[allow(unused_variables)]
|
||||
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
|
||||
#server_fn_path::call_server_fn(
|
||||
&{
|
||||
let prefix = #struct_name::PREFIX.to_string();
|
||||
prefix + "/" + #struct_name::URL
|
||||
},
|
||||
#struct_name { #(#field_names_5),* },
|
||||
#encoding
|
||||
).await
|
||||
}
|
||||
#func
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user