mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ad3164f6f | ||
|
|
e0c9a9523a | ||
|
|
0726a3034d | ||
|
|
a88d047eff | ||
|
|
4001561987 | ||
|
|
2f860b37bd | ||
|
|
b86009b9d0 | ||
|
|
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 | ||
|
|
011c97e3a4 | ||
|
|
2ca3d2c7a4 | ||
|
|
cc52c94348 | ||
|
|
4b8cc96dfa | ||
|
|
338d2ab839 | ||
|
|
54fc6da24e | ||
|
|
825b3fb858 | ||
|
|
fd0212a142 | ||
|
|
3b397cb39c | ||
|
|
1e002c2c2f | ||
|
|
8f45daeca8 | ||
|
|
105ef989b7 | ||
|
|
9e7c31d1e4 | ||
|
|
771dfa6b68 | ||
|
|
fb52cfa73e | ||
|
|
b2c75d215b | ||
|
|
951607de74 | ||
|
|
122fd2bc74 | ||
|
|
f102125d3c | ||
|
|
14bda76b30 | ||
|
|
3af115a663 | ||
|
|
a344804734 | ||
|
|
d8eaa5c004 |
@@ -220,8 +220,8 @@ for reference: they include large amounts of manual SSR route handling, etc.
|
||||
## `cargo-leptos` helpers
|
||||
|
||||
`leptos_config` and `leptos_hot_reload` exist to support two different features
|
||||
of `cargo-leptos`, namely its configuration and its view-patching/hot-
|
||||
reloading features.
|
||||
of `cargo-leptos`, namely its configuration and its view-patching/hot-reloading
|
||||
features.
|
||||
|
||||
It’s important to say that the main feature `cargo-leptos` remains its ability
|
||||
to conveniently tie together different build tooling, compiling your app to
|
||||
|
||||
@@ -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.8"
|
||||
|
||||
[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.8" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.8" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.8" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.8" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.8" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.8" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.8" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.8" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.8" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.8" }
|
||||
leptos_router = { path = "./router", version = "0.4.8" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.8" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.8" }
|
||||
|
||||
[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).
|
||||
@@ -107,7 +105,7 @@ Open browser to [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
### What’s up with the name?
|
||||
|
||||
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refined, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
|
||||
[tasks.check-style]
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
[tasks.lint]
|
||||
dependencies = ["check-format-flow", "clippy-each-feature"]
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
|
||||
[tasks.clippy-each-feature]
|
||||
dependencies = ["install-clippy"]
|
||||
command = "cargo"
|
||||
args = ["hack", "clippy", "--all", "--each-feature", "--no-dev-deps"]
|
||||
|
||||
@@ -13,6 +13,3 @@ RUSTFLAGS = "-D warnings"
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["lint", "test"]
|
||||
|
||||
[tasks.lint]
|
||||
dependencies = ["check-format-flow"]
|
||||
|
||||
@@ -26,7 +26,7 @@ cargo init leptos-tutorial
|
||||
cargo add leptos --features=csr,nightly
|
||||
```
|
||||
|
||||
Or you can leave off `nighly` if you're using stable Rust
|
||||
Or you can leave off `nightly` if you're using stable Rust
|
||||
```bash
|
||||
cargo add leptos --features=csr
|
||||
```
|
||||
|
||||
@@ -136,7 +136,7 @@ view! { cx,
|
||||
|
||||
In this example, clicking the button will cause the text inside `<p>` to be updated, cloning `state.name` again! Because signals are the atomic unit of reactivity, updating any field of the signal triggers updates to everything that depends on the signal.
|
||||
|
||||
There’s a better way. You can use take fine-grained, reactive slices by using [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html) or [`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html) (which uses `create_memo` but also provides a setter). “Memoizing” a value means creating a new reactive value which will only update when it changes. “Memoizing a slice” means creating a new reactive value which will only update when some field of the state struct updates.
|
||||
There’s a better way. You can take fine-grained, reactive slices by using [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html) or [`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html) (which uses `create_memo` but also provides a setter). “Memoizing” a value means creating a new reactive value which will only update when it changes. “Memoizing a slice” means creating a new reactive value which will only update when some field of the state struct updates.
|
||||
|
||||
Here, instead of reading from the state signal directly, we create “slices” of that state with fine-grained updates via `create_slice`. Each slice signal only updates when the particular piece of the larger struct it accesses updates. This means you can create a single root signal, and then take independent, fine-grained slices of it in different components, each of which can update without notifying the others of changes.
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ pub fn SimpleCounter(cx: Scope) -> impl IntoView {
|
||||
|
||||
The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`.
|
||||
|
||||
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in responsive to a change.
|
||||
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in response to a change.
|
||||
|
||||
So remember two things:
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use leptos_router::*;
|
||||
|
||||
Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, the rest of the app.
|
||||
|
||||
> You shouldn’t try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which ones decides what to do when the URL changes?
|
||||
> You shouldn’t try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which one decides what to do when the URL changes?
|
||||
|
||||
Let’s start with a simple `<App/>` component using the router:
|
||||
|
||||
@@ -87,15 +87,17 @@ The `view` is a function that takes a `Scope` and returns a view.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home/> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users/> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile/> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound/> }/>
|
||||
<Route path="/" view=Home/>
|
||||
<Route path="/users" view=Users/>
|
||||
<Route path="/users/:id" view=UserProfile/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The router scores each route to see how good a match it is, so you can define your routes in any order.
|
||||
> `view` takes a `Fn(Scope) -> impl IntoView`. If a component has no props, it is a function that takes `Scope` and returns `impl IntoView`, so it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|cx| view! { cx, <Home/> }`.
|
||||
|
||||
Now if you navigate to `/` or to `/users` you’ll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` you’ll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
|
||||
|
||||
Note that you can define your routes in any order. The router scores each route to see how good a match it is, rather than simply trying to match them top to bottom.
|
||||
|
||||
Simple enough?
|
||||
|
||||
@@ -4,10 +4,10 @@ We just defined the following set of routes:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <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=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="/" view=Home/>
|
||||
<Route path="/users" view=Users>
|
||||
<Route path=":id" view=UserProfile/>
|
||||
</Route>
|
||||
<Route path="/*any" view=|cx| view! { cx, <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=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <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=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <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=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="" view=|cx| view! { cx, <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=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <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=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }>
|
||||
<Route path="" view=|cx| view! { cx, <EmailAndPhone/> }/>
|
||||
<Route path="address" view=|cx| view! { cx, <Address/> }/>
|
||||
<Route path="messages" view=|cx| view! { cx, <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>
|
||||
@@ -201,12 +201,9 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
// /contacts has nested routes
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=|cx| view! { cx, <ContactList/> }
|
||||
>
|
||||
view=ContactList
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=|cx| view! { cx,
|
||||
<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`.
|
||||
|
||||
@@ -108,12 +116,10 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
// /contacts has nested routes
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=|cx| view! { cx, <ContactList/> }
|
||||
view=ContactList
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=|cx| view! { cx,
|
||||
<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.
|
||||
@@ -52,12 +54,10 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
// /contacts has nested routes
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=|cx| view! { cx, <ContactList/> }
|
||||
view=ContactList
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=|cx| view! { cx,
|
||||
<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=|cx| view! { cx, <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
|
||||
|
||||
@@ -64,7 +69,7 @@ If you’re using server-side rendering, the synchronous mode is almost never wh
|
||||
|
||||
5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. If one of them reads from one or more “blocking resources” (see below), the fallback will not be sent; rather, the server will wait until that `<Suspense/>` has resolved and then replace the fallback with the resolved fragment on the server, which means that it is included in the initial HTML response and appears even if JavaScript is disabled or not supported. Other `<Suspense/>` stream in out of order as usual.
|
||||
|
||||
This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is *not* useful if there’s only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering.
|
||||
This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is _not_ useful if there’s only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering.
|
||||
|
||||
- _Pros_: Works if JavaScript is disabled or not supported on the user’s device.
|
||||
- _Cons_
|
||||
@@ -79,13 +84,13 @@ 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=|cx| view! { cx, <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
|
||||
<Route
|
||||
path="/post/:id"
|
||||
view=|cx| view! { cx, <Post/> }
|
||||
view=Post
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
</Routes>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -34,7 +34,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! { cx, <ExampleErrors/> }/>
|
||||
<Route path="" view=ExampleErrors/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
|
||||
<A href="/job">
|
||||
<strong>"Jobs"</strong>
|
||||
</A>
|
||||
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
|
||||
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
|
||||
<A href="/job">
|
||||
<strong>"Jobs"</strong>
|
||||
</A>
|
||||
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
|
||||
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -170,7 +170,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
<hr/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! { cx, <Todos/> }/> //Route
|
||||
<Route path="" view=Todos/> //Route
|
||||
<Route path="signup" view=move |cx| view! {
|
||||
cx,
|
||||
<Signup action=signup/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! { cx, <Todos/> }/>
|
||||
<Route path="" view=Todos/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -126,10 +126,6 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<form method="POST" action="/weird">
|
||||
<input type="text" name="hi" value="John"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -104,10 +104,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/> //Route
|
||||
<Route path="" view=Todos/> //Route
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -14,7 +14,7 @@ fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
|
||||
r#"
|
||||
<script crossorigin=""{nonce_str}>(function () {{
|
||||
{}
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
let ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
@@ -52,7 +52,7 @@ pub fn html_parts(
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to maintain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME at compile time
|
||||
// Otherwise we need to add _bg because wasm_pack always does.
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
@@ -60,7 +60,7 @@ pub fn html_parts(
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let leptos_autoreload = autoreload("".into(), options);
|
||||
let leptos_autoreload = autoreload("", options);
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
@@ -94,7 +94,7 @@ pub fn html_parts_separated(
|
||||
.map(|nonce| format!(" nonce=\"{nonce}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to maintain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME at compile time
|
||||
// Otherwise we need to add _bg because wasm_pack always does.
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
|
||||
@@ -4,7 +4,7 @@ version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -52,12 +52,12 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Outlet/>
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
<Route path="none" view=|cx| view! { cx, <None/> }/>
|
||||
<Route path="" view=Nested
|
||||
<Route path="inside" view=NestedResourceInside
|
||||
<Route path="single" view=Single
|
||||
<Route path="parallel" view=Parallel
|
||||
<Route path="inside-component" view=InsideComponent
|
||||
<Route path="none" view=None
|
||||
</Route>
|
||||
// in-order
|
||||
<Route
|
||||
@@ -69,12 +69,12 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Outlet/>
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
<Route path="none" view=|cx| view! { cx, <None/> }/>
|
||||
<Route path="" view=Nested
|
||||
<Route path="inside" view=NestedResourceInside
|
||||
<Route path="single" view=Single
|
||||
<Route path="parallel" view=Parallel
|
||||
<Route path="inside-component" view=InsideComponent
|
||||
<Route path="none" view=None
|
||||
</Route>
|
||||
// async
|
||||
<Route
|
||||
@@ -86,12 +86,12 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Outlet/>
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
<Route path="none" view=|cx| view! { cx, <None/> }/>
|
||||
<Route path="" view=Nested
|
||||
<Route path="inside" view=NestedResourceInside
|
||||
<Route path="single" view=Single
|
||||
<Route path="parallel" view=Parallel
|
||||
<Route path="inside-component" view=InsideComponent
|
||||
<Route path="none" view=None
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -179,9 +179,9 @@ impl TryFrom<String> for Env {
|
||||
/// Loads [LeptosOptions] from a Cargo.toml text content with layered overrides.
|
||||
/// If an env var is specified, like `LEPTOS_ENV`, it will override a setting in the file.
|
||||
pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> {
|
||||
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
|
||||
let re: Regex = Regex::new(r"(?m)^\[package.metadata.leptos\]").unwrap();
|
||||
let re_workspace: Regex =
|
||||
Regex::new(r#"(?m)^\[\[workspace.metadata.leptos\]\]"#).unwrap();
|
||||
Regex::new(r"(?m)^\[\[workspace.metadata.leptos\]\]").unwrap();
|
||||
|
||||
let metadata_name;
|
||||
let start;
|
||||
|
||||
@@ -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/>-->
|
||||
|
||||
@@ -196,7 +196,7 @@ impl TimeoutHandle {
|
||||
/// Executes the given function after the given duration of time has passed.
|
||||
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
@@ -206,7 +206,7 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
|
||||
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
#[inline(always)]
|
||||
@@ -325,11 +325,10 @@ impl IntervalHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls,
|
||||
/// returning a cancelable handle.
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls.
|
||||
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
|
||||
@@ -340,7 +339,7 @@ pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
|
||||
/// returning a cancelable handle.
|
||||
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
#[inline(always)]
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -37,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::*;
|
||||
@@ -211,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`].
|
||||
@@ -272,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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -357,10 +357,10 @@ fn fragments_to_chunks(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
<script{nonce_str}>
|
||||
var id = "{fragment_id}";
|
||||
var open = undefined;
|
||||
var close = undefined;
|
||||
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
||||
(function() {{ let id = "{fragment_id}";
|
||||
let open = undefined;
|
||||
let close = undefined;
|
||||
let walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
||||
while(walker.nextNode()) {{
|
||||
if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{
|
||||
open = walker.currentNode;
|
||||
@@ -368,12 +368,12 @@ fn fragments_to_chunks(
|
||||
close = walker.currentNode;
|
||||
}}
|
||||
}}
|
||||
var range = new Range();
|
||||
let range = new Range();
|
||||
range.setStartAfter(open);
|
||||
range.setEndBefore(close);
|
||||
range.deleteContents();
|
||||
var tpl = document.getElementById("{fragment_id}f");
|
||||
close.parentNode.insertBefore(tpl.content.cloneNode(true), close);
|
||||
let tpl = document.getElementById("{fragment_id}f");
|
||||
close.parentNode.insertBefore(tpl.content.cloneNode(true), close);}})()
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
@@ -483,18 +483,36 @@ impl View {
|
||||
true,
|
||||
Box::new(move || {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{}", t.content).into()
|
||||
// if we don't check if the string is empty,
|
||||
// the HTML is an empty string; but an empty string
|
||||
// is not a text node in HTML, so can't be updated
|
||||
// in the future. so we put a one-space text node instead
|
||||
let was_empty = t.content.is_empty();
|
||||
let content = if was_empty {
|
||||
" ".into()
|
||||
} else {
|
||||
t.content
|
||||
};
|
||||
// escape content unless we're in a <script> or <style>
|
||||
let content = if dont_escape_text {
|
||||
content
|
||||
} else {
|
||||
html_escape::encode_safe(&content)
|
||||
.to_string()
|
||||
.into()
|
||||
};
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{content}",).into()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
} else {
|
||||
child.render_to_string_helper(
|
||||
@@ -710,12 +728,12 @@ pub(crate) fn render_serializers(
|
||||
let json = json.replace('<', "\\u003c");
|
||||
format!(
|
||||
r#"<script{nonce_str}>
|
||||
var val = {json:?};
|
||||
(function() {{ let val = {json:?};
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
|
||||
}} else {{
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, val);
|
||||
}}
|
||||
}} }})();
|
||||
</script>"#,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -416,34 +416,50 @@ impl View {
|
||||
Box::new(
|
||||
move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
let content = if dont_escape_text {
|
||||
// if we don't check if the string is empty,
|
||||
// the HTML is an empty string; but an empty string
|
||||
// is not a text node in HTML, so can't be updated
|
||||
// in the future. so we put a one-space text node instead
|
||||
let was_empty =
|
||||
t.content.is_empty();
|
||||
let content = if was_empty {
|
||||
" ".into()
|
||||
} else {
|
||||
t.content
|
||||
};
|
||||
// escape content unless we're in a <script> or <style>
|
||||
let content = if dont_escape_text {
|
||||
content
|
||||
} else {
|
||||
html_escape::encode_safe(
|
||||
&t.content,
|
||||
&content,
|
||||
)
|
||||
.to_string()
|
||||
.into()
|
||||
};
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
chunks.push_back(
|
||||
if !cfg!(debug_assertions) {
|
||||
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"
|
||||
@@ -33,12 +33,12 @@ log = "0.4"
|
||||
typed-builder = "0.14"
|
||||
trybuild = "1"
|
||||
leptos = { path = "../leptos" }
|
||||
insta = "1.29"
|
||||
|
||||
[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()
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,9 @@ impl Default for Mode {
|
||||
|
||||
mod params;
|
||||
mod view;
|
||||
use template::render_template;
|
||||
use view::render_view;
|
||||
use view::{client_template::render_template, render_view};
|
||||
mod component;
|
||||
mod slot;
|
||||
mod template;
|
||||
|
||||
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
|
||||
/// same rules as HTML, with the following differences:
|
||||
@@ -169,14 +167,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 +931,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"),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
511
leptos_macro/src/view/client_builder.rs
Normal file
511
leptos_macro/src/view/client_builder.rs
Normal file
@@ -0,0 +1,511 @@
|
||||
use super::{
|
||||
component_builder::component_to_tokens,
|
||||
expr_to_ident, fancy_class_name, fancy_style_name,
|
||||
ide_helper::IdeTagHelper,
|
||||
is_ambiguous_element, is_custom_element, is_math_ml_element,
|
||||
is_svg_element, parse_event_name,
|
||||
slot_helper::{get_slot, slot_to_tokens},
|
||||
};
|
||||
use crate::attribute_value;
|
||||
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned};
|
||||
use rstml::node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName};
|
||||
use std::collections::HashMap;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum TagType {
|
||||
Unknown,
|
||||
Html,
|
||||
Svg,
|
||||
Math,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn fragment_to_tokens(
|
||||
cx: &Ident,
|
||||
_span: Span,
|
||||
nodes: &[Node],
|
||||
lazy: bool,
|
||||
parent_type: TagType,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
let mut slots = HashMap::new();
|
||||
let has_slots = parent_slots.is_some();
|
||||
|
||||
let mut nodes = nodes
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
let node = node_to_tokens(
|
||||
cx,
|
||||
node,
|
||||
parent_type,
|
||||
has_slots.then_some(&mut slots),
|
||||
global_class,
|
||||
None,
|
||||
)?;
|
||||
|
||||
Some(quote! {
|
||||
#node.into_view(#cx)
|
||||
})
|
||||
})
|
||||
.peekable();
|
||||
|
||||
if nodes.peek().is_none() {
|
||||
_ = nodes.collect::<Vec<_>>();
|
||||
if let Some(parent_slots) = parent_slots {
|
||||
for (slot, mut values) in slots.drain() {
|
||||
parent_slots
|
||||
.entry(slot)
|
||||
.and_modify(|entry| entry.append(&mut values))
|
||||
.or_insert(values);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let tokens = if lazy {
|
||||
quote! {
|
||||
{
|
||||
::leptos::Fragment::lazy(|| [
|
||||
#(#nodes),*
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
{
|
||||
::leptos::Fragment::new([
|
||||
#(#nodes),*
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(parent_slots) = parent_slots {
|
||||
for (slot, mut values) in slots.drain() {
|
||||
parent_slots
|
||||
.entry(slot)
|
||||
.and_modify(|entry| entry.append(&mut values))
|
||||
.or_insert(values);
|
||||
}
|
||||
}
|
||||
|
||||
Some(tokens)
|
||||
}
|
||||
|
||||
pub(crate) fn node_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &Node,
|
||||
parent_type: TagType,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
match node {
|
||||
Node::Fragment(fragment) => fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
true,
|
||||
parent_type,
|
||||
None,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => Some(quote! {}),
|
||||
Node::Text(node) => Some(quote! {
|
||||
::leptos::leptos_dom::html::text(#node)
|
||||
}),
|
||||
Node::Block(node) => Some(quote! { #node }),
|
||||
Node::RawText(r) => {
|
||||
let text = r.to_string_best();
|
||||
let text = syn::LitStr::new(&text, r.span());
|
||||
Some(quote! { #text })
|
||||
}
|
||||
Node::Element(node) => element_to_tokens(
|
||||
cx,
|
||||
node,
|
||||
parent_type,
|
||||
parent_slots,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn element_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
mut parent_type: TagType,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
let name = node.name();
|
||||
if is_component_node(node) {
|
||||
if let Some(slot) = get_slot(node) {
|
||||
slot_to_tokens(cx, node, slot, parent_slots, global_class);
|
||||
None
|
||||
} else {
|
||||
Some(component_to_tokens(cx, node, global_class))
|
||||
}
|
||||
} else {
|
||||
let tag = name.to_string();
|
||||
// collect close_tag name to emit semantic information for IDE.
|
||||
let mut ide_helper_close_tag = IdeTagHelper::new();
|
||||
let close_tag = node.close_tag.as_ref().map(|c| &c.name);
|
||||
let name = if is_custom_element(&tag) {
|
||||
let name = node.name().to_string();
|
||||
// link custom ident to name span for IDE docs
|
||||
let custom = Ident::new("custom", name.span());
|
||||
quote! { ::leptos::leptos_dom::html::#custom(#cx, ::leptos::leptos_dom::html::Custom::new(#name)) }
|
||||
} else if is_svg_element(&tag) {
|
||||
parent_type = TagType::Svg;
|
||||
quote! { ::leptos::leptos_dom::svg::#name(#cx) }
|
||||
} else if is_math_ml_element(&tag) {
|
||||
parent_type = TagType::Math;
|
||||
quote! { ::leptos::leptos_dom::math::#name(#cx) }
|
||||
} else if is_ambiguous_element(&tag) {
|
||||
match parent_type {
|
||||
TagType::Unknown => {
|
||||
// We decided this warning was too aggressive, but I'll leave it here in case we want it later
|
||||
/* proc_macro_error::emit_warning!(name.span(), "The view macro is assuming this is an HTML element, \
|
||||
but it is ambiguous; if it is an SVG or MathML element, prefix with svg:: or math::"); */
|
||||
quote! {
|
||||
::leptos::leptos_dom::html::#name(#cx)
|
||||
}
|
||||
}
|
||||
TagType::Html => {
|
||||
quote! { ::leptos::leptos_dom::html::#name(#cx) }
|
||||
}
|
||||
TagType::Svg => {
|
||||
quote! { ::leptos::leptos_dom::svg::#name(#cx) }
|
||||
}
|
||||
TagType::Math => {
|
||||
quote! { ::leptos::leptos_dom::math::#name(#cx) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parent_type = TagType::Html;
|
||||
quote! { ::leptos::leptos_dom::html::#name(#cx) }
|
||||
};
|
||||
|
||||
if let Some(close_tag) = close_tag {
|
||||
ide_helper_close_tag.save_tag_completion(close_tag)
|
||||
}
|
||||
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
let name = name.trim();
|
||||
if name.starts_with("class:")
|
||||
|| fancy_class_name(name, cx, node).is_some()
|
||||
|| name.starts_with("style:")
|
||||
|| fancy_style_name(name, cx, node).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let class_attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
|
||||
Some(fancy)
|
||||
} else if name.trim().starts_with("class:") {
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let style_attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if let Some((fancy, _, _)) = fancy_style_name(&name, cx, node) {
|
||||
Some(fancy)
|
||||
} else if name.trim().starts_with("style:") {
|
||||
Some(attribute_to_tokens(cx, node, global_class))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let global_class_expr = match global_class {
|
||||
None => quote! {},
|
||||
Some(class) => {
|
||||
quote! {
|
||||
.classes(
|
||||
#[allow(unused_braces)]
|
||||
#class
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
let children = node.children.iter().map(|node| {
|
||||
let (child, is_static) = match node {
|
||||
Node::Fragment(fragment) => (
|
||||
fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
true,
|
||||
parent_type,
|
||||
None,
|
||||
global_class,
|
||||
None,
|
||||
)
|
||||
.unwrap_or({
|
||||
let span = Span::call_site();
|
||||
quote_spanned! {
|
||||
span => ::leptos::leptos_dom::Unit
|
||||
}
|
||||
}),
|
||||
false,
|
||||
),
|
||||
Node::Text(node) => (quote! { #node }, true),
|
||||
Node::RawText(node) => {
|
||||
let text = node.to_string_best();
|
||||
let text = syn::LitStr::new(&text, node.span());
|
||||
(quote! { #text }, true)
|
||||
}
|
||||
Node::Block(node) => (
|
||||
quote! {
|
||||
#node
|
||||
},
|
||||
false,
|
||||
),
|
||||
Node::Element(node) => (
|
||||
element_to_tokens(
|
||||
cx,
|
||||
node,
|
||||
parent_type,
|
||||
None,
|
||||
global_class,
|
||||
None,
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
false,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => (quote! {}, false),
|
||||
};
|
||||
if is_static {
|
||||
quote! {
|
||||
.child(#child)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.child((#cx, #child))
|
||||
}
|
||||
}
|
||||
});
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let ide_helper_close_tag = ide_helper_close_tag.into_iter();
|
||||
Some(quote! {
|
||||
{
|
||||
#(#ide_helper_close_tag)*
|
||||
#name
|
||||
#(#attrs)*
|
||||
#(#class_attrs)*
|
||||
#(#style_attrs)*
|
||||
#global_class_expr
|
||||
#(#children)*
|
||||
#view_marker
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attribute_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &KeyedAttribute,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let span = node.key.span();
|
||||
let name = node.key.to_string();
|
||||
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
|
||||
let value = expr_to_ident(attribute_value(node));
|
||||
let node_ref = quote_spanned! { span => node_ref };
|
||||
|
||||
quote! {
|
||||
.#node_ref(#value)
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = attribute_value(node);
|
||||
|
||||
let (event_type, is_custom, is_force_undelegated) =
|
||||
parse_event_name(name);
|
||||
|
||||
let event_name_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => {
|
||||
if parts.len() >= 2 {
|
||||
Some(&parts[1])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let undelegated_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => parts.last().and_then(|last| {
|
||||
if last.to_string() == "undelegated" {
|
||||
Some(last)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = {
|
||||
let span = on.span();
|
||||
quote_spanned! {
|
||||
span => .on
|
||||
}
|
||||
};
|
||||
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
|
||||
}
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
|
||||
let event_type = if is_force_undelegated {
|
||||
let undelegated = if let Some(undelegated) = undelegated_ident {
|
||||
let span = undelegated.span();
|
||||
quote_spanned! {
|
||||
span => #undelegated
|
||||
}
|
||||
} else {
|
||||
quote! { undelegated }
|
||||
};
|
||||
quote! { ::leptos::ev::#undelegated(::leptos::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::ev::#event_type }
|
||||
};
|
||||
|
||||
quote! {
|
||||
#on(#event_type, #handler)
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("prop:") {
|
||||
let value = attribute_value(node);
|
||||
let prop = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let prop = {
|
||||
let span = prop.span();
|
||||
quote_spanned! {
|
||||
span => .prop
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#prop(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("class:") {
|
||||
let value = attribute_value(node);
|
||||
let class = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let class = {
|
||||
let span = class.span();
|
||||
quote_spanned! {
|
||||
span => .class
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#class(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("style:") {
|
||||
let value = attribute_value(node);
|
||||
let style = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let style = {
|
||||
let span = style.span();
|
||||
quote_spanned! {
|
||||
span => .style
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#style(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
}
|
||||
} else {
|
||||
let name = name.replacen("attr:", "", 1);
|
||||
|
||||
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
|
||||
return fancy;
|
||||
}
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& node.value().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = node.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
|
||||
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
|
||||
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
|
||||
};
|
||||
|
||||
// all other attributes
|
||||
let value = match node.value() {
|
||||
Some(value) => {
|
||||
quote! { #value }
|
||||
}
|
||||
None => quote_spanned! { span => "" },
|
||||
};
|
||||
|
||||
let attr = match &node.key {
|
||||
NodeName::Punctuated(parts) => Some(&parts[0]),
|
||||
_ => None,
|
||||
};
|
||||
let attr = if let Some(attr) = attr {
|
||||
let span = attr.span();
|
||||
quote_spanned! {
|
||||
span => .attr
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.attr
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#attr(#name, (#cx, #value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{attribute_value, view::IdeTagHelper};
|
||||
use super::{component_builder::component_to_tokens, IdeTagHelper};
|
||||
use crate::attribute_value;
|
||||
use itertools::Either;
|
||||
use leptos_hot_reload::parsing::{
|
||||
block_to_primitive_expression, is_component_node, value_to_string,
|
||||
@@ -9,13 +10,10 @@ use rstml::node::{
|
||||
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
|
||||
};
|
||||
use syn::spanned::Spanned;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) fn render_template(cx: &Ident, nodes: &[Node]) -> TokenStream {
|
||||
let template_uid = Ident::new(
|
||||
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
|
||||
Span::call_site(),
|
||||
);
|
||||
// No reason to make template unique, because its "static" is in inner scope.
|
||||
let template_uid = Ident::new("__TEMPLATE", Span::call_site());
|
||||
|
||||
match nodes.first() {
|
||||
Some(Node::Element(node)) => {
|
||||
@@ -36,7 +34,7 @@ fn root_element_to_tokens(
|
||||
let mut expressions = Vec::new();
|
||||
|
||||
if is_component_node(node) {
|
||||
crate::view::component_to_tokens(cx, node, None)
|
||||
component_to_tokens(cx, node, None)
|
||||
} else {
|
||||
element_to_tokens(
|
||||
cx,
|
||||
@@ -65,11 +63,11 @@ fn root_element_to_tokens(
|
||||
quote! {
|
||||
{
|
||||
thread_local! {
|
||||
static #template_uid: leptos::web_sys::HtmlTemplateElement = {
|
||||
let document = leptos::document();
|
||||
static #template_uid: ::leptos::web_sys::HtmlTemplateElement = {
|
||||
let document = ::leptos::document();
|
||||
let el = document.create_element("template").unwrap();
|
||||
el.set_inner_html(#template);
|
||||
leptos::wasm_bindgen::JsCast::unchecked_into(el)
|
||||
::leptos::wasm_bindgen::JsCast::unchecked_into(el)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +77,10 @@ fn root_element_to_tokens(
|
||||
#(#navigations)*
|
||||
#(#expressions;)*
|
||||
|
||||
leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
|
||||
::leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
|
||||
#[cfg(debug_assertions)]
|
||||
name: #tag_name.into(),
|
||||
element: leptos::wasm_bindgen::JsCast::unchecked_into(root),
|
||||
element: ::leptos::wasm_bindgen::JsCast::unchecked_into(root),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
})
|
||||
@@ -149,7 +147,7 @@ fn element_to_tokens(
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
let #this_el_ident =
|
||||
leptos::wasm_bindgen::JsCast::unchecked_into::<leptos::web_sys::Node>(#parent.clone());
|
||||
::leptos::wasm_bindgen::JsCast::unchecked_into::<leptos::web_sys::Node>(#parent.clone());
|
||||
//debug!("=> got {}", #this_el_ident.node_name());
|
||||
}
|
||||
} else if let Some(prev_sib) = &prev_sib {
|
||||
@@ -302,7 +300,7 @@ fn attr_to_tokens(
|
||||
let (event_type, handler) =
|
||||
crate::view::event_from_attribute_node(node, false);
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::add_event_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #event_type, #handler);
|
||||
::leptos::leptos_dom::add_event_helper(::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #event_type, #handler);
|
||||
})
|
||||
}
|
||||
// Properties
|
||||
@@ -310,7 +308,7 @@ fn attr_to_tokens(
|
||||
let value = attribute_value(node);
|
||||
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos::leptos_dom::property(#cx, leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name, #value.into_property(#cx))
|
||||
span => ::leptos::leptos_dom::property(#cx, ::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name, #value.into_property(#cx))
|
||||
});
|
||||
}
|
||||
// Classes
|
||||
@@ -318,7 +316,7 @@ fn attr_to_tokens(
|
||||
let value = attribute_value(node);
|
||||
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos::leptos_dom::class_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), #value.into_class(#cx))
|
||||
span => ::leptos::leptos_dom::class_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), #value.into_class(#cx))
|
||||
});
|
||||
}
|
||||
// Attributes
|
||||
@@ -342,7 +340,7 @@ fn attr_to_tokens(
|
||||
// For client-side rendering, dynamic attributes don't need to be rendered in the template
|
||||
// They'll immediately be set synchronously before the cloned template is mounted
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos::leptos_dom::attribute_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), {#value}.into_attribute(#cx))
|
||||
span => ::leptos::leptos_dom::attribute_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), {#value}.into_attribute(#cx))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -493,10 +491,10 @@ fn block_to_tokens(
|
||||
|
||||
let mount_kind = match &next_sib {
|
||||
Some(child) => {
|
||||
quote! { leptos::leptos_dom::MountKind::Before(&#child.clone()) }
|
||||
quote! { ::leptos::leptos_dom::MountKind::Before(&#child.clone()) }
|
||||
}
|
||||
None => {
|
||||
quote! { leptos::leptos_dom::MountKind::Append(&#parent) }
|
||||
quote! { ::leptos::leptos_dom::MountKind::Append(&#parent) }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -516,7 +514,7 @@ fn block_to_tokens(
|
||||
navigations.push(location);
|
||||
|
||||
expressions.push(quote! {
|
||||
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
|
||||
::leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
|
||||
});
|
||||
|
||||
if let Some(name) = name {
|
||||
176
leptos_macro/src/view/component_builder.rs
Normal file
176
leptos_macro/src/view/component_builder.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use super::{
|
||||
client_builder::{fragment_to_tokens, TagType},
|
||||
event_from_attribute_node, ident_from_tag_name,
|
||||
};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote};
|
||||
use rstml::node::{NodeAttribute, NodeElement};
|
||||
use std::collections::HashMap;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
pub(crate) fn component_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
let name = node.name();
|
||||
#[cfg(debug_assertions)]
|
||||
let component_name = ident_from_tag_name(node.name());
|
||||
let span = node.name().span();
|
||||
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let props = attrs
|
||||
.clone()
|
||||
.filter(|attr| {
|
||||
!attr.key.to_string().starts_with("bind:")
|
||||
&& !attr.key.to_string().starts_with("clone:")
|
||||
&& !attr.key.to_string().starts_with("on:")
|
||||
})
|
||||
.map(|attr| {
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value()
|
||||
.map(|v| {
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
|
||||
quote! {
|
||||
.#name(#[allow(unused_braces)] #value)
|
||||
}
|
||||
});
|
||||
|
||||
let items_to_bind = attrs
|
||||
.clone()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("bind:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let items_to_clone = attrs
|
||||
.clone()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("clone:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let events = attrs
|
||||
.filter(|attr| attr.key.to_string().starts_with("on:"))
|
||||
.map(|attr| {
|
||||
let (event_type, handler) = event_from_attribute_node(attr, true);
|
||||
|
||||
quote! {
|
||||
.on(#event_type, #handler)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut slots = HashMap::new();
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let marker = format!("<{component_name}/>-children");
|
||||
let view_marker = quote! { .with_view_marker(#marker) };
|
||||
} else {
|
||||
let view_marker = quote! {};
|
||||
}
|
||||
}
|
||||
|
||||
let children = fragment_to_tokens(
|
||||
cx,
|
||||
span,
|
||||
&node.children,
|
||||
true,
|
||||
TagType::Unknown,
|
||||
Some(&mut slots),
|
||||
global_class,
|
||||
None,
|
||||
);
|
||||
|
||||
if let Some(children) = children {
|
||||
let bindables =
|
||||
items_to_bind.iter().map(|ident| quote! { #ident, });
|
||||
|
||||
let clonables = items_to_clone
|
||||
.iter()
|
||||
.map(|ident| quote! { let #ident = #ident.clone(); });
|
||||
|
||||
if bindables.len() > 0 {
|
||||
quote! {
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
move |#cx, #(#bindables)*| #children #view_marker
|
||||
})
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
Box::new(move |#cx| #children #view_marker)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
};
|
||||
|
||||
let slots = slots.drain().map(|(slot, values)| {
|
||||
let slot = Ident::new(&slot, span);
|
||||
if values.len() > 1 {
|
||||
quote! {
|
||||
.#slot(vec![
|
||||
#(#values)*
|
||||
])
|
||||
}
|
||||
} else {
|
||||
let value = &values[0];
|
||||
quote! { .#slot(#value) }
|
||||
}
|
||||
});
|
||||
|
||||
#[allow(unused_mut)] // used in debug
|
||||
let mut component = quote! {
|
||||
::leptos::component_view(
|
||||
&#name,
|
||||
#cx,
|
||||
::leptos::component_props_builder(&#name)
|
||||
#(#props)*
|
||||
#(#slots)*
|
||||
#children
|
||||
.build()
|
||||
)
|
||||
};
|
||||
|
||||
// (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
|
||||
} else {
|
||||
quote! {
|
||||
#component.into_view(#cx)
|
||||
#(#events)*
|
||||
}
|
||||
}
|
||||
}
|
||||
152
leptos_macro/src/view/ide_helper.rs
Normal file
152
leptos_macro/src/view/ide_helper.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use leptos_hot_reload::parsing::is_component_tag_name;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::quote;
|
||||
use rstml::node::{NodeElement, NodeName};
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
/// Helper type to emit semantic info about tags, for IDE.
|
||||
/// Implement `IntoIterator` with `Item="let _ = foo::docs;"`.
|
||||
///
|
||||
/// `IdeTagHelper` uses warning instead of errors everywhere,
|
||||
/// it's aim is to add usability, not introduce additional typecheck in `view`/`template` code.
|
||||
/// On stable `emit_warning` don't produce anything.
|
||||
pub(crate) struct IdeTagHelper(Vec<TokenStream>);
|
||||
|
||||
// TODO: Unhandled cases:
|
||||
// - svg::div, my_elements::foo - tags with custom paths, that doesnt look like component
|
||||
// - my_component::Foo - components with custom paths
|
||||
// - html:div - tags punctuated by `:`
|
||||
// - {div}, {"div"} - any rust expression
|
||||
impl IdeTagHelper {
|
||||
pub fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
/// Save stmts for tag name.
|
||||
/// Emit warning if tag is component.
|
||||
pub fn save_tag_completion(&mut self, name: &NodeName) {
|
||||
let tag_name = name.to_string();
|
||||
if is_component_tag_name(&tag_name) {
|
||||
proc_macro_error::emit_warning!(
|
||||
name.span(),
|
||||
"BUG: Component tag is used in regular tag completion."
|
||||
);
|
||||
}
|
||||
for path in Self::completion_stmts(name) {
|
||||
self.0.push(quote! {
|
||||
let _ = #path;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Save stmts for open and close tags.
|
||||
/// Emit warning if tag is component.
|
||||
pub fn save_element_completion(&mut self, node: &NodeElement) {
|
||||
self.save_tag_completion(node.name());
|
||||
if let Some(close_tag) = node.close_tag.as_ref().map(|c| &c.name) {
|
||||
self.save_tag_completion(close_tag)
|
||||
}
|
||||
}
|
||||
|
||||
/* 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
|
||||
/// current builder pattern, this clones the whole component constructor,
|
||||
/// but it will never be used.
|
||||
///
|
||||
/// ```no_build
|
||||
/// if false {
|
||||
/// close_tag(cx, unreachable!())
|
||||
/// }
|
||||
/// else {
|
||||
/// open_tag(open_tag.props().slots().children().build())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn add_component_completion(
|
||||
cx: &Ident,
|
||||
component: &mut TokenStream,
|
||||
node: &NodeElement,
|
||||
) {
|
||||
// emit ide helper info
|
||||
if let Some(close_tag) = node.close_tag.as_ref().map(|c| &c.name) {
|
||||
*component = quote! {
|
||||
{
|
||||
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`.
|
||||
fn create_regular_tag_fn_path(name: &Ident) -> TokenStream {
|
||||
let tag_name = name.to_string();
|
||||
let namespace = if crate::view::is_svg_element(&tag_name) {
|
||||
quote! { ::leptos::leptos_dom::svg }
|
||||
} else if crate::view::is_math_ml_element(&tag_name) {
|
||||
quote! { ::leptos::leptos_dom::math }
|
||||
} else {
|
||||
// todo: check is html, and emit_warning in case of custom tag
|
||||
quote! { ::leptos::leptos_dom::html }
|
||||
};
|
||||
quote!( #namespace::#name)
|
||||
}
|
||||
|
||||
/// Returns `syn::Path`-like `TokenStream` to the `custom` section in docs.
|
||||
fn create_custom_tag_fn_path(span: Span) -> TokenStream {
|
||||
let custom_ident = Ident::new("custom", span);
|
||||
quote! {leptos::leptos_dom::html::#custom_ident::<leptos::leptos_dom::html::Custom>}
|
||||
}
|
||||
|
||||
// Extract from NodeName completion idents.
|
||||
// Custom tags (like foo-bar-baz) is mapped
|
||||
// to vec!["custom", "custom",.. ] for each token in tag, even for "-".
|
||||
// Only last ident from `Path` is used.
|
||||
fn completion_stmts(name: &NodeName) -> Vec<TokenStream> {
|
||||
match name {
|
||||
NodeName::Block(_) => vec![],
|
||||
NodeName::Punctuated(c) => c
|
||||
.pairs()
|
||||
.flat_map(|c| {
|
||||
let mut idents =
|
||||
vec![Self::create_custom_tag_fn_path(c.value().span())];
|
||||
if let Some(p) = c.punct() {
|
||||
idents.push(Self::create_custom_tag_fn_path(p.span()))
|
||||
}
|
||||
idents
|
||||
})
|
||||
.collect(),
|
||||
NodeName::Path(e) => e
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.map(|p| &p.ident)
|
||||
.map(Self::create_regular_tag_fn_path)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for IdeTagHelper {
|
||||
type Item = TokenStream;
|
||||
type IntoIter = <Vec<TokenStream> as IntoIterator>::IntoIter;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
548
leptos_macro/src/view/mod.rs
Normal file
548
leptos_macro/src/view/mod.rs
Normal file
@@ -0,0 +1,548 @@
|
||||
use crate::{attribute_value, Mode};
|
||||
use convert_case::{Case::Snake, Casing};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned};
|
||||
use rstml::node::{KeyedAttribute, Node, NodeElement, NodeName};
|
||||
use syn::{spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprPath, Lit};
|
||||
|
||||
pub mod client_builder;
|
||||
pub mod client_template;
|
||||
pub mod component_builder;
|
||||
pub mod ide_helper;
|
||||
pub mod server_template;
|
||||
pub mod slot_helper;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(crate) use ide_helper::*;
|
||||
|
||||
pub(crate) fn render_view(
|
||||
cx: &Ident,
|
||||
nodes: &[Node],
|
||||
mode: Mode,
|
||||
global_class: Option<&TokenTree>,
|
||||
call_site: Option<String>,
|
||||
) -> TokenStream {
|
||||
let empty = {
|
||||
let span = Span::call_site();
|
||||
quote_spanned! {
|
||||
span => leptos::leptos_dom::Unit
|
||||
}
|
||||
};
|
||||
|
||||
if mode == Mode::Ssr {
|
||||
match nodes.len() {
|
||||
0 => empty,
|
||||
1 => server_template::root_node_to_tokens_ssr(
|
||||
cx,
|
||||
&nodes[0],
|
||||
global_class,
|
||||
call_site,
|
||||
),
|
||||
_ => server_template::fragment_to_tokens_ssr(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
nodes,
|
||||
global_class,
|
||||
call_site,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
match nodes.len() {
|
||||
0 => empty,
|
||||
1 => client_builder::node_to_tokens(
|
||||
cx,
|
||||
&nodes[0],
|
||||
client_builder::TagType::Unknown,
|
||||
None,
|
||||
global_class,
|
||||
call_site,
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
_ => client_builder::fragment_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
nodes,
|
||||
true,
|
||||
client_builder::TagType::Unknown,
|
||||
None,
|
||||
global_class,
|
||||
call_site,
|
||||
)
|
||||
.unwrap_or(empty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep list alphabetized for binary search
|
||||
const TYPED_EVENTS: [&str; 126] = [
|
||||
"DOMContentLoaded",
|
||||
"abort",
|
||||
"afterprint",
|
||||
"animationcancel",
|
||||
"animationend",
|
||||
"animationiteration",
|
||||
"animationstart",
|
||||
"auxclick",
|
||||
"beforeinput",
|
||||
"beforeprint",
|
||||
"beforeunload",
|
||||
"blur",
|
||||
"canplay",
|
||||
"canplaythrough",
|
||||
"change",
|
||||
"click",
|
||||
"close",
|
||||
"compositionend",
|
||||
"compositionstart",
|
||||
"compositionupdate",
|
||||
"contextmenu",
|
||||
"copy",
|
||||
"cuechange",
|
||||
"cut",
|
||||
"dblclick",
|
||||
"devicemotion",
|
||||
"deviceorientation",
|
||||
"drag",
|
||||
"dragend",
|
||||
"dragenter",
|
||||
"dragleave",
|
||||
"dragover",
|
||||
"dragstart",
|
||||
"drop",
|
||||
"durationchange",
|
||||
"emptied",
|
||||
"ended",
|
||||
"error",
|
||||
"focus",
|
||||
"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",
|
||||
"mousemove",
|
||||
"mouseout",
|
||||
"mouseover",
|
||||
"mouseup",
|
||||
"offline",
|
||||
"online",
|
||||
"orientationchange",
|
||||
"pagehide",
|
||||
"pageshow",
|
||||
"paste",
|
||||
"pause",
|
||||
"play",
|
||||
"playing",
|
||||
"pointercancel",
|
||||
"pointerdown",
|
||||
"pointerenter",
|
||||
"pointerleave",
|
||||
"pointerlockchange",
|
||||
"pointerlockerror",
|
||||
"pointermove",
|
||||
"pointerout",
|
||||
"pointerover",
|
||||
"pointerup",
|
||||
"popstate",
|
||||
"progress",
|
||||
"ratechange",
|
||||
"readystatechange",
|
||||
"rejectionhandled",
|
||||
"reset",
|
||||
"resize",
|
||||
"scroll",
|
||||
"securitypolicyviolation",
|
||||
"seeked",
|
||||
"seeking",
|
||||
"select",
|
||||
"selectionchange",
|
||||
"selectstart",
|
||||
"slotchange",
|
||||
"stalled",
|
||||
"storage",
|
||||
"submit",
|
||||
"suspend",
|
||||
"timeupdate",
|
||||
"toggle",
|
||||
"touchcancel",
|
||||
"touchend",
|
||||
"touchmove",
|
||||
"touchstart",
|
||||
"transitioncancel",
|
||||
"transitionend",
|
||||
"transitionrun",
|
||||
"transitionstart",
|
||||
"unhandledrejection",
|
||||
"unload",
|
||||
"visibilitychange",
|
||||
"volumechange",
|
||||
"waiting",
|
||||
"webkitanimationend",
|
||||
"webkitanimationiteration",
|
||||
"webkitanimationstart",
|
||||
"webkittransitionend",
|
||||
"wheel",
|
||||
];
|
||||
|
||||
const CUSTOM_EVENT: &str = "Custom";
|
||||
|
||||
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
|
||||
let (name, is_force_undelegated) = parse_event(name);
|
||||
|
||||
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");
|
||||
};
|
||||
|
||||
let event_type = if is_custom {
|
||||
quote! { Custom::new(#name) }
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
(event_type, is_custom, is_force_undelegated)
|
||||
}
|
||||
|
||||
fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
|
||||
match expr {
|
||||
syn::Expr::Block(block) => block.block.stmts.last().and_then(|stmt| {
|
||||
if let syn::Stmt::Expr(expr, ..) = stmt {
|
||||
expr_to_ident(expr)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
syn::Expr::Path(path) => Some(path),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_to_snake_case(name: String) -> String {
|
||||
if !name.is_case(Snake) {
|
||||
name.to_case(Snake)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fn is_custom_element(tag: &str) -> bool {
|
||||
tag.contains('-')
|
||||
}
|
||||
|
||||
fn is_self_closing(node: &NodeElement) -> bool {
|
||||
// self-closing tags
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
||||
[
|
||||
"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 {
|
||||
let mut chars = tag_name.chars();
|
||||
let first = chars.next();
|
||||
let underscore = if tag_name == "option" { "_" } else { "" };
|
||||
first
|
||||
.map(|f| f.to_ascii_uppercase())
|
||||
.into_iter()
|
||||
.chain(chars)
|
||||
.collect::<String>()
|
||||
+ underscore
|
||||
}
|
||||
|
||||
fn is_svg_element(tag: &str) -> bool {
|
||||
// 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 {
|
||||
// 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 {
|
||||
tag == "a" || tag == "script" || tag == "title"
|
||||
}
|
||||
|
||||
fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
if let Some(event_name) = event_name.strip_suffix(":undelegated") {
|
||||
(event_name, true)
|
||||
} else {
|
||||
(event_name, false)
|
||||
}
|
||||
}
|
||||
|
||||
fn fancy_class_name<'a>(
|
||||
name: &str,
|
||||
cx: &Ident,
|
||||
node: &'a KeyedAttribute,
|
||||
) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex class names:
|
||||
// e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)`
|
||||
if name == "class" {
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let class = quote_spanned! {
|
||||
span => .class
|
||||
};
|
||||
let class_name = &tuple.elems[0];
|
||||
let class_name = if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) = class_name
|
||||
{
|
||||
s.value()
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
class_name.span(),
|
||||
"class name must be a string literal"
|
||||
);
|
||||
Default::default()
|
||||
};
|
||||
let value = &tuple.elems[1];
|
||||
return Some((
|
||||
quote! {
|
||||
#class(#class_name, (#cx, #value))
|
||||
},
|
||||
class_name,
|
||||
value,
|
||||
));
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
tuple.span(),
|
||||
"class tuples must have two elements."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
|
||||
match tag_name {
|
||||
NodeName::Path(path) => path
|
||||
.path
|
||||
.segments
|
||||
.iter()
|
||||
.last()
|
||||
.map(|segment| segment.ident.clone())
|
||||
.expect("element needs to have a name"),
|
||||
NodeName::Block(_) => {
|
||||
let span = tag_name.span();
|
||||
proc_macro_error::emit_error!(
|
||||
span,
|
||||
"blocks not allowed in tag-name position"
|
||||
);
|
||||
Ident::new("", span)
|
||||
}
|
||||
_ => Ident::new(
|
||||
&tag_name.to_string().replace(['-', ':'], "_"),
|
||||
tag_name.span(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn fancy_style_name<'a>(
|
||||
name: &str,
|
||||
cx: &Ident,
|
||||
node: &'a KeyedAttribute,
|
||||
) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex dynamic style names:
|
||||
if name == "style" {
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let style = quote_spanned! {
|
||||
span => .style
|
||||
};
|
||||
let style_name = &tuple.elems[0];
|
||||
let style_name = if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) = style_name
|
||||
{
|
||||
s.value()
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
style_name.span(),
|
||||
"style name must be a string literal"
|
||||
);
|
||||
Default::default()
|
||||
};
|
||||
let value = &tuple.elems[1];
|
||||
return Some((
|
||||
quote! {
|
||||
#style(#style_name, (#cx, #value))
|
||||
},
|
||||
style_name,
|
||||
value,
|
||||
));
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
tuple.span(),
|
||||
"style tuples must have two elements."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn event_from_attribute_node(
|
||||
attr: &KeyedAttribute,
|
||||
force_undelegated: bool,
|
||||
) -> (TokenStream, &Expr) {
|
||||
let event_name = attr
|
||||
.key
|
||||
.to_string()
|
||||
.strip_prefix("on:")
|
||||
.expect("expected `on:` directive")
|
||||
.to_owned();
|
||||
|
||||
let handler = attribute_value(attr);
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let (name, name_undelegated) = parse_event(&event_name);
|
||||
|
||||
let event_type = TYPED_EVENTS
|
||||
.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");
|
||||
};
|
||||
|
||||
let event_type = if force_undelegated || name_undelegated {
|
||||
quote! { ::leptos::leptos_dom::ev::undelegated(::leptos::leptos_dom::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::leptos_dom::ev::#event_type }
|
||||
};
|
||||
(event_type, handler)
|
||||
}
|
||||
707
leptos_macro/src/view/server_template.rs
Normal file
707
leptos_macro/src/view/server_template.rs
Normal file
@@ -0,0 +1,707 @@
|
||||
use super::{
|
||||
camel_case_tag_name,
|
||||
component_builder::component_to_tokens,
|
||||
fancy_class_name, fancy_style_name,
|
||||
ide_helper::IdeTagHelper,
|
||||
is_custom_element, is_math_ml_element, is_self_closing, is_svg_element,
|
||||
parse_event_name,
|
||||
slot_helper::{get_slot, slot_to_tokens},
|
||||
};
|
||||
use crate::attribute_value;
|
||||
use leptos_hot_reload::parsing::{
|
||||
block_to_primitive_expression, is_component_node, value_to_string,
|
||||
};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use quote::quote;
|
||||
use rstml::node::{
|
||||
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
pub(crate) enum SsrElementChunks {
|
||||
String {
|
||||
template: String,
|
||||
holes: Vec<TokenStream>,
|
||||
},
|
||||
View(TokenStream),
|
||||
}
|
||||
|
||||
pub(crate) fn root_node_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &Node,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
match node {
|
||||
Node::Fragment(fragment) => fragment_to_tokens_ssr(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
global_class,
|
||||
view_marker,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
quote! {
|
||||
leptos::leptos_dom::html::text(#node)
|
||||
}
|
||||
}
|
||||
Node::RawText(r) => {
|
||||
let text = r.to_string_best();
|
||||
let text = syn::LitStr::new(&text, r.span());
|
||||
quote! {
|
||||
leptos::leptos_dom::html::text(#text)
|
||||
}
|
||||
}
|
||||
Node::Block(node) => {
|
||||
quote! {
|
||||
#node
|
||||
}
|
||||
}
|
||||
Node::Element(node) => {
|
||||
root_element_to_tokens_ssr(cx, node, global_class, view_marker)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fragment_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
_span: Span,
|
||||
nodes: &[Node],
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> TokenStream {
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let nodes = nodes.iter().map(|node| {
|
||||
let node = root_node_to_tokens_ssr(cx, node, global_class, None);
|
||||
quote! {
|
||||
#node.into_view(#cx)
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::lazy(|| [
|
||||
#(#nodes),*
|
||||
].to_vec())
|
||||
#view_marker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn root_element_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
// TODO: simplify, this is checked twice, second time in `element_to_tokens_ssr` body
|
||||
if is_component_node(node) {
|
||||
if let Some(slot) = get_slot(node) {
|
||||
slot_to_tokens(cx, node, slot, None, global_class);
|
||||
None
|
||||
} else {
|
||||
Some(component_to_tokens(cx, node, global_class))
|
||||
}
|
||||
} else {
|
||||
let mut stmts_for_ide = IdeTagHelper::new();
|
||||
let mut exprs_for_compiler = Vec::<TokenStream>::new();
|
||||
|
||||
let mut template = String::new();
|
||||
let mut holes = Vec::new();
|
||||
let mut chunks = Vec::new();
|
||||
element_to_tokens_ssr(
|
||||
cx,
|
||||
node,
|
||||
None,
|
||||
&mut template,
|
||||
&mut holes,
|
||||
&mut chunks,
|
||||
&mut stmts_for_ide,
|
||||
&mut exprs_for_compiler,
|
||||
true,
|
||||
global_class,
|
||||
);
|
||||
|
||||
// push final chunk
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String { template, holes })
|
||||
}
|
||||
|
||||
let chunks = chunks.into_iter().map(|chunk| match chunk {
|
||||
SsrElementChunks::String { template, holes } => {
|
||||
if holes.is_empty() {
|
||||
let template = template.replace("\\{", "{").replace("\\}", "}");
|
||||
quote! {
|
||||
leptos::leptos_dom::html::StringOrView::String(#template.into())
|
||||
}
|
||||
} else {
|
||||
let template = template.replace("\\{", "{{").replace("\\}", "}}");
|
||||
quote! {
|
||||
leptos::leptos_dom::html::StringOrView::String(
|
||||
format!(
|
||||
#template,
|
||||
#(#holes),*
|
||||
)
|
||||
.into()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SsrElementChunks::View(view) => {
|
||||
quote! {
|
||||
#[allow(unused_braces)]
|
||||
{
|
||||
let view = #view;
|
||||
leptos::leptos_dom::html::StringOrView::View(std::rc::Rc::new(move || view.clone()))
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let tag_name = node.name().to_string();
|
||||
let is_custom_element = is_custom_element(&tag_name);
|
||||
|
||||
// Use any other span instead of node.name.span(), to avoid misunderstanding in IDE.
|
||||
// We can use open_tag.span(), to provide similar (to name span) diagnostic
|
||||
// in case of expansion error, but it will also highlight "<" token.
|
||||
let typed_element_name = if is_custom_element {
|
||||
Ident::new("Custom", Span::call_site())
|
||||
} else {
|
||||
let camel_cased = camel_case_tag_name(
|
||||
tag_name
|
||||
.trim_start_matches("svg::")
|
||||
.trim_start_matches("math::")
|
||||
.trim_end_matches('_'),
|
||||
);
|
||||
Ident::new(&camel_cased, Span::call_site())
|
||||
};
|
||||
let typed_element_name = if is_svg_element(&tag_name) {
|
||||
quote! { svg::#typed_element_name }
|
||||
} else if is_math_ml_element(&tag_name) {
|
||||
quote! { math::#typed_element_name }
|
||||
} else {
|
||||
quote! { html::#typed_element_name }
|
||||
};
|
||||
let full_name = if is_custom_element {
|
||||
quote! {
|
||||
::leptos::leptos_dom::html::Custom::new(#tag_name)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::leptos_dom::#typed_element_name::default()
|
||||
}
|
||||
};
|
||||
let view_marker = if let Some(marker) = view_marker {
|
||||
quote! { .with_view_marker(#marker) }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let stmts_for_ide = stmts_for_ide.into_iter();
|
||||
Some(quote! {
|
||||
{
|
||||
#(#stmts_for_ide)*
|
||||
#(#exprs_for_compiler)*
|
||||
::leptos::HtmlElement::from_chunks(#cx, #full_name, [#(#chunks),*])#view_marker
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn element_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
chunks: &mut Vec<SsrElementChunks>,
|
||||
stmts_for_ide: &mut IdeTagHelper,
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
is_root: bool,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
if is_component_node(node) {
|
||||
if let Some(slot) = get_slot(node) {
|
||||
slot_to_tokens(cx, node, slot, parent_slots, global_class);
|
||||
return;
|
||||
}
|
||||
|
||||
let component = component_to_tokens(cx, node, global_class);
|
||||
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String {
|
||||
template: std::mem::take(template),
|
||||
holes: std::mem::take(holes),
|
||||
})
|
||||
}
|
||||
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#component}.into_view(#cx)
|
||||
}));
|
||||
} else {
|
||||
let tag_name = node.name().to_string();
|
||||
let tag_name = tag_name
|
||||
.trim_start_matches("svg::")
|
||||
.trim_start_matches("math::")
|
||||
.trim_end_matches('_');
|
||||
let is_script_or_style = tag_name == "script" || tag_name == "style";
|
||||
template.push('<');
|
||||
template.push_str(tag_name);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
stmts_for_ide.save_element_completion(node);
|
||||
|
||||
let mut inner_html = None;
|
||||
|
||||
for attr in node.attributes() {
|
||||
if let NodeAttribute::Attribute(attr) = attr {
|
||||
inner_html = attribute_to_tokens_ssr(
|
||||
cx,
|
||||
attr,
|
||||
template,
|
||||
holes,
|
||||
exprs_for_compiler,
|
||||
global_class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// insert hydration ID
|
||||
let hydration_id = if is_root {
|
||||
quote! { ::leptos::leptos_dom::HydrationCtx::peek() }
|
||||
} else {
|
||||
quote! { ::leptos::leptos_dom::HydrationCtx::id() }
|
||||
};
|
||||
match node
|
||||
.attributes()
|
||||
.iter()
|
||||
.find(|node| matches!(node, NodeAttribute::Attribute(attr) if attr.key.to_string() == "id"))
|
||||
{
|
||||
Some(_) => {
|
||||
template.push_str(" leptos-hk=\"_{}\"");
|
||||
}
|
||||
None => {
|
||||
template.push_str(" id=\"_{}\"");
|
||||
}
|
||||
}
|
||||
holes.push(hydration_id);
|
||||
|
||||
set_class_attribute_ssr(cx, node, template, holes, global_class);
|
||||
set_style_attribute_ssr(cx, node, template, holes);
|
||||
|
||||
if is_self_closing(node) {
|
||||
template.push_str("/>");
|
||||
} else {
|
||||
template.push('>');
|
||||
|
||||
if let Some(inner_html) = inner_html {
|
||||
template.push_str("{}");
|
||||
let value = inner_html;
|
||||
|
||||
holes.push(quote! {
|
||||
(#value).into_attribute(#cx).as_nameless_value_string().unwrap_or_default()
|
||||
})
|
||||
} else {
|
||||
for child in &node.children {
|
||||
match child {
|
||||
Node::Element(child) => {
|
||||
element_to_tokens_ssr(
|
||||
cx,
|
||||
child,
|
||||
None,
|
||||
template,
|
||||
holes,
|
||||
chunks,
|
||||
stmts_for_ide,
|
||||
exprs_for_compiler,
|
||||
false,
|
||||
global_class,
|
||||
);
|
||||
}
|
||||
Node::Text(text) => {
|
||||
let value = text.value_string();
|
||||
let value = if is_script_or_style {
|
||||
value.into()
|
||||
} else {
|
||||
html_escape::encode_safe(&value)
|
||||
};
|
||||
template.push_str(
|
||||
&value.replace('{', "\\{").replace('}', "\\}"),
|
||||
);
|
||||
}
|
||||
Node::RawText(r) => {
|
||||
let value = r.to_string_best();
|
||||
let value = if is_script_or_style {
|
||||
value.into()
|
||||
} else {
|
||||
html_escape::encode_safe(&value)
|
||||
};
|
||||
template.push_str(
|
||||
&value.replace('{', "\\{").replace('}', "\\}"),
|
||||
);
|
||||
}
|
||||
Node::Block(NodeBlock::ValidBlock(block)) => {
|
||||
if let Some(value) =
|
||||
block_to_primitive_expression(block)
|
||||
.and_then(value_to_string)
|
||||
{
|
||||
template.push_str(&value);
|
||||
} else {
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String {
|
||||
template: std::mem::take(template),
|
||||
holes: std::mem::take(holes),
|
||||
})
|
||||
}
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#block}.into_view(#cx)
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Keep invalid blocks for faster IDE diff (on user type)
|
||||
Node::Block(block @ NodeBlock::Invalid { .. }) => {
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#block}.into_view(#cx)
|
||||
}));
|
||||
}
|
||||
Node::Fragment(_) => abort!(
|
||||
Span::call_site(),
|
||||
"You can't nest a fragment inside an element."
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template.push_str("</");
|
||||
template.push_str(tag_name);
|
||||
template.push('>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns `inner_html`
|
||||
fn attribute_to_tokens_ssr<'a>(
|
||||
cx: &Ident,
|
||||
attr: &'a KeyedAttribute,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> Option<&'a syn::Expr> {
|
||||
let name = attr.key.to_string();
|
||||
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
|
||||
// ignore refs on SSR
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = attribute_value(attr);
|
||||
let (event_type, _, _) = parse_event_name(name);
|
||||
|
||||
exprs_for_compiler.push(quote! {
|
||||
leptos::leptos_dom::helpers::ssr_event_listener(::leptos::ev::#event_type, #handler);
|
||||
})
|
||||
} else if name.strip_prefix("prop:").is_some()
|
||||
|| name.strip_prefix("class:").is_some()
|
||||
|| name.strip_prefix("style:").is_some()
|
||||
{
|
||||
// ignore props for SSR
|
||||
// ignore classes and sdtyles: we'll handle these separately
|
||||
if name.starts_with("prop:") {
|
||||
let value = attr.value();
|
||||
exprs_for_compiler.push(quote! {
|
||||
#[allow(unused_braces)]
|
||||
{ _ = #value; }
|
||||
});
|
||||
}
|
||||
} else if name == "inner_html" {
|
||||
return attr.value();
|
||||
} else {
|
||||
let name = name.replacen("attr:", "", 1);
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if name == "class"
|
||||
&& global_class.is_some()
|
||||
&& attr.value().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = attr.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
|
||||
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
|
||||
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
|
||||
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
|
||||
};
|
||||
|
||||
if name != "class" && name != "style" {
|
||||
template.push(' ');
|
||||
|
||||
if let Some(value) = attr.value() {
|
||||
if let Some(value) = value_to_string(value) {
|
||||
template.push_str(&name);
|
||||
template.push_str("=\"");
|
||||
template.push_str(&html_escape::encode_quoted_attribute(
|
||||
&value,
|
||||
));
|
||||
template.push('"');
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
holes.push(quote! {
|
||||
&{#value}.into_attribute(#cx)
|
||||
.as_nameless_value_string()
|
||||
.map(|a| format!("{}=\"{}\"", #name, leptos::leptos_dom::ssr::escape_attr(&a)))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
template.push_str(&name);
|
||||
}
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn set_class_attribute_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
let (static_global_class, dyn_global_class) = match global_class {
|
||||
Some(TokenTree::Literal(lit)) => {
|
||||
let str = lit.to_string();
|
||||
// A lit here can be a string, byte_string, char, byte_char, int or float.
|
||||
// If it's a string we remove the quotes so folks can use them directly
|
||||
// without needing braces. E.g. view!{cx, class="my-class", ... }
|
||||
let str = if str.starts_with('"') && str.ends_with('"') {
|
||||
str[1..str.len() - 1].to_string()
|
||||
} else {
|
||||
str
|
||||
};
|
||||
(str, None)
|
||||
}
|
||||
None => (String::new(), None),
|
||||
Some(val) => (String::new(), Some(val)),
|
||||
};
|
||||
let static_class_attr = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
NodeAttribute::Attribute(attr)
|
||||
if attr.key.to_string() == "class" =>
|
||||
{
|
||||
attr.value().and_then(value_to_string)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.chain(Some(static_global_class))
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
let dyn_class_attr = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
if let NodeAttribute::Attribute(a) = a {
|
||||
if a.key.to_string() == "class" {
|
||||
if a.value().and_then(value_to_string).is_some()
|
||||
|| fancy_class_name(&a.key.to_string(), cx, a).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), a.value()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let class_attrs = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "class" {
|
||||
return if let Some((_, name, value)) =
|
||||
fancy_class_name(&name, cx, node)
|
||||
{
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
if name.starts_with("class:") || name.starts_with("class-") {
|
||||
let name = if name.starts_with("class:") {
|
||||
name.replacen("class:", "", 1)
|
||||
} else if name.starts_with("class-") {
|
||||
name.replacen("class-", "", 1)
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let value = attribute_value(node);
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !static_class_attr.is_empty()
|
||||
|| !dyn_class_attr.is_empty()
|
||||
|| !class_attrs.is_empty()
|
||||
|| dyn_global_class.is_some()
|
||||
{
|
||||
template.push_str(" class=\"");
|
||||
|
||||
template.push_str(&html_escape::encode_quoted_attribute(
|
||||
&static_class_attr,
|
||||
));
|
||||
|
||||
for (_span, value) in dyn_class_attr {
|
||||
if let Some(value) = value {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! {
|
||||
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (_span, name, value) in &class_attrs {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! {
|
||||
(#cx, #value).into_class(#cx).as_value_string(#name)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(dyn_global_class) = dyn_global_class {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! { #dyn_global_class });
|
||||
}
|
||||
|
||||
template.push('"');
|
||||
}
|
||||
}
|
||||
|
||||
fn set_style_attribute_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
) {
|
||||
let static_style_attr = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
NodeAttribute::Attribute(attr)
|
||||
if attr.key.to_string() == "style" =>
|
||||
{
|
||||
attr.value().and_then(value_to_string)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.next()
|
||||
.map(|style| format!("{style};"));
|
||||
|
||||
let dyn_style_attr = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
if let NodeAttribute::Attribute(a) = a {
|
||||
if a.key.to_string() == "style" {
|
||||
if a.value().and_then(value_to_string).is_some()
|
||||
|| fancy_style_name(&a.key.to_string(), cx, a).is_some()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), a.value()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let style_attrs = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "style" {
|
||||
return if let Some((_, name, value)) =
|
||||
fancy_style_name(&name, cx, node)
|
||||
{
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
if name.starts_with("style:") || name.starts_with("style-") {
|
||||
let name = if name.starts_with("style:") {
|
||||
name.replacen("style:", "", 1)
|
||||
} else if name.starts_with("style-") {
|
||||
name.replacen("style-", "", 1)
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let value = attribute_value(node);
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if static_style_attr.is_some()
|
||||
|| !dyn_style_attr.is_empty()
|
||||
|| !style_attrs.is_empty()
|
||||
{
|
||||
template.push_str(" style=\"");
|
||||
|
||||
template.push_str(&static_style_attr.unwrap_or_default());
|
||||
|
||||
for (_span, value) in dyn_style_attr {
|
||||
if let Some(value) = value {
|
||||
template.push_str(" {};");
|
||||
holes.push(quote! {
|
||||
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (_span, name, value) in &style_attrs {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! {
|
||||
(#cx, #value).into_style(#cx).as_value_string(#name).unwrap_or_default()
|
||||
});
|
||||
}
|
||||
|
||||
template.push('"');
|
||||
}
|
||||
}
|
||||
191
leptos_macro/src/view/slot_helper.rs
Normal file
191
leptos_macro/src/view/slot_helper.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use super::{
|
||||
client_builder::{fragment_to_tokens, TagType},
|
||||
convert_to_snake_case, ident_from_tag_name,
|
||||
};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote};
|
||||
use rstml::node::{KeyedAttribute, NodeAttribute, NodeElement};
|
||||
use std::collections::HashMap;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
pub(crate) fn slot_to_tokens(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
slot: &KeyedAttribute,
|
||||
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
let name = slot.key.to_string();
|
||||
let name = name.trim();
|
||||
let name = convert_to_snake_case(if name.starts_with("slot:") {
|
||||
name.replacen("slot:", "", 1)
|
||||
} else {
|
||||
node.name().to_string()
|
||||
});
|
||||
|
||||
let component_name = ident_from_tag_name(node.name());
|
||||
let span = node.name().span();
|
||||
|
||||
let Some(parent_slots) = parent_slots else {
|
||||
proc_macro_error::emit_error!(
|
||||
span,
|
||||
"slots cannot be used inside HTML elements"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let attrs = node.attributes().iter().filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
if is_slot(node) {
|
||||
None
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let props = attrs
|
||||
.clone()
|
||||
.filter(|attr| {
|
||||
!attr.key.to_string().starts_with("bind:")
|
||||
&& !attr.key.to_string().starts_with("clone:")
|
||||
})
|
||||
.map(|attr| {
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value()
|
||||
.map(|v| {
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
|
||||
quote! {
|
||||
.#name(#[allow(unused_braces)] #value)
|
||||
}
|
||||
});
|
||||
|
||||
let items_to_bind = attrs
|
||||
.clone()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("bind:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let items_to_clone = attrs
|
||||
.clone()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("clone:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut slots = HashMap::new();
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let marker = format!("<{component_name}/>-children");
|
||||
let view_marker = quote! { .with_view_marker(#marker) };
|
||||
} else {
|
||||
let view_marker = quote! {};
|
||||
}
|
||||
}
|
||||
|
||||
let children = fragment_to_tokens(
|
||||
cx,
|
||||
span,
|
||||
&node.children,
|
||||
true,
|
||||
TagType::Unknown,
|
||||
Some(&mut slots),
|
||||
global_class,
|
||||
None,
|
||||
);
|
||||
|
||||
if let Some(children) = children {
|
||||
let bindables =
|
||||
items_to_bind.iter().map(|ident| quote! { #ident, });
|
||||
|
||||
let clonables = items_to_clone
|
||||
.iter()
|
||||
.map(|ident| quote! { let #ident = #ident.clone(); });
|
||||
|
||||
if bindables.len() > 0 {
|
||||
quote! {
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
move |#cx, #(#bindables)*| #children #view_marker
|
||||
})
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
Box::new(move |#cx| #children #view_marker)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
};
|
||||
|
||||
let slots = slots.drain().map(|(slot, values)| {
|
||||
let slot = Ident::new(&slot, span);
|
||||
if values.len() > 1 {
|
||||
quote! {
|
||||
.#slot([
|
||||
#(#values)*
|
||||
].to_vec())
|
||||
}
|
||||
} else {
|
||||
let value = &values[0];
|
||||
quote! { .#slot(#value) }
|
||||
}
|
||||
});
|
||||
|
||||
let slot = quote! {
|
||||
#component_name::builder()
|
||||
#(#props)*
|
||||
#(#slots)*
|
||||
#children
|
||||
.build()
|
||||
.into(),
|
||||
};
|
||||
|
||||
parent_slots
|
||||
.entry(name)
|
||||
.and_modify(|entry| entry.push(slot.clone()))
|
||||
.or_insert(vec![slot]);
|
||||
}
|
||||
|
||||
pub(crate) fn is_slot(node: &KeyedAttribute) -> bool {
|
||||
let key = node.key.to_string();
|
||||
let key = key.trim();
|
||||
key == "slot" || key.starts_with("slot:")
|
||||
}
|
||||
|
||||
pub(crate) fn get_slot(node: &NodeElement) -> Option<&KeyedAttribute> {
|
||||
node.attributes().iter().find_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
if is_slot(node) {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: leptos_macro/src/view/tests.rs
|
||||
expression: pretty(result)
|
||||
---
|
||||
fn view() {
|
||||
::leptos::component_view(
|
||||
&SimpleCounter,
|
||||
cx,
|
||||
::leptos::component_props_builder(&SimpleCounter)
|
||||
.initial_value(#[allow(unused_braces)] 0)
|
||||
.step(#[allow(unused_braces)] 1)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user