Compare commits

..

64 Commits

Author SHA1 Message Date
Greg Johnston
54733e1b34 v0.4.7 2023-08-02 17:03:38 -04:00
Greg Johnston
56f01888b7 Merge pull request #1486 from leptos-rs/export-all-helpers
fix: correctly export all DOM helpers
2023-08-02 17:02:19 -04:00
Greg Johnston
8320f16716 chore: fix new clippy warnings 2023-08-02 16:05:42 -04:00
Greg Johnston
0b16e5992d fix: correctly export all DOM helpers 2023-08-02 14:41:54 -04:00
Danik Vitek
248beb4a55 docs: typo in docs for ServerFnErrorErr (#1477) 2023-08-01 14:27:39 -04:00
martin frances
c9f608d030 docs: fix doclink to Error (#1469) 2023-08-01 13:24:13 -04:00
Greg Johnston
f837d3e6a2 fix: correctly escape HTML in DynChild text nodes (closes #1475) (#1478) 2023-08-01 13:22:24 -04:00
Greg Johnston
8847d5fc42 fix: compile-time regression for deeply-nested component trees (#1476) 2023-07-31 14:23:09 -04:00
Greg Johnston
7819a6fac0 fix: properly replace text nodes in DynChild (closes #1456) (#1472) 2023-07-30 22:37:53 -04:00
Marco Inacio
c199185808 docs: README.md to reflect new version (#1470) 2023-07-30 11:52:09 -04:00
martin frances
e0b5738606 chore: document the magic number in FILTER_SHOW_COMMENT. (#1468) 2023-07-29 16:53:10 -04:00
Sebastian Dobe
f3e3880a57 fix: AnimatedShow - possible panic on cleanup (#1464) 2023-07-29 06:33:49 -04:00
Greg Johnston
d44b90c16d feat: allow mut in component props and suppress "needless lifetime" warning (closes #1458) (#1459) 2023-07-29 06:32:06 -04:00
Joseph Cruz
cc32a3e863 perf(examples): speed up the test-info report (#1446) (#1447) 2023-07-27 20:40:26 -04:00
Greg Johnston
5740c9b76b feat: add MaybeProp type (#1443) 2023-07-27 18:18:25 -04:00
Greg Johnston
80fa6ad3eb docs: fix typo in 23_ssr_modes.md (#1445) 2023-07-26 16:33:21 -04:00
Greg Johnston
7bc1ad2b4f fix: incorrect opening node for <Each/> in debug mode (closes #1168) (#1436) 2023-07-26 10:43:46 -04:00
Joseph Cruz
82a2fe7cbe fix(examples): unable to parse makefile (#1440) (#1441) 2023-07-26 10:43:20 -04:00
Bechma
40bf944957 docs: expand spawn_local documentation (#1433) 2023-07-25 11:42:48 -04:00
Greg Johnston
7ef7546fa9 v0.4.6 2023-07-25 06:08:53 -04:00
Greg Johnston
5e26e84d77 feat: allow feature-name flexibility when using server functions (#1427) 2023-07-25 06:07:52 -04:00
mforsb
e67bc2083a feat: add noscroll attribute to Form, ActionForm (#1432) 2023-07-25 06:07:37 -04:00
g-re-g
a3cb3f7f77 perf: use binary search for event and tag names in view macro (#1430) 2023-07-24 15:06:34 -04:00
Greg Johnston
daeb47e72e build(examples): update Makefiles for recent examples (#1431) 2023-07-24 12:02:30 -04:00
Joseph Cruz
8c5ab99fa7 build(examples): pull up compile tasks (#1417)
* build(examples): pull up compile tasks

* build(examples): set toolchain for compiles tasks

* build(examples): set toolchain for build and check

* build(examples): set toolchain of other examples
2023-07-24 11:35:34 -04:00
Greg Johnston
984a7388f1 fix: clear <title> correctly when navigating between pages (closes #1369) (#1428) 2023-07-24 11:25:28 -04:00
Greg Johnston
274b105676 docs: fix messed up component closing tags router docs 2023-07-24 11:24:58 -04:00
Greg Johnston
a689d1b4c0 docs: note on optional generic component props 2023-07-24 07:52:40 -04:00
Greg Johnston
1581e91317 docs: note on how to opt out of client-side routing 2023-07-24 07:52:29 -04:00
Andrew Grande
20f4034c1c docs: proofreading and fixing the links (#1425)
* Update 23_ssr_modes.md

Fixed grammar, added the section anchor links

* Fixed a broken link

The github link doesn't get properly rendered in the Leptos book site. Make the book work, 'break' the github link.

* Update 26_extractors.md

Fixed broken Axum links. Added an Axum extract function doc link for consistency (had Actix, but not Axum before)
2023-07-24 07:25:02 -04:00
Jason Hansen
9fb1c4b67c docs/warnings: fix warning message about updating a signal after it has been disposed (#1423)
* Remove "either" because it didn't make sense in the sentence
* Change "cause cause" to "not cause"
2023-07-24 07:24:06 -04:00
Ari Seyhun
2e559d6a06 feat: add create_query_signal for URL-synced signals (#1377) 2023-07-23 12:20:15 -04:00
Sebastian Dobe
71de6c395b feat: create a <AnimatedShow> component (closes #1370) (#1386) 2023-07-23 07:46:47 -04:00
Vladimir Motylenko
b09f9e4814 feat: Update rstml to v0.11.0 (#1416) 2023-07-23 07:46:33 -04:00
Greg Johnston
ec4bfb0e8a chore: resolve clippy incorrect_clone_impl_on_copy_type (closes #1401) (#1418) 2023-07-22 22:14:52 -04:00
Greg Johnston
39bf38d1e4 docs: CONTRIBUTING.md with helpful info re: CI (#1415) 2023-07-22 08:26:46 -04:00
Joshua Marsh
e6fd1379b8 docs: fix typo for WrapsChildren (#1402) 2023-07-22 08:26:36 -04:00
Doug A
1d9931a5a8 docs: typo in 01_basic_component.md (#1412)
typo fix
2023-07-22 08:24:30 -04:00
Greg Johnston
06164d34b5 docs: note about typed params on stable (#1414) 2023-07-22 08:23:05 -04:00
Joseph Cruz
f3de288e19 build(examples): make it easy to see which examples do what kind of testing (#1411)
* build(examples): list example projects with CI tests runners

* build(examples): add show all mode
2023-07-22 08:13:49 -04:00
Greg Johnston
62bf315059 fix: <use_/> as typed top-level element in view (#1410) 2023-07-21 10:07:34 -04:00
Greg Johnston
011c97e3a4 fix: closing element names wrong for svg::, math::, and use_ (closes #1403) (#1406) 2023-07-20 17:03:30 -04:00
Greg Johnston
2ca3d2c7a4 fix: RawText/unquoted text nodes in SSR (closes #1384) (#1407) 2023-07-20 17:03:19 -04:00
Greg Johnston
cc52c94348 docs/examples: use shorthand form for <Route/> views when possible (#1375) 2023-07-20 16:28:43 -04:00
Andrew Grande
4b8cc96dfa docs: typo in 16_routes.md
Fixed grammar
2023-07-20 16:13:25 -04:00
Greg Johnston
338d2ab839 Merge pull request #1379 from agilarity/lint-with-clippy
ci: lint with clippy
2023-07-20 14:15:16 -04:00
Greg Johnston
54fc6da24e feat: implement Resource::dispose() (#1393) 2023-07-20 14:14:49 -04:00
Andrew Grande
825b3fb858 chore: typo in README.md (#1399) 2023-07-20 14:00:43 -04:00
Andrew Grande
fd0212a142 docs: typo in 15_global_state.md (#1395)
Proofreading
2023-07-20 08:57:12 -04:00
Greg Johnston
3b397cb39c examples: remove random <form> (#1398) 2023-07-20 08:56:49 -04:00
martin frances
1e002c2c2f chore: Removed call to .into(), plus minor touch to docs. (#1396) 2023-07-20 08:07:31 -04:00
Greg Johnston
8f45daeca8 docs: correct docs for create_memo to reflect laziness (#1388) 2023-07-19 14:50:34 -04:00
Joseph Cruz
105ef989b7 ci: install clippy 2023-07-19 08:54:05 -04:00
Greg Johnston
9e7c31d1e4 docs: small issues (#1385) 2023-07-19 08:53:37 -04:00
Joseph Cruz
771dfa6b68 ci: lint with clippy 2023-07-19 08:44:36 -04:00
Joseph Cruz
fb52cfa73e fix: needless_raw_string_hashes 2023-07-19 08:43:57 -04:00
Ari Seyhun
b2c75d215b chore: remove unnecessary string allocation in TryFrom for Url (#1376) 2023-07-19 07:04:06 -04:00
Andrew Grande
951607de74 docs: typos
* Fixed wording

* Update ARCHITECTURE.md

Fixed superfluous whitespace
2023-07-19 07:03:50 -04:00
Joseph Cruz
122fd2bc74 fix: useless_conversion 2023-07-18 20:56:55 -04:00
Joseph Cruz
f102125d3c fix: needless_borrow 2023-07-18 20:56:55 -04:00
Joseph Cruz
14bda76b30 fix: needless_raw_string_hashes (allow) 2023-07-18 20:56:39 -04:00
Joseph Cruz
3af115a663 fix: maybe_misused_cfg 2023-07-18 20:56:39 -04:00
Joseph Cruz
a344804734 fix: incorrect_clone_impl_on_copy_type (allow) 2023-07-18 20:54:51 -04:00
Joseph Cruz
d8eaa5c004 build: use cargo hack for clippy 2023-07-18 08:02:24 -04:00
115 changed files with 1815 additions and 791 deletions

View File

@@ -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.
Its important to say that the main feature `cargo-leptos` remains its ability
to conveniently tie together different build tooling, compiling your app to

View File

@@ -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).

View File

@@ -26,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.4.5"
version = "0.4.7"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.4.5" }
leptos_dom = { path = "./leptos_dom", version = "0.4.5" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.5" }
leptos_macro = { path = "./leptos_macro", version = "0.4.5" }
leptos_reactive = { path = "./leptos_reactive", version = "0.4.5" }
leptos_server = { path = "./leptos_server", version = "0.4.5" }
server_fn = { path = "./server_fn", version = "0.4.5" }
server_fn_macro = { path = "./server_fn_macro", version = "0.4.5" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.5" }
leptos_config = { path = "./leptos_config", version = "0.4.5" }
leptos_router = { path = "./router", version = "0.4.5" }
leptos_meta = { path = "./meta", version = "0.4.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.5" }
leptos = { path = "./leptos", version = "0.4.7" }
leptos_dom = { path = "./leptos_dom", version = "0.4.7" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.7" }
leptos_macro = { path = "./leptos_macro", version = "0.4.7" }
leptos_reactive = { path = "./leptos_reactive", version = "0.4.7" }
leptos_server = { path = "./leptos_server", version = "0.4.7" }
server_fn = { path = "./server_fn", version = "0.4.7" }
server_fn_macro = { path = "./server_fn_macro", version = "0.4.7" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.7" }
leptos_config = { path = "./leptos_config", version = "0.4.7" }
leptos_router = { path = "./router", version = "0.4.7" }
leptos_meta = { path = "./meta", version = "0.4.7" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.7" }
[profile.release]
codegen-units = 1

View File

@@ -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"

View File

@@ -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/).
### Whats 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?

View File

@@ -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"]

View File

@@ -13,6 +13,3 @@ RUSTFLAGS = "-D warnings"
[tasks.ci]
dependencies = ["lint", "test"]
[tasks.lint]
dependencies = ["check-format-flow"]

View File

@@ -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.
Theres 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.
Theres 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.

View File

@@ -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:

View File

@@ -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 shouldnt 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 shouldnt 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?
Lets 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` youll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` youll 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?

View File

@@ -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 @@ Lets 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 @@ Lets 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 contacts 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)"

View File

@@ -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 youre 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)"

View File

@@ -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 its 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 isnt part of your Leptos app, you can just use `<a rel="external">` to tell the router it isnt 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)"

View File

@@ -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>

View File

@@ -113,7 +113,7 @@ Server functions are a cool technology, but its very important to remember. *
So far, everything Ive 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.

View File

@@ -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 weve 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 doesnt 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/)). Weve built integrations with each servers 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/)). Weve built integrations with each servers router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls.
> If havent seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, nows a good time to check them out.
> If you havent seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, nows 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 youll 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 youll need to add something to handle the error case.
```rust
#[server(AxumExtract, "/api")]

View File

@@ -8,7 +8,12 @@ If youve ever listened to streaming music or watched a video online, Im 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 youre 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 theres 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 theres 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 users device.
- _Cons_
@@ -79,13 +84,13 @@ Because it offers the best blend of performance characteristics, Leptos defaults
```rust
<Routes>
// Well 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>

View File

@@ -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!
Lets make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Lets replace “set this value to 3” with “increment this value by 1”:

View File

@@ -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 theyre 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 _cant_ be specified with an `impl` yet (`progress: impl Fn() -> i32 + 'static,`), in part because theyre 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 cant specify optional generic props for a component. Lets 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. Leptoss view macro doesnt 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 cant 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.

View File

@@ -115,11 +115,11 @@ Calling it like this will create a list:
```rust
view! { cx,
<WrappedChildren>
<WrapsChildren>
"A"
"B"
"C"
</WrappedChildren>
</WrapsChildren>
}
```

View File

@@ -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
'''

View 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"

View File

@@ -0,0 +1 @@
extend = [{ path = "../cargo-make/main.toml" }]

View 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.

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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>
}
}

View 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 />
}
})
}

View 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"

View File

@@ -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]

View File

@@ -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"

View File

@@ -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"

View 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"

View File

@@ -0,0 +1 @@
extend = [{ path = "../cargo-make/main.toml" }]

View 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/)

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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>
}
}

View 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>
}
})
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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]

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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/>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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();

View File

@@ -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
View 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>
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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>

View File

@@ -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;

View File

@@ -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,

View File

@@ -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/>-->

View File

@@ -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)]

View File

@@ -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()));

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -405,11 +405,14 @@ impl View {
self,
dont_escape_text: bool,
) -> Cow<'static, str> {
println!("render_to_string_helper {:?}", self);
match self {
View::Text(node) => {
if dont_escape_text {
println!("don't escape {:?}", node.content);
node.content
} else {
println!("encode_safe {:?}", node.content);
html_escape::encode_safe(&node.content).to_string().into()
}
}
@@ -492,9 +495,17 @@ impl View {
// browser create the dynamic text as it's own text node
if let View::Text(t) = child {
if !cfg!(debug_assertions) {
format!("<!>{}", t.content).into()
format!(
"<!>{}",
html_escape::encode_safe(
&t.content
)
)
.into()
} else {
t.content
html_escape::encode_safe(&t.content)
.to_string()
.into()
}
} else {
child.render_to_string_helper(

View File

@@ -438,12 +438,16 @@ impl View {
StreamChunk::Sync(
format!(
"<!>{}",
content
html_escape::encode_safe(
&content
)
)
.into(),
)
} else {
StreamChunk::Sync(content)
StreamChunk::Sync(html_escape::encode_safe(
&content
).to_string().into())
},
);
} else {

View File

@@ -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"

View File

@@ -21,7 +21,7 @@ proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
rstml = "0.10.6"
rstml = "0.11.0"
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.6.0"
@@ -35,10 +35,9 @@ trybuild = "1"
leptos = { path = "../leptos" }
[features]
default = ["ssr"]
csr = []
hydrate = []
ssr = []
ssr = ["server_fn_macro/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = []

View File

@@ -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"/>
}
}

View File

@@ -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()
}

View File

@@ -169,14 +169,26 @@ mod template;
/// # });
/// ```
///
/// Class names can include dashes, but cannot (at the moment) include a dash-separated segment of only numbers.
/// Class names can include dashes, and since leptos v0.5.0 can include a dash-separated segment of only numbers.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 2);
/// view! { cx, <div class:hidden-div-25={move || count.get() < 3}>"Now you see me, now you dont."</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 dont."</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 dont."</div> }
/// # ;
/// # }
/// # });
@@ -921,8 +933,8 @@ pub fn params_derive(
}
pub(crate) fn attribute_value(attr: &KeyedAttribute) -> &syn::Expr {
match &attr.possible_value {
Some(value) => &value.value,
match attr.value() {
Some(value) => value,
None => abort!(attr.key, "attribute should have value"),
}
}

View File

@@ -20,32 +20,19 @@ enum TagType {
Math,
}
// Keep list alphabetized for binary search
const TYPED_EVENTS: [&str; 126] = [
"afterprint",
"beforeprint",
"beforeunload",
"gamepadconnected",
"gamepaddisconnected",
"hashchange",
"languagechange",
"message",
"messageerror",
"offline",
"online",
"pagehide",
"pageshow",
"popstate",
"rejectionhandled",
"storage",
"unhandledrejection",
"unload",
"DOMContentLoaded",
"abort",
"afterprint",
"animationcancel",
"animationend",
"animationiteration",
"animationstart",
"auxclick",
"beforeinput",
"beforeprint",
"beforeunload",
"blur",
"canplay",
"canplaythrough",
@@ -56,8 +43,12 @@ const TYPED_EVENTS: [&str; 126] = [
"compositionstart",
"compositionupdate",
"contextmenu",
"copy",
"cuechange",
"cut",
"dblclick",
"devicemotion",
"deviceorientation",
"drag",
"dragend",
"dragenter",
@@ -73,17 +64,25 @@ const TYPED_EVENTS: [&str; 126] = [
"focusin",
"focusout",
"formdata",
"fullscreenchange",
"fullscreenerror",
"gamepadconnected",
"gamepaddisconnected",
"gotpointercapture",
"hashchange",
"input",
"invalid",
"keydown",
"keypress",
"keyup",
"languagechange",
"load",
"loadeddata",
"loadedmetadata",
"loadstart",
"lostpointercapture",
"message",
"messageerror",
"mousedown",
"mouseenter",
"mouseleave",
@@ -91,6 +90,12 @@ const TYPED_EVENTS: [&str; 126] = [
"mouseout",
"mouseover",
"mouseup",
"offline",
"online",
"orientationchange",
"pagehide",
"pageshow",
"paste",
"pause",
"play",
"playing",
@@ -98,12 +103,17 @@ const TYPED_EVENTS: [&str; 126] = [
"pointerdown",
"pointerenter",
"pointerleave",
"pointerlockchange",
"pointerlockerror",
"pointermove",
"pointerout",
"pointerover",
"pointerup",
"popstate",
"progress",
"ratechange",
"readystatechange",
"rejectionhandled",
"reset",
"resize",
"scroll",
@@ -115,6 +125,7 @@ const TYPED_EVENTS: [&str; 126] = [
"selectstart",
"slotchange",
"stalled",
"storage",
"submit",
"suspend",
"timeupdate",
@@ -127,6 +138,9 @@ const TYPED_EVENTS: [&str; 126] = [
"transitionend",
"transitionrun",
"transitionstart",
"unhandledrejection",
"unload",
"visibilitychange",
"volumechange",
"waiting",
"webkitanimationend",
@@ -134,21 +148,10 @@ const TYPED_EVENTS: [&str; 126] = [
"webkitanimationstart",
"webkittransitionend",
"wheel",
"DOMContentLoaded",
"devicemotion",
"deviceorientation",
"orientationchange",
"copy",
"cut",
"paste",
"fullscreenchange",
"fullscreenerror",
"pointerlockchange",
"pointerlockerror",
"readystatechange",
"visibilitychange",
];
const CUSTOM_EVENT: &str = "Custom";
pub(crate) fn render_view(
cx: &Ident,
nodes: &[Node],
@@ -348,10 +351,13 @@ fn root_element_to_tokens_ssr(
// We can use open_tag.span(), to provide simmilar(to name span) diagnostic
// in case of expansion error, but it will also higlight "<" token.
let typed_element_name = if is_custom_element {
Ident::new("Custom", Span::call_site())
Ident::new(CUSTOM_EVENT, Span::call_site())
} else {
let camel_cased = camel_case_tag_name(
&tag_name.replace("svg::", "").replace("math::", ""),
tag_name
.trim_start_matches("svg::")
.trim_start_matches("math::")
.trim_end_matches('_'),
);
Ident::new(&camel_cased, Span::call_site())
};
@@ -517,6 +523,17 @@ fn element_to_tokens_ssr(
&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)
@@ -551,7 +568,7 @@ fn element_to_tokens_ssr(
}
template.push_str("</");
template.push_str(&node.name().to_string());
template.push_str(tag_name);
template.push('>');
}
}
@@ -1225,7 +1242,7 @@ fn attribute_to_tokens(
};
let undelegated_ident = match &node.key {
NodeName::Punctuated(parts) => parts.last().and_then(|last| {
if last == "undelegated" {
if last.to_string() == "undelegated" {
Some(last)
} else {
None
@@ -1246,9 +1263,8 @@ fn attribute_to_tokens(
let event_type = if is_custom {
event_type
} else if let Some(ev_name) = event_name_ident {
let span = ev_name.span();
quote_spanned! {
span => #ev_name
quote! {
#ev_name
}
} else {
event_type
@@ -1256,9 +1272,8 @@ fn attribute_to_tokens(
let event_type = if is_force_undelegated {
let undelegated = if let Some(undelegated) = undelegated_ident {
let span = undelegated.span();
quote_spanned! {
span => #undelegated
quote! {
#undelegated
}
} else {
quote! { undelegated }
@@ -1366,12 +1381,10 @@ fn attribute_to_tokens(
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
let (name, is_force_undelegated) = parse_event(name);
let event_type = TYPED_EVENTS
.iter()
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
let is_custom = event_type == "Custom";
let (event_type, is_custom) = TYPED_EVENTS
.binary_search(&name)
.map(|_| (name, false))
.unwrap_or((CUSTOM_EVENT, true));
let Ok(event_type) = event_type.parse::<TokenStream>() else {
abort!(event_type, "couldn't parse event name");
@@ -1699,8 +1712,10 @@ pub(crate) fn component_to_tokens(
)
};
#[cfg(debug_assertions)]
IdeTagHelper::add_component_completion(&mut component, node);
// (Temporarily?) removed
// See note on the function itself below.
/* #[cfg(debug_assertions)]
IdeTagHelper::add_component_completion(cx, &mut component, node); */
if events.is_empty() {
component
@@ -1729,10 +1744,9 @@ pub(crate) fn event_from_attribute_node(
let (name, name_undelegated) = parse_event(&event_name);
let event_type = TYPED_EVENTS
.iter()
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
.binary_search(&name)
.map(|_| (name))
.unwrap_or(CUSTOM_EVENT);
let Ok(event_type) = event_type.parse::<TokenStream>() else {
abort!(attr.key, "couldn't parse event name");
@@ -1819,33 +1833,19 @@ fn is_custom_element(tag: &str) -> bool {
fn is_self_closing(node: &NodeElement) -> bool {
// self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
matches!(
node.name().to_string().as_str(),
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
)
// Keep list alphabetized for binary search
[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link",
"meta", "param", "source", "track", "wbr",
]
.binary_search(&node.name().to_string().as_str())
.is_ok()
}
fn camel_case_tag_name(tag_name: &str) -> String {
let mut chars = tag_name.chars();
let first = chars.next();
let underscore = if tag_name == "option" || tag_name == "use" {
"_"
} else {
""
};
let underscore = if tag_name == "option" { "_" } else { "" };
first
.map(|f| f.to_ascii_uppercase())
.into_iter()
@@ -1855,109 +1855,113 @@ fn camel_case_tag_name(tag_name: &str) -> String {
}
fn is_svg_element(tag: &str) -> bool {
matches!(
tag,
"animate"
| "animateMotion"
| "animateTransform"
| "circle"
| "clipPath"
| "defs"
| "desc"
| "discard"
| "ellipse"
| "feBlend"
| "feColorMatrix"
| "feComponentTransfer"
| "feComposite"
| "feConvolveMatrix"
| "feDiffuseLighting"
| "feDisplacementMap"
| "feDistantLight"
| "feDropShadow"
| "feFlood"
| "feFuncA"
| "feFuncB"
| "feFuncG"
| "feFuncR"
| "feGaussianBlur"
| "feImage"
| "feMerge"
| "feMergeNode"
| "feMorphology"
| "feOffset"
| "fePointLight"
| "feSpecularLighting"
| "feSpotLight"
| "feTile"
| "feTurbulence"
| "filter"
| "foreignObject"
| "g"
| "hatch"
| "hatchpath"
| "image"
| "line"
| "linearGradient"
| "marker"
| "mask"
| "metadata"
| "mpath"
| "path"
| "pattern"
| "polygon"
| "polyline"
| "radialGradient"
| "rect"
| "set"
| "stop"
| "svg"
| "switch"
| "symbol"
| "text"
| "textPath"
| "tspan"
| "use"
| "use_"
| "view"
)
// Keep list alphabetized for binary search
[
"animate",
"animateMotion",
"animateTransform",
"circle",
"clipPath",
"defs",
"desc",
"discard",
"ellipse",
"feBlend",
"feColorMatrix",
"feComponentTransfer",
"feComposite",
"feConvolveMatrix",
"feDiffuseLighting",
"feDisplacementMap",
"feDistantLight",
"feDropShadow",
"feFlood",
"feFuncA",
"feFuncB",
"feFuncG",
"feFuncR",
"feGaussianBlur",
"feImage",
"feMerge",
"feMergeNode",
"feMorphology",
"feOffset",
"fePointLight",
"feSpecularLighting",
"feSpotLight",
"feTile",
"feTurbulence",
"filter",
"foreignObject",
"g",
"hatch",
"hatchpath",
"image",
"line",
"linearGradient",
"marker",
"mask",
"metadata",
"mpath",
"path",
"pattern",
"polygon",
"polyline",
"radialGradient",
"rect",
"set",
"stop",
"svg",
"switch",
"symbol",
"text",
"textPath",
"tspan",
"use",
"use_",
"view",
]
.binary_search(&tag)
.is_ok()
}
fn is_math_ml_element(tag: &str) -> bool {
matches!(
tag,
"math"
| "mi"
| "mn"
| "mo"
| "ms"
| "mspace"
| "mtext"
| "menclose"
| "merror"
| "mfenced"
| "mfrac"
| "mpadded"
| "mphantom"
| "mroot"
| "mrow"
| "msqrt"
| "mstyle"
| "mmultiscripts"
| "mover"
| "mprescripts"
| "msub"
| "msubsup"
| "msup"
| "munder"
| "munderover"
| "mtable"
| "mtd"
| "mtr"
| "maction"
| "annotation"
| "semantics"
)
// Keep list alphabetized for binary search
[
"annotation",
"maction",
"math",
"menclose",
"merror",
"mfenced",
"mfrac",
"mi",
"mmultiscripts",
"mn",
"mo",
"mover",
"mpadded",
"mphantom",
"mprescripts",
"mroot",
"mrow",
"ms",
"mspace",
"msqrt",
"mstyle",
"msub",
"msubsup",
"msup",
"mtable",
"mtd",
"mtext",
"mtr",
"munder",
"munderover",
"semantics",
]
.binary_search(&tag)
.is_ok()
}
fn is_ambiguous_element(tag: &str) -> bool {
@@ -2108,6 +2112,19 @@ impl IdeTagHelper {
}
}
/* This has been (temporarily?) removed.
* Its purpose was simply to add syntax highlighting and IDE hints for
* component closing tags in debug mode by associating the closing tag
* ident with the component function.
*
* Doing this in a way that correctly inferred types, however, required
* duplicating the entire component constructor.
*
* In view trees with many nested components, this led to a massive explosion
* in compile times.
*
* See https://github.com/leptos-rs/leptos/issues/1283
*
/// Add completion to the closing tag of the component.
///
/// In order to ensure that generics are passed through correctly in the
@@ -2124,22 +2141,21 @@ impl IdeTagHelper {
/// ```
#[cfg(debug_assertions)]
pub fn add_component_completion(
cx: &Ident,
component: &mut TokenStream,
node: &NodeElement,
) {
// emit ide helper info
if node.close_tag.is_some() {
let constructor = component.clone();
if let Some(close_tag) = node.close_tag.as_ref().map(|c| &c.name) {
*component = quote! {
if false {
#[allow(unreachable_code)]
#constructor
} else {
#component
{
let #close_tag = |cx| #component;
#close_tag(#cx)
}
}
}
}
*/
/// Returns `syn::Path`-like `TokenStream` to the fn in docs.
/// If tag name is `Component` returns `None`.

View File

@@ -57,15 +57,15 @@ use std::{any::Any, cell::RefCell, fmt, marker::PhantomData, rc::Rc};
/// });
///
/// // instead, we create a memo
/// // 🆗 run #1: the calculation runs once immediately
/// // ✅ creation: the computation does not run on creation, because memos are lazy
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value.get()));
/// create_effect(cx, move |_| {
/// // 🆗 reads the current value of the memo
/// // 🆗 run #1: reading the memo for the first time causes the computation to run for the first time
/// // can be `memoized()` on nightly
/// log::debug!("memoized = {}", memoized.get());
/// });
/// create_effect(cx, move |_| {
/// // ✅ reads the current value **without re-running the calculation**
/// // ✅ reads the current value again **without re-running the calculation**
/// let value = memoized.get();
/// // do something else...
/// });
@@ -149,15 +149,15 @@ where
/// });
///
/// // instead, we create a memo
/// // 🆗 run #1: the calculation runs once immediately
// // ✅ creation: the computation does not run on creation, because memos are lazy
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value.get()));
/// create_effect(cx, move |_| {
/// // 🆗 reads the current value of the memo
/// // 🆗 run #1: reading the memo for the first time causes the computation to run for the first time
/// // can be `memoized()` on nightly
/// log::debug!("memoized = {}", memoized.get());
/// });
/// create_effect(cx, move |_| {
/// // ✅ reads the current value **without re-running the calculation**
/// // can be `memoized()` on nightly
/// // ✅ reads the current value again **without re-running the calculation**
/// let value = memoized.get();
/// // do something else...
/// });
@@ -179,13 +179,7 @@ where
T: 'static,
{
fn clone(&self) -> Self {
Self {
runtime: self.runtime,
id: self.id,
ty: PhantomData,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: self.defined_at,
}
*self
}
}

View File

@@ -6,8 +6,8 @@ use crate::{
serialization::Serializable,
spawn::spawn_local,
use_context, GlobalSuspenseContext, Memo, ReadSignal, Scope, ScopeProperty,
SignalGetUntracked, SignalSet, SignalUpdate, SignalWith, SuspenseContext,
WriteSignal,
SignalDispose, SignalGetUntracked, SignalSet, SignalUpdate, SignalWith,
SuspenseContext, WriteSignal,
};
use std::{
any::Any,
@@ -237,7 +237,7 @@ where
id,
source_ty: PhantomData,
out_ty: PhantomData,
#[cfg(any(debug_assertions, features = "ssr"))]
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}
}
@@ -373,7 +373,7 @@ where
id,
source_ty: PhantomData,
out_ty: PhantomData,
#[cfg(any(debug_assertions, features = "ssr"))]
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}
}
@@ -719,7 +719,7 @@ where
pub(crate) id: ResourceId,
pub(crate) source_ty: PhantomData<S>,
pub(crate) out_ty: PhantomData<T>,
#[cfg(any(debug_assertions, features = "ssr"))]
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
@@ -734,19 +734,8 @@ where
S: 'static,
T: 'static,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
fn clone(&self) -> Self {
Self {
runtime: self.runtime,
id: self.id,
source_ty: PhantomData,
out_ty: PhantomData,
#[cfg(any(debug_assertions, features = "ssr"))]
defined_at: self.defined_at,
}
*self
}
}
@@ -1093,3 +1082,25 @@ thread_local! {
pub fn suppress_resource_load(suppress: bool) {
SUPPRESS_RESOURCE_LOAD.with(|w| w.set(suppress));
}
impl<S, T> SignalDispose for Resource<S, T>
where
S: 'static,
T: 'static,
{
#[track_caller]
fn dispose(self) {
let res = with_runtime(self.runtime, |runtime| {
let mut resources = runtime.resources.borrow_mut();
resources.remove(self.id)
});
if res.ok().flatten().is_none() {
crate::macros::debug_warn!(
"At {}, you are calling Resource::dispose() on a resource \
that no longer exists, probably because its Scope has \
already been disposed.",
std::panic::Location::caller()
);
}
}
}

View File

@@ -476,7 +476,7 @@ impl RuntimeId {
}
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[inline(always)]

View File

@@ -37,7 +37,7 @@ pub fn create_scope(
///
/// You usually don't need to call this manually.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn raw_scope_and_disposer(runtime: RuntimeId) -> (Scope, ScopeDisposer) {
@@ -52,7 +52,7 @@ pub fn raw_scope_and_disposer(runtime: RuntimeId) -> (Scope, ScopeDisposer) {
///
/// You usually don't need to call this manually.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn run_scope<T>(
@@ -69,7 +69,7 @@ pub fn run_scope<T>(
///
/// You usually don't need to call this manually.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn run_scope_undisposed<T>(
@@ -128,7 +128,7 @@ impl Scope {
/// dispose of them when they are no longer needed (e.g., a list item has been destroyed or the user
/// has navigated away from the route.)
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[inline(always)]
@@ -147,7 +147,7 @@ impl Scope {
/// dispose of them when they are no longer needed (e.g., a list item has been destroyed or the user
/// has navigated away from the route.)
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[inline(always)]
@@ -203,7 +203,7 @@ impl Scope {
/// # });
/// ```
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[inline(always)]
@@ -229,7 +229,7 @@ impl Scope {
/// 2. run all cleanup functions defined for this scope by [`on_cleanup`](crate::on_cleanup).
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn dispose(self) {
@@ -306,7 +306,7 @@ impl Scope {
})
}
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub(crate) fn push_scope_property(&self, prop: ScopeProperty) {
@@ -322,7 +322,7 @@ impl Scope {
})
}
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub(crate) fn remove_scope_property(&self, prop: ScopeProperty) {
@@ -343,7 +343,7 @@ impl Scope {
})
}
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
/// Returns the the parent Scope, if any.
@@ -361,7 +361,7 @@ impl Scope {
}
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
fn push_cleanup(cx: Scope, cleanup_fn: Box<dyn FnOnce()>) {
@@ -370,7 +370,7 @@ fn push_cleanup(cx: Scope, cleanup_fn: Box<dyn FnOnce()>) {
let cleanups = cleanups
.entry(cx.id)
.expect("trying to clean up a Scope that has already been disposed")
.or_insert_with(Default::default);
.or_default();
cleanups.push(cleanup_fn);
});
}
@@ -424,7 +424,7 @@ impl ScopeDisposer {
impl Scope {
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn all_resources(&self) -> Vec<ResourceId> {
@@ -435,7 +435,7 @@ impl Scope {
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope that are
/// pending from the server.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn pending_resources(&self) -> Vec<ResourceId> {
@@ -445,7 +445,7 @@ impl Scope {
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn serialization_resolvers(
@@ -460,7 +460,7 @@ impl Scope {
/// Registers the given [`SuspenseContext`](crate::SuspenseContext) with the current scope,
/// calling the `resolver` when its resources are all resolved.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn register_suspense(
@@ -517,7 +517,7 @@ impl Scope {
/// The keys are hydration IDs. Values are tuples of two pinned
/// `Future`s that return content for out-of-order and in-order streaming, respectively.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn pending_fragments(&self) -> HashMap<String, FragmentData> {
@@ -530,7 +530,7 @@ impl Scope {
/// A future that will resolve when all blocking fragments are ready.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn blocking_fragments_ready(self) -> PinnedFuture<()> {
@@ -557,7 +557,7 @@ impl Scope {
/// Returns a tuple of two pinned `Future`s that return content for out-of-order
/// and in-order streaming, respectively.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn take_pending_fragment(&self, id: &str) -> Option<FragmentData> {
@@ -576,7 +576,7 @@ impl Scope {
/// # Panics
/// Panics if the runtime this scope belongs to has already been disposed.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
#[inline(always)]

View File

@@ -95,7 +95,7 @@ cfg_if! {
fn de(json: &str) -> Result<Self, SerializationError> {
let intermediate =
serde_json::from_str(&json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))?;
serde_json::from_str(json).map_err(|e| SerializationError::Deserialize(Rc::new(e)))?;
Self::deserialize(&intermediate).map_err(|e| SerializationError::Deserialize(Rc::new(e)))
}
}

View File

@@ -2059,11 +2059,11 @@ impl NodeId {
debug_warn!(
"[Signal::update] Youre trying to update a \
Signal<{}> (defined at {defined_at}) that has \
already been disposed of. This is probably \
either a logic error in a component that creates \
and disposes of scopes. If it does cause cause \
any issues, it is safe to ignore this warning, \
which occurs only in debug mode.",
already been disposed of. This is probably a \
logic error in a component that creates and \
disposes of scopes. If it does not cause any \
issues, it is safe to ignore this warning, which \
occurs only in debug mode.",
std::any::type_name::<T>()
);
}
@@ -2106,11 +2106,11 @@ impl NodeId {
debug_warn!(
"[Signal::update] Youre trying to update a \
Signal<{}> (defined at {defined_at}) that has \
already been disposed of. This is probably \
either a logic error in a component that creates \
and disposes of scopes. If it does cause cause \
any issues, it is safe to ignore this warning, \
which occurs only in debug mode.",
already been disposed of. This is probably a \
logic error in a component that creates and \
disposes of scopes. If it does not cause any \
issues, it is safe to ignore this warning, which \
occurs only in debug mode.",
std::any::type_name::<T>()
);
}

View File

@@ -71,11 +71,7 @@ where
impl<T> Clone for Signal<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: self.defined_at,
}
*self
}
}
@@ -436,13 +432,7 @@ where
impl<T> Clone for SignalTypes<T> {
fn clone(&self) -> Self {
match self {
Self::ReadSignal(arg0) => Self::ReadSignal(*arg0),
Self::Memo(arg0) => Self::Memo(*arg0),
Self::DerivedSignal(arg0, arg1) => {
Self::DerivedSignal(*arg0, *arg1)
}
}
*self
}
}
@@ -566,6 +556,15 @@ impl<T: Default> Default for MaybeSignal<T> {
/// # });
/// ```
impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeSignal::get()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn get(&self) -> T {
match self {
Self::Static(t) => t.clone(),
@@ -573,6 +572,15 @@ impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeSignal::try_get()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn try_get(&self) -> Option<T> {
match self {
Self::Static(t) => Some(t.clone()),
@@ -650,6 +658,15 @@ impl<T> SignalWith<T> for MaybeSignal<T> {
}
impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeSignal::with_untracked()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
match self {
Self::Static(t) => f(t),
@@ -657,6 +674,15 @@ impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeSignal::try_with_untracked()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
match self {
Self::Static(t) => Some(f(t)),
@@ -666,6 +692,15 @@ impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
}
impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeSignal::get_untracked()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn get_untracked(&self) -> T {
match self {
Self::Static(t) => t.clone(),
@@ -673,6 +708,15 @@ impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeSignal::try_get_untracked()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn try_get_untracked(&self) -> Option<T> {
match self {
Self::Static(t) => Some(t.clone()),
@@ -682,6 +726,15 @@ impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
}
impl<T: Clone> SignalStream<T> for MaybeSignal<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeSignal::to_stream()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn to_stream(
&self,
cx: Scope,
@@ -774,4 +827,400 @@ impl From<&str> for MaybeSignal<String> {
}
}
/// A wrapping type for an optional component prop, which can either be a signal or a
/// non-reactive value, and which may or may not have a value. In other words, this is
/// an `Option<MaybeSignal<Option<T>>>` that automatically flattens its getters.
///
/// This creates an extremely flexible type for component libraries, etc.
///
/// ## Core Trait Implementations
/// - [`.get()`](#impl-SignalGet<T>-for-MaybeProp<T>) (or calling the signal as a function) clones the current
/// value of the signal. If you call it within an effect, it will cause that effect
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-MaybeProp<T>) clones the value of the signal
/// without reactively tracking it.
/// - [`.with()`](#impl-SignalWith<T>-for-MaybeProp<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeProp<T>) allows you to access the signals
/// value without reactively tracking it.
/// - [`.to_stream()`](#impl-SignalStream<T>-for-MaybeProp<T>) converts the signal to an `async` stream of values.
///
/// ## Examples
/// ```rust
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, Some(2));
/// let double = |n| n * 2;
/// let double_count = MaybeProp::derive(cx, move || count.get().map(double));
/// let memoized_double_count =
/// create_memo(cx, move |_| count.get().map(double));
/// let static_value = 5;
///
/// // this function takes either a reactive or non-reactive value
/// fn above_3(arg: &MaybeProp<i32>) -> bool {
/// // ✅ calling the signal clones and returns the value
/// // it is a shorthand for arg.get()q
/// arg.get().map(|arg| arg > 3).unwrap_or(false)
/// }
///
/// assert_eq!(above_3(&None::<i32>.into()), false);
/// assert_eq!(above_3(&static_value.into()), true);
/// assert_eq!(above_3(&count.into()), false);
/// assert_eq!(above_3(&double_count), true);
/// assert_eq!(above_3(&memoized_double_count.into()), true);
/// # });
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MaybeProp<T: 'static>(Option<MaybeSignal<Option<T>>>);
impl<T: Copy> Copy for MaybeProp<T> {}
impl<T> Default for MaybeProp<T> {
fn default() -> Self {
Self(None)
}
}
/// # Examples
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, Some(2));
/// let double = |n| n * 2;
/// let double_count = MaybeProp::derive(cx, move || count.get().map(double));
/// let memoized_double_count =
/// create_memo(cx, move |_| count.get().map(double));
/// let static_value = 5;
///
/// // this function takes either a reactive or non-reactive value
/// fn above_3(arg: &MaybeProp<i32>) -> bool {
/// // ✅ calling the signal clones and returns the value
/// // it is a shorthand for arg.get()q
/// arg.get().map(|arg| arg > 3).unwrap_or(false)
/// }
///
/// assert_eq!(above_3(&None::<i32>.into()), false);
/// assert_eq!(above_3(&static_value.into()), true);
/// assert_eq!(above_3(&count.into()), false);
/// assert_eq!(above_3(&double_count), true);
/// assert_eq!(above_3(&memoized_double_count.into()), true);
/// # });
/// ```
impl<T: Clone> SignalGet<Option<T>> for MaybeProp<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::get()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn get(&self) -> Option<T> {
self.0.as_ref().and_then(|s| s.get())
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::try_get()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn try_get(&self) -> Option<Option<T>> {
self.0.as_ref().and_then(|s| s.try_get())
}
}
/// # Examples
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, Some("Alice".to_string()));
/// let name_upper = MaybeProp::derive(cx, move || {
/// name.with(|n| n.as_ref().map(|n| n.to_uppercase()))
/// });
/// let memoized_lower = create_memo(cx, move |_| {
/// name.with(|n| n.as_ref().map(|n| n.to_lowercase()))
/// });
/// let static_value: MaybeProp<String> = "Bob".to_string().into();
///
/// // this function takes any kind of wrapped signal
/// fn current_len_inefficient(arg: &MaybeProp<String>) -> usize {
/// // ❌ unnecessarily clones the string
/// arg.get().map(|n| n.len()).unwrap_or(0)
/// }
///
/// fn current_len(arg: &MaybeProp<String>) -> usize {
/// // ✅ gets the length without cloning the `String`
/// arg.with(|value| value.len()).unwrap_or(0)
/// }
///
/// assert_eq!(current_len(&None::<String>.into()), 0);
/// assert_eq!(current_len(&name_upper), 5);
/// assert_eq!(current_len(&memoized_lower.into()), 5);
/// assert_eq!(current_len(&static_value), 3);
///
/// assert_eq!(name.get(), Some("Alice".to_string()));
/// assert_eq!(name_upper.get(), Some("ALICE".to_string()));
/// assert_eq!(memoized_lower.get(), Some("alice".to_string()));
/// assert_eq!(static_value.get(), Some("Bob".to_string()));
/// # });
/// ```
impl<T> MaybeProp<T> {
/// Applies a function to the current value, returning the result.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::with()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.0
.as_ref()
.and_then(|inner| inner.with(|value| value.as_ref().map(f)))
}
/// Applies a function to the current value, returning the result. Returns `None`
/// if the value has already been disposed.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::try_with()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
pub fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.0
.as_ref()
.and_then(|inner| inner.try_with(|value| value.as_ref().map(f)))
.flatten()
}
/// Applies a function to the current value, returning the result, without
/// causing the current reactive scope to track changes.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::with_untracked()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
pub fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.0.as_ref().and_then(|inner| {
inner.with_untracked(|value| value.as_ref().map(f))
})
}
/// Applies a function to the current value, returning the result, without
/// causing the current reactive scope to track changes. Returns `None` if
/// the value has already been disposed.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::try_with_untracked()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
pub fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.0
.as_ref()
.and_then(|inner| {
inner.try_with_untracked(|value| value.as_ref().map(f))
})
.flatten()
}
}
impl<T: Clone> SignalGetUntracked<Option<T>> for MaybeProp<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::get_untracked()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn get_untracked(&self) -> Option<T> {
self.0.as_ref().and_then(|inner| inner.get_untracked())
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::try_get_untracked()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn try_get_untracked(&self) -> Option<Option<T>> {
self.0.as_ref().and_then(|inner| inner.try_get_untracked())
}
}
impl<T: Clone> SignalStream<Option<T>> for MaybeProp<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::to_stream()",
skip_all,
fields(ty = %std::any::type_name::<T>())
)
)]
fn to_stream(
&self,
cx: Scope,
) -> std::pin::Pin<Box<dyn futures::Stream<Item = Option<T>>>> {
match &self.0 {
None => Box::pin(futures::stream::once(async move { None })),
Some(MaybeSignal::Static(t)) => {
let t = t.clone();
let stream = futures::stream::once(async move { t });
Box::pin(stream)
}
Some(MaybeSignal::Dynamic(s)) => s.to_stream(cx),
}
}
}
impl<T> MaybeProp<T>
where
T: 'static,
{
/// Wraps a derived signal, i.e., any computation that accesses one or more
/// reactive signals.
/// ```rust
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, Some(2));
/// let double_count = Signal::derive(cx, move || count.get().map(|n| n * 2));
///
/// // this function takes any kind of wrapped signal
/// fn above_3(arg: &MaybeProp<i32>) -> bool {
/// arg.get().unwrap_or(0) > 3
/// }
///
/// assert_eq!(above_3(&count.into()), false);
/// assert_eq!(above_3(&double_count.into()), true);
/// assert_eq!(above_3(&2.into()), false);
/// # });
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "MaybeProp::derive()",
skip_all,
fields(
cx = ?cx.id,
ty = %std::any::type_name::<T>()
)
)
)]
pub fn derive(
cx: Scope,
derived_signal: impl Fn() -> Option<T> + 'static,
) -> Self {
Self(Some(MaybeSignal::derive(cx, derived_signal)))
}
}
impl<T> From<T> for MaybeProp<T> {
fn from(value: T) -> Self {
Self(Some(MaybeSignal::from(Some(value))))
}
}
impl<T> From<Option<T>> for MaybeProp<T> {
fn from(value: Option<T>) -> Self {
Self(Some(MaybeSignal::from(value)))
}
}
impl<T> From<MaybeSignal<Option<T>>> for MaybeProp<T> {
fn from(value: MaybeSignal<Option<T>>) -> Self {
Self(Some(value))
}
}
impl<T> From<Option<MaybeSignal<Option<T>>>> for MaybeProp<T> {
fn from(value: Option<MaybeSignal<Option<T>>>) -> Self {
Self(value)
}
}
impl<T> From<ReadSignal<Option<T>>> for MaybeProp<T> {
fn from(value: ReadSignal<Option<T>>) -> Self {
Self(Some(value.into()))
}
}
impl<T> From<RwSignal<Option<T>>> for MaybeProp<T> {
fn from(value: RwSignal<Option<T>>) -> Self {
Self(Some(value.into()))
}
}
impl<T> From<Memo<Option<T>>> for MaybeProp<T> {
fn from(value: Memo<Option<T>>) -> Self {
Self(Some(value.into()))
}
}
impl<T> From<Signal<Option<T>>> for MaybeProp<T> {
fn from(value: Signal<Option<T>>) -> Self {
Self(Some(value.into()))
}
}
impl From<&str> for MaybeProp<String> {
fn from(value: &str) -> Self {
Self(Some(MaybeSignal::from(Some(value.to_string()))))
}
}
impl_get_fn_traits![Signal, MaybeSignal];
#[cfg(feature = "nightly")]
impl<T: Clone> FnOnce<()> for MaybeProp<T> {
type Output = Option<T>;
#[inline(always)]
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(feature = "nightly")]
impl<T: Clone> FnMut<()> for MaybeProp<T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(feature = "nightly")]
impl<T: Clone> Fn<()> for MaybeProp<T> {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}

View File

@@ -63,11 +63,7 @@ where
impl<T> Clone for SignalSetter<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: self.defined_at,
}
*self
}
}
@@ -231,11 +227,7 @@ where
impl<T> Clone for SignalSetterTypes<T> {
fn clone(&self) -> Self {
match self {
Self::Write(arg0) => Self::Write(*arg0),
Self::Mapped(cx, f) => Self::Mapped(*cx, *f),
Self::Default => Self::Default,
}
*self
}
}

View File

@@ -4,7 +4,73 @@ use std::future::Future;
/// Spawns and runs a thread-local [`Future`] in a platform-independent way.
///
/// This can be used to interface with any `async` code.
/// This can be used to interface with any `async` code by spawning a task
/// to run a `Future`.
///
/// ## Limitations
///
/// You should not use `spawn_local` to synchronize `async` code with a
/// signals value during server rendering. The server response will not
/// be notified to wait for the spawned task to complete, creating a race
/// condition between the response and your task. Instead, use
/// [`create_resource`](crate::create_resource) and `<Suspense/>` to coordinate
/// asynchronous work with the rendering process.
///
/// ```
/// # use leptos::*;
/// # #[cfg(not(any(feature = "csr", feature = "serde-lite", feature = "miniserde", feature = "rkyv")))]
/// # {
///
/// async fn get_user(user: String) -> Result<String, ServerFnError> {
/// Ok(format!("this user is {user}"))
/// }
///
/// // ❌ Write into a signal from `spawn_local` on the serevr
/// #[component]
/// fn UserBad(cx: Scope) -> impl IntoView {
/// let signal = create_rw_signal(cx, String::new());
///
/// // ❌ If the rest of the response is already complete,
/// // `signal` will no longer exist when `get_user` resolves
/// #[cfg(feature = "ssr")]
/// spawn_local(async move {
/// let user_res = get_user("user".into()).await.unwrap_or_default();
/// signal.set(user_res);
/// });
///
/// view!{cx,
/// <p>
/// "This will be empty (hopefully the client will render it) -> "
/// {move || signal.get()}
/// </p>
/// }
/// }
///
/// // ✅ Use a resource and suspense
/// #[component]
/// fn UserGood(cx: Scope) -> impl IntoView {
/// // new resource with no dependencies (it will only called once)
/// let user = create_resource(cx, || (), |_| async { get_user("john".into()).await });
/// view!{cx,
/// // handles the loading
/// <Suspense fallback=move || view! {cx, <p>"Loading User"</p> }>
/// // handles the error from the resource
/// <ErrorBoundary fallback=|cx, _| {view! {cx, <p>"Something went wrong"</p>}}>
/// {move || {
/// user.read(cx).map(move |x| {
/// // the resource has a result
/// x.map(move |y| {
/// // successful call from the server fn
/// view!{cx, <p>"User result filled in server and client: "{y}</p>}
/// })
/// })
/// }}
/// </ErrorBoundary>
/// </Suspense>
/// }
/// }
/// # }
/// ```
pub fn spawn_local<F>(fut: F)
where
F: Future<Output = ()> + 'static,

View File

@@ -33,11 +33,7 @@ where
impl<T> Clone for StoredValue<T> {
fn clone(&self) -> Self {
Self {
runtime: self.runtime,
id: self.id,
ty: self.ty,
}
*self
}
}

View File

@@ -1,9 +1,9 @@
use crate::{Scope, ScopeProperty};
/// A version of [`create_effect`] that listens to any dependency that is accessed inside `deps` and returns
/// A version of [`create_effect`](crate::create_effect) that listens to any dependency that is accessed inside `deps` and returns
/// a stop handler.
/// The return value of `deps` is passed into `callback` as an argument together with the previous value.
/// Additionally the last return value of `callback` is provided as a third argument as is done in [`create_effect`].
/// Additionally the last return value of `callback` is provided as a third argument as is done in [`create_effect`](crate::create_effect).
///
/// ## Usage
///

Some files were not shown because too many files have changed in this diff Show More