mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 07:52:34 -05:00
Compare commits
62 Commits
v0.2.4
...
server-fn-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89a4ab5fe7 | ||
|
|
cb904c6f63 | ||
|
|
41a5e09caa | ||
|
|
9478245986 | ||
|
|
4c1c12734a | ||
|
|
5d3a360456 | ||
|
|
4e7a0db950 | ||
|
|
cee6ed9a9f | ||
|
|
fa1013f7c3 | ||
|
|
8b57ba7aa8 | ||
|
|
ea638e37f6 | ||
|
|
4342d45a2f | ||
|
|
fe4d2382b8 | ||
|
|
2a13609eff | ||
|
|
c2ff1cabf1 | ||
|
|
e72ed26809 | ||
|
|
64e056ffa9 | ||
|
|
db9b7db53d | ||
|
|
a9e6590b5e | ||
|
|
b67121b755 | ||
|
|
7bce4de682 | ||
|
|
8bdb427133 | ||
|
|
4c23f3c478 | ||
|
|
9502de561b | ||
|
|
210c11a733 | ||
|
|
6917027204 | ||
|
|
e78ce7e6b9 | ||
|
|
a3327f8841 | ||
|
|
f727dd773b | ||
|
|
952646f066 | ||
|
|
1e037ecb60 | ||
|
|
c9f75d82d6 | ||
|
|
de3849c20c | ||
|
|
c391c2e938 | ||
|
|
1cde4b1f8a | ||
|
|
42360d109b | ||
|
|
7aa4d9e6db | ||
|
|
9ed3390b81 | ||
|
|
1ff56f7bfd | ||
|
|
16917997cd | ||
|
|
f42568d262 | ||
|
|
97bbdf561a | ||
|
|
f4043cbd9f | ||
|
|
e9ff26abb4 | ||
|
|
e6b1298915 | ||
|
|
98a9ec8335 | ||
|
|
5329561687 | ||
|
|
89ca047f2f | ||
|
|
a94711fcf0 | ||
|
|
97d88c65ae | ||
|
|
e482e3748d | ||
|
|
8ab9c08448 | ||
|
|
56de70b714 | ||
|
|
38d97babd8 | ||
|
|
4cfecb5d82 | ||
|
|
08b5970b2b | ||
|
|
af20f80b2b | ||
|
|
c2fdd2cd70 | ||
|
|
286f3eebe4 | ||
|
|
509223ab2e | ||
|
|
665b0b8ed2 | ||
|
|
508ad52582 |
37
.github/workflows/publish-book.yml
vendored
Normal file
37
.github/workflows/publish-book.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Deploy book
|
||||
on:
|
||||
push:
|
||||
paths: ['docs/book/**']
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pull-requests: write # To create a PR from that branch
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install mdbook
|
||||
run: |
|
||||
mkdir mdbook
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.27/mdbook-v0.4.27-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Deploy GitHub Pages
|
||||
run: |
|
||||
cd docs/book
|
||||
mdbook build
|
||||
git worktree add gh-pages
|
||||
git config user.name "Deploy book from CI"
|
||||
git config user.email ""
|
||||
cd gh-pages
|
||||
# Delete the ref to avoid keeping history.
|
||||
git update-ref -d refs/heads/gh-pages
|
||||
rm -rf *
|
||||
mv ../book/* .
|
||||
git add .
|
||||
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
|
||||
git push --force --set-upstream origin gh-pages
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.3"
|
||||
version = "0.2.5"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.3" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.3" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.3" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.3" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.3" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.3" }
|
||||
leptos_router = { path = "./router", version = "0.2.3" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.3" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.5" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.5" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.5" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.5" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.5" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.5" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.5" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.5" }
|
||||
leptos_router = { path = "./router", version = "0.2.5" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.2.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.5" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -59,10 +59,10 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
|
||||
Here are some resources for learning more about Leptos:
|
||||
|
||||
- [Book](https://leptos-rs.github.io/leptos/) (work in progress)
|
||||
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
|
||||
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
- Leptos Guide (in progress)
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
|
||||
@@ -61,3 +61,19 @@ view! {
|
||||
<input prop:value=a on:input=on_input />
|
||||
}
|
||||
```
|
||||
|
||||
## Build configuration
|
||||
|
||||
### Cargo feature resolution in workspaces
|
||||
|
||||
A new [version](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) of Cargo's feature resolver was introduced for the 2021 edition of Rust.
|
||||
For single crate projects it will select a resolver version based on the Rust edition in `Cargo.toml`. As there is no Rust edition present for `Cargo.toml` in a workspace, Cargo will default to the pre 2021 edition resolver.
|
||||
This can cause issues resulting in non WASM compatible code being built for a WASM target. Seeing `mio` failing to build is often a sign that none WASM compatible code is being included in the build.
|
||||
|
||||
The resolver version can be set in the workspace `Cargo.toml` to remedy this issue.
|
||||
|
||||
```toml
|
||||
[workspace]
|
||||
members = ["member1", "member2"]
|
||||
resolver = "2"
|
||||
```
|
||||
|
||||
@@ -107,4 +107,6 @@ create_effect(cx, move |prev_value| {
|
||||
|
||||
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -84,7 +84,7 @@ fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
This kind of “provide a signal in a parent, consume it in a child” should be familiar
|
||||
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
|
||||
pattern you use to communicate between parents and children works for grandparents and
|
||||
grandchildren, or any ancestors and descendents: in other words, between “global” state
|
||||
grandchildren, or any ancestors and descendants: in other words, between “global” state
|
||||
in the root component of your app and any other components anywhere else in the app.
|
||||
|
||||
Because of the fine-grained nature of updates, this is usually all you need. However,
|
||||
@@ -122,6 +122,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
provide_context(cx, state);
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Then child components can access “slices” of that state with fine-grained
|
||||
@@ -168,4 +169,6 @@ somewhere else that only takes `state.name`, clicking the button won’t cause
|
||||
that other slice to update. This allows you to combine the benefits of a top-down
|
||||
data flow and of fine-grained reactive updates.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px">
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh">
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
- [Nested Routing](./router/17_nested_routing.md)
|
||||
- [Params and Queries](./router/18_params_and_queries.md)
|
||||
- [`<A/>`](./router/19_a.md)
|
||||
- [`<Form/>`]()
|
||||
- [`<Form/>`](./router/20_form.md)
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Metadata]()
|
||||
- [SSR]()
|
||||
|
||||
@@ -50,4 +50,6 @@ view! { cx,
|
||||
|
||||
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -69,4 +69,6 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
|
||||
|
||||
This inversion of the flow of control makes it easier to add or remove individual resources, as you don’t need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which we’ll talk about during a later chapter.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -4,6 +4,8 @@ You’ll notice in the `<Suspense/>` example that if you keep reloading the data
|
||||
|
||||
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
|
||||
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data laods. This can be a much better user experience than constantly falling back to a loading message.
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -91,4 +91,6 @@ view! { cx,
|
||||
|
||||
Now, there’s a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, you’ll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Don’t worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -167,4 +167,6 @@ In fact, in this case, we don’t even need to rerender the `<Contact/>` compone
|
||||
|
||||
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple we’ll cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so don’t be surprised if there’s anything you don’t understand.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -58,8 +58,8 @@ let id = move || {
|
||||
The untyped versions return `Memo<ParamsMap>`. Again, it’s memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
|
||||
|
||||
```rust
|
||||
let params = use_params::<ContactParams>(cx);
|
||||
let query = use_query::<ContactSearch>(cx);
|
||||
let params = use_params_map(cx);
|
||||
let query = use_query_map(cx);
|
||||
|
||||
// id: || -> Option<String>
|
||||
let id = move || {
|
||||
@@ -74,4 +74,6 @@ This can get a little messy: deriving a signal that wraps an `Option<_>` or `Res
|
||||
|
||||
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explain them all yet.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -18,4 +18,6 @@ The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_
|
||||
|
||||
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
69
docs/book/src/router/20_form.md
Normal file
69
docs/book/src/router/20_form.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# The `<Form/>` Component
|
||||
|
||||
Links and forms sometimes seem completely unrelated. But in fact, they work in very similar ways.
|
||||
|
||||
In plain HTML, there are three ways to navigate to another page:
|
||||
|
||||
1. An `<a>` element that links to another page. Navigates to the URL in its `href` attribute with the `GET` HTTP method.
|
||||
2. A `<form method="GET">`. Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
|
||||
3. A `<form method="POST">`. Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
|
||||
|
||||
Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way.
|
||||
|
||||
The router provides a [`<Form>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Form.html) component, which works like the HTML `<form>` element, but uses client-side navigations instead of full page reloads. `<Form/>` works with both `GET` and `POST` requests. With `method="GET"`, it will navigate to the URL encoded in the form data. With `method="POST"` it will make a `POST` request and handle the server’s response.
|
||||
|
||||
`<Form/>` provides the basis for some components like `<ActionForm/>` and `<MultiActionForm/>` that we’ll see in later chapters. But it also enables some powerful patterns of its own.
|
||||
|
||||
For example, imagine that you want to create a search field that updates search results in real time as the user searches, without a page reload, but that also stores the search in the URL so a user can copy and paste it to share results with someone else.
|
||||
|
||||
It turns out that the patterns we’ve learned so far make this easy to implement.
|
||||
|
||||
```rust
|
||||
async fn fetch_results() {
|
||||
// some async function to fetch our search results
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Search(cx: Scope) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn FormExample(cx: Scope) -> impl IntoView {
|
||||
// reactive access to URL query strings
|
||||
let query = use_query_map(cx);
|
||||
// search stored as ?q=
|
||||
let search = move || query().get("q").cloned().unwrap_or_default();
|
||||
// a resource driven by the search string
|
||||
let search_results = create_resource(cx, search, fetch_results);
|
||||
|
||||
view! { cx,
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search/>
|
||||
<input type="submit"/>
|
||||
</Form>
|
||||
<Transition fallback=move || ()>
|
||||
/* render search results */
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Whenever you click `Submit`, the `<Form/>` will “navigate” to `?q={search}`. But because this navigation is done on the client side, there’s no page flicker or reload. The URL query string changes, which triggers `search` to update. Because `search` is the source signal for the `search_results` resource, this triggers `search_results` to reload its resource. The `<Transition/>` continues displaying the current search results until the new ones have loaded. When they are complete, it switches to displaying the new result.
|
||||
|
||||
This is a great pattern. The data flow is extremely clear: all data flows from the URL to the resource into the UI. The current state of the application is stored in the URL, which means you can refresh the page or text the link to a friend and it will show exactly what you’re expecting. And once we introduce server rendering, this pattern will prove to be really fault-tolerant, too: because it uses a `<form>` element and URLs under the hood, it actually works really well without even loading your WASM on the client.
|
||||
|
||||
We can actually take it a step further and do something kind of clever:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search
|
||||
oninput="this.form.requestSubmit()"
|
||||
/>
|
||||
</Form>
|
||||
}
|
||||
```
|
||||
|
||||
You’ll notice that this version drops the `Submit` button. Instead, we add an `oninput` attribute to the input. Note that this is _not_ `on:input`, which would listen for the `input` event and run some Rust code. Without the colon, `oninput` is the plain HTML attribute. So the string is actually a JavaScript string. `this.form` gives us the form the input is attached to. `requestSubmit()` fires the `submit` event on the `<form>`, which is caught by `<Form/>` just as if we had clicked a `Submit` button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the user’s input as they type.
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
@@ -16,7 +16,7 @@ The Leptos Router works with the path and query (`/blog/search?q=Search`). Given
|
||||
|
||||
## The Philosophy
|
||||
|
||||
In most cases, the path should drive what is displayed on the page. From the user’s perspective, for most appliations, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
|
||||
In most cases, the path should drive what is displayed on the page. From the user’s perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
|
||||
|
||||
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
|
||||
|
||||
|
||||
@@ -107,27 +107,28 @@ fn clear() {
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
We’ll use some manual DOM operations to grab the `<div>` that wraps
|
||||
the whole component, as well as the `clear` button.
|
||||
|
||||
```rust
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
// this would be easier if they had IDs
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = test_wrapper
|
||||
.query_selector("button")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlElement>();
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
// this would be easier if they had IDs
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = test_wrapper
|
||||
.query_selector("button")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlElement>();
|
||||
```
|
||||
|
||||
Now we can use ordinary DOM APIs to simulate user interaction.
|
||||
|
||||
```rust
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
```
|
||||
|
||||
You can test individual DOM element attributes or text node values. Sometimes
|
||||
@@ -135,27 +136,27 @@ I like to test the whole view at once. We can do this by testing the element’s
|
||||
`outerHTML` against our expectations.
|
||||
|
||||
```rust
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
That test involved us manually replicating the `view` that’s inside the component.
|
||||
@@ -164,15 +165,14 @@ with the initial value `0`. This is where our wrapping element comes in: I’ll
|
||||
the wrapper’s `innerHTML` against another comparison case.
|
||||
|
||||
```rust
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
}
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
```
|
||||
|
||||
This is only a very limited introduction to testing. But I hope it’s useful as you begin to build applications.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# A Basic Component
|
||||
|
||||
That “Hello, world!” was a *very* simple example. Let’s move on to something a
|
||||
That “Hello, world!” was a _very_ simple example. Let’s move on to something a
|
||||
little more like an ordinary app.
|
||||
|
||||
First, let’s edit the `main` function so that, instead of rendering the whole
|
||||
app, it just renders an `<App/>` component. Components are the basic unit of
|
||||
composition and design in most web frameworks, and Leptos is no exception.
|
||||
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
`PascalCase`, so most Leptos applications will start with something like an
|
||||
composition and design in most web frameworks, and Leptos is no exception.
|
||||
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
`PascalCase`, so most Leptos applications will start with something like an
|
||||
`<App/>` component.
|
||||
|
||||
```rust
|
||||
@@ -39,11 +39,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
```
|
||||
|
||||
## The Component Signature
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
```
|
||||
|
||||
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||
used as a component in your Leptos application. We’ll see some of the other features of
|
||||
this macro in a couple chapters.
|
||||
|
||||
@@ -52,6 +53,7 @@ fn App(cx: Scope) -> impl IntoView
|
||||
```
|
||||
|
||||
Every component is a function with the following characteristics
|
||||
|
||||
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||
as its first argument. This `Scope` is our entrypoint into the reactive system.
|
||||
By convention, it’s usually named `cx`.
|
||||
@@ -60,7 +62,8 @@ Every component is a function with the following characteristics
|
||||
anything you could return from a Leptos `view`.
|
||||
|
||||
## The Component Body
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
render function that reruns multiple times. You’ll typically use it to create a
|
||||
few reactive variables, define any side effects that run in response to those values
|
||||
changing, and describe the user interface.
|
||||
@@ -68,16 +71,17 @@ changing, and describe the user interface.
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
```
|
||||
|
||||
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
|
||||
creates a signal, the basic unit of reactive change and state management in Leptos.
|
||||
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||
current value, you’ll call `set_count.set(...)` (or `set_count(...)`).
|
||||
|
||||
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||
efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||
> efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||
|
||||
## The View
|
||||
## The View
|
||||
|
||||
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
|
||||
|
||||
@@ -100,25 +104,28 @@ view! { cx,
|
||||
This should mostly be easy to understand: it looks like HTML, with a special
|
||||
`on:click` to define a `click` event listener, a text node that’s formatted like
|
||||
a Rust string, and then...
|
||||
|
||||
```rust
|
||||
{move || count.get()}
|
||||
```
|
||||
|
||||
whatever that is.
|
||||
|
||||
People sometimes joke that they use more closures in their first Leptos application
|
||||
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||
People sometimes joke that they use more closures in their first Leptos application
|
||||
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||
into the view tells the framework: “Hey, this is something that might change.”
|
||||
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
nothing else in your application. This is what allows for extremely efficient updates
|
||||
to the DOM.
|
||||
|
||||
Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice
|
||||
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||
Leptos with `nightly` Rust, signals are already functions, so the closure is unnecessary.
|
||||
As a result, you can write a simpler view:
|
||||
As a result, you can write a simpler view:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<button /* ... */>
|
||||
@@ -129,15 +136,17 @@ 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
|
||||
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()}` access 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!
|
||||
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
> show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
> and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# `view`: Dynamic Attributes and Classes
|
||||
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
create dynamic text by passing a function (such as a signal) into the view.
|
||||
|
||||
But of course there are other things you might want to update in your user interface.
|
||||
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||
and we’ll introduce the concept of a **derived signal**.
|
||||
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
increment a counter.
|
||||
|
||||
```rust
|
||||
@@ -20,6 +20,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
"Click me: "
|
||||
{move || count()}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So far, this is just the example from the last chapter.
|
||||
@@ -28,27 +34,31 @@ So far, this is just the example from the last chapter.
|
||||
|
||||
Now let’s say I’d like to update the list of CSS classes on this element dynamically.
|
||||
For example, let’s say I want to add the class `red` when the count is odd. I can
|
||||
do this using the `class:` syntax.
|
||||
do this using the `class:` syntax.
|
||||
|
||||
```rust
|
||||
class:red=move || count() % 2 == 1
|
||||
```
|
||||
|
||||
`class:` attributes take
|
||||
|
||||
1. the class name, following the colon (`red`)
|
||||
2. a value, which can be a `bool` or a function that returns a `bool`
|
||||
|
||||
When the value is `true`, the class is added. When the value is `false`, the class
|
||||
is removed. And if the value is a function that accesses a signal, the class will
|
||||
is removed. And if the value is a function that accesses a signal, the class will
|
||||
reactively update when the signal changes.
|
||||
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
the number switches between even and odd.
|
||||
|
||||
## Dynamic Attributes
|
||||
|
||||
The same applies to plain attributes. Passing a plain string or primitive value to
|
||||
an attribute gives it a static value. Passing a function (including a signal) to
|
||||
an attribute causes it to update its value reactively. Let’s add another element
|
||||
an attribute causes it to update its value reactively. Let’s add another element
|
||||
to our view:
|
||||
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
@@ -57,17 +67,18 @@ to our view:
|
||||
/>
|
||||
```
|
||||
|
||||
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||
our progress bar will move forward.
|
||||
|
||||
## Derived Signals
|
||||
## Derived Signals
|
||||
|
||||
Let’s go one layer deeper, just for fun.
|
||||
|
||||
You already know that we create reactive interfaces just by passing functions into
|
||||
You already know that we create reactive interfaces just by passing functions into
|
||||
the `view`. This means that we can easily change our progress bar. For example,
|
||||
suppose we want it to move twice as fast:
|
||||
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
@@ -77,28 +88,31 @@ suppose we want it to move twice as fast:
|
||||
|
||||
But imagine we want to reuse that calculation in more than one place. You can do this
|
||||
using a **derived signal**: a closure that accesses a signal.
|
||||
|
||||
```rust
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
/* insert the rest of the view */
|
||||
/* insert the rest of the view */
|
||||
<progress
|
||||
max="50"
|
||||
// we use it once here
|
||||
value=double_count
|
||||
/>
|
||||
<p>
|
||||
"Double Count: "
|
||||
"Double Count: "
|
||||
// and again here
|
||||
{double_count}
|
||||
</p>
|
||||
```
|
||||
|
||||
Derived signals let you create reactive computed values that can be used in multiple
|
||||
Derived signals let you create reactive computed values that can be used in multiple
|
||||
places in your application with minimal overhead.
|
||||
|
||||
> Note: Using a derived signal like this means that the calculation runs once per
|
||||
signal change per place we access `double_count`; in other words, twice. This is a
|
||||
very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
are designed to solve this problem for expensive calculations.
|
||||
> Note: Using a derived signal like this means that the calculation runs once per
|
||||
> signal change per place we access `double_count`; in other words, twice. This is a
|
||||
> very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
> are designed to solve this problem for expensive calculations.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Components and Props
|
||||
|
||||
So far, we’ve been building our whole application in a single component. This
|
||||
is fine for really tiny examples, but in any real application you’ll need to
|
||||
break the user interface out into multiple components, so you can break your
|
||||
So far, we’ve been building our whole application in a single component. This
|
||||
is fine for really tiny examples, but in any real application you’ll need to
|
||||
break the user interface out into multiple components, so you can break your
|
||||
interface down into smaller, reusable, composable chunks.
|
||||
|
||||
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||
instead of one: one that advances one tick per click, one that advances two ticks
|
||||
per click.
|
||||
|
||||
@@ -15,7 +15,7 @@ You _could_ do this by just creating two `<progress>` elements:
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! {
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
value=count
|
||||
@@ -24,10 +24,11 @@ view! {
|
||||
max="50"
|
||||
value=double_count
|
||||
/>
|
||||
}
|
||||
```
|
||||
|
||||
But of course, this doesn’t scale very well. If you want to add a third progress
|
||||
bar, you need to add this code another time. And if you want to edit anything
|
||||
bar, you need to add this code another time. And if you want to edit anything
|
||||
about it, you need to edit it in triplicate.
|
||||
|
||||
Instead, let’s create a `<ProgressBar/>` component.
|
||||
@@ -47,15 +48,15 @@ fn ProgressBar(
|
||||
}
|
||||
```
|
||||
|
||||
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||
When we were defining everything manually, we just used the local variable names.
|
||||
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||
When we were defining everything manually, we just used the local variable names.
|
||||
Now we need some way to pass an argument into the component.
|
||||
|
||||
## Component Props
|
||||
## Component Props
|
||||
|
||||
We do this using component properties, or “props.” If you’ve used another frontend
|
||||
framework, this is probably a familiar idea. Basically, properties are to components
|
||||
as attributes are to HTML elements: they let you pass additional information into
|
||||
framework, this is probably a familiar idea. Basically, properties are to components
|
||||
as attributes are to HTML elements: they let you pass additional information into
|
||||
the component.
|
||||
|
||||
In Leptos, you define props by giving additional arguments to the component function.
|
||||
@@ -69,7 +70,7 @@ fn ProgressBar(
|
||||
view! { cx,
|
||||
<progress
|
||||
max="50"
|
||||
// now this works
|
||||
// now this works
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
@@ -92,41 +93,42 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||
notice that you can easily tell the difference between an element and a component
|
||||
because components always have `PascalCase` names. You pass the `progress` prop
|
||||
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||
notice that you can easily tell the difference between an element and a component
|
||||
because components always have `PascalCase` names. You pass the `progress` prop
|
||||
in as if it were an HTML element attribute. Simple.
|
||||
|
||||
> ### Important Note
|
||||
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||
is what allows us to have named props, when Rust does not have named function parameters.
|
||||
If you’re defining a component in one module and importing it into another, make
|
||||
sure you include this `ComponentProps` type:
|
||||
> ### Important Note
|
||||
>
|
||||
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||
> is what allows us to have named props, when Rust does not have named function parameters.
|
||||
> If you’re defining a component in one module and importing it into another, make
|
||||
> sure you include this `ComponentProps` type:
|
||||
>
|
||||
> `use progress_bar::{ProgressBar, ProgressBarProps};`
|
||||
|
||||
### Reactive and Static Props
|
||||
|
||||
You’ll notice that throughout this example, `progress` takes a reactive
|
||||
You’ll notice that throughout this example, `progress` takes a reactive
|
||||
`ReadSignal<i32>`, and not a plain `i32`. This is **very important**.
|
||||
|
||||
Component props have no special meaning attached to them. A component is simply
|
||||
a function that runs once to set up the user interface. The only way to tell the
|
||||
interface to respond to changing is to pass it a signal type. So if you have a
|
||||
component property that will change over time, like our `progress`, it should
|
||||
Component props have no special meaning attached to them. A component is simply
|
||||
a function that runs once to set up the user interface. The only way to tell the
|
||||
interface to respond to changing is to pass it a signal type. So if you have a
|
||||
component property that will change over time, like our `progress`, it should
|
||||
be a signal.
|
||||
|
||||
### `optional` Props
|
||||
### `optional` Props
|
||||
|
||||
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||
argument to the component function with `#[prop(optional)]`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
// mark this prop optional
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
max: u16,
|
||||
@@ -142,7 +144,7 @@ fn ProgressBar(
|
||||
```
|
||||
|
||||
Now, we can use `<ProgressBar max=50 value=count/>`, or we can omit `max`
|
||||
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||
on an `optional` is its `Default::default()` value, which for a `u16` is going to
|
||||
be `0`. In the case of a progress bar, a max value of `0` is not very useful.
|
||||
|
||||
@@ -187,20 +189,20 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
"Click me"
|
||||
</button>
|
||||
<ProgressBar progress=count/>
|
||||
// add a second progress bar
|
||||
// add a second progress bar
|
||||
<ProgressBar progress=double_count/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hm... this won’t compile. It should be pretty easy to understand why: we’ve declared
|
||||
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||
it’s a closure that returns an `i32`.
|
||||
|
||||
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||
could just take any function?” If you’re savvy, you may know that both these
|
||||
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||
could just take any function?” If you’re savvy, you may know that both these
|
||||
implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
|
||||
```rust
|
||||
@@ -210,8 +212,8 @@ fn ProgressBar<F>(
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
) -> impl IntoView
|
||||
where
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> i32 + 'static,
|
||||
{
|
||||
view! { cx,
|
||||
@@ -223,27 +225,26 @@ where
|
||||
}
|
||||
```
|
||||
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
any value that implements this `Fn()` trait.
|
||||
|
||||
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
|
||||
or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
> or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
> a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
|
||||
### `into` Props
|
||||
### `into` Props
|
||||
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
This attribute automatically calls `.into()` on the values you pass as props,
|
||||
which allows you to easily pass props with different values.
|
||||
|
||||
In this case, it’s helpful to know about the
|
||||
In this case, it’s helpful to know about the
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
|
||||
is an enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
is an enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
reactive value.
|
||||
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
@@ -252,7 +253,7 @@ fn ProgressBar(
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
progress: Signal<i32>
|
||||
) -> impl IntoView
|
||||
) -> impl IntoView
|
||||
{
|
||||
view! { cx,
|
||||
<progress
|
||||
@@ -281,12 +282,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
## Documenting Components
|
||||
|
||||
This is one of the least essential but most important sections of this book.
|
||||
It’s not strictly necessary to document your components and their props. It may
|
||||
be very important, depending on the size of your team and your app. But it’s very
|
||||
This is one of the least essential but most important sections of this book.
|
||||
It’s not strictly necessary to document your components and their props. It may
|
||||
be very important, depending on the size of your team and your app. But it’s very
|
||||
easy, and bears immediate fruit.
|
||||
|
||||
To document a component and its props, you can simply add doc comments on the
|
||||
To document a component and its props, you can simply add doc comments on the
|
||||
component function, and each one of the props:
|
||||
|
||||
```rust
|
||||
@@ -309,9 +310,11 @@ That’s all you need to do. These behave like ordinary Rust doc comments, excep
|
||||
that you can document individual component props, which can’t be done with Rust
|
||||
function arguments.
|
||||
|
||||
This will automatically generate documentation for your component, its `Props`
|
||||
type, and each of the fields used to add props. It can be a little hard to
|
||||
understand how powerful this is until you hover over the component name or props
|
||||
This will automatically generate documentation for your component, its `Props`
|
||||
type, and each of the fields used to add props. It can be a little hard to
|
||||
understand how powerful this is until you hover over the component name or props
|
||||
and see the power of the `#[component]` macro combined with rust-analyzer here.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
# Iteration
|
||||
|
||||
Whether you’re listing todos, displaying a table, or showing product images,
|
||||
Whether you’re listing todos, displaying a table, or showing product images,
|
||||
iterating over a list of items is a common task in web applications. Reconciling
|
||||
the differences between changing sets of items can also be one of the trickiest
|
||||
tasks for a framework to handle well.
|
||||
|
||||
Leptos supports to two different patterns for iterating over items:
|
||||
|
||||
1. For static views: `Vec<_>`
|
||||
2. For dynamic lists: `<For/>`
|
||||
|
||||
## Static Views with `Vec<_>`
|
||||
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
any `Vec<IV> where IV: IntoView` into your view. In other words, if you can render
|
||||
`T`, you can render `Vec<T>`.
|
||||
|
||||
@@ -58,31 +59,34 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||
it changes, this will rerender every item in the list. This is quite inefficient!
|
||||
Fortunately, there’s a better way.
|
||||
|
||||
## Dynamic Rendering with the `<For/>` Component
|
||||
|
||||
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||
keyed dynamic list. It takes three props:
|
||||
|
||||
- `each`: a function (such as a signal) that returns the items `T` to be iterated over
|
||||
- `key`: a key function that takes `&T` and returns a stable, unique key or ID
|
||||
- `view`: renders each `T` into a view
|
||||
- `view`: renders each `T` into a view
|
||||
|
||||
`key` is, well, the key. You can add, remove, and move items within the list. As
|
||||
long as each item’s key is stable over time, the framework does not need to rerender
|
||||
any of the items, unless they are new additions, and it can very efficiently add,
|
||||
remove, and move items as they change. This allows for extremely efficient updates
|
||||
remove, and move items as they change. This allows for extremely efficient updates
|
||||
to the list as it changes, with minimal additional work.
|
||||
|
||||
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||
indices change.
|
||||
|
||||
But it’s a great idea to do something like generating a unique ID for each row as
|
||||
But it’s a great idea to do something like generating a unique ID for each row as
|
||||
it is generated, and using that as an ID for the key function.
|
||||
|
||||
Check out the `<DynamicList/>` component below for an example.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
# Forms and Inputs
|
||||
|
||||
Forms and form inputs are an important part of interactive apps. There are two
|
||||
Forms and form inputs are an important part of interactive apps. There are two
|
||||
basic patterns for interacting with inputs in Leptos, which you may recognize
|
||||
if you’re familiar with React, SolidJS, or a similar framework: using **controlled**
|
||||
or **uncontrolled** inputs.
|
||||
|
||||
## Controlled Inputs
|
||||
|
||||
In a "controlled input," the framework controls the state of the input
|
||||
element. On every `input` event, it updates a local signal that holds the current
|
||||
In a "controlled input," the framework controls the state of the input
|
||||
element. On every `input` event, it updates a local signal that holds the current
|
||||
state, which in turn updates the `value` prop of the input.
|
||||
|
||||
There are two important things to remember:
|
||||
1. The `input` event fires on (almost) every change to the element, while the
|
||||
`change` event fires (more or less) when you unfocus the input. You probably
|
||||
|
||||
1. The `input` event fires on (almost) every change to the element, while the
|
||||
`change` event fires (more or less) when you unfocus the input. You probably
|
||||
want `on:input`, but we give you the freedom to choose.
|
||||
2. The `value` *attribute* only sets the initial value of the input, i.e., it
|
||||
only updates the input up to the point that you begin typing. The `value`
|
||||
*property* continues updating the input after that. You usually want to set
|
||||
2. The `value` _attribute_ only sets the initial value of the input, i.e., it
|
||||
only updates the input up to the point that you begin typing. The `value`
|
||||
_property_ continues updating the input after that. You usually want to set
|
||||
`prop:value` for this reason.
|
||||
|
||||
```rust
|
||||
@@ -41,14 +42,14 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
## Uncontrolled Inputs
|
||||
## Uncontrolled Inputs
|
||||
|
||||
In an "uncontrolled input," the browser controls the state of the input element.
|
||||
Rather than continuously updating a signal to hold its value, we use a
|
||||
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||
In an "uncontrolled input," the browser controls the state of the input element.
|
||||
Rather than continuously updating a signal to hold its value, we use a
|
||||
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||
the input once when we want to get its value.
|
||||
|
||||
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
event.
|
||||
|
||||
```rust
|
||||
@@ -56,7 +57,8 @@ let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||
|
||||
let input_element: NodeRef<Input> = create_node_ref(cx);
|
||||
```
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
underlying DOM node. Its value will be set when the element is rendered.
|
||||
|
||||
```rust
|
||||
@@ -76,13 +78,14 @@ let on_submit = move |ev: SubmitEvent| {
|
||||
set_name(value);
|
||||
};
|
||||
```
|
||||
|
||||
Our `on_submit` handler will access the input’s value and use it to call `set_name`.
|
||||
To access the DOM node stored in the `NodeRef`, we can simply call it as a function
|
||||
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||
know it will already have been filled when we rendered the view, so it’s safe to
|
||||
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||
know it will already have been filled when we rendered the view, so it’s safe to
|
||||
unwrap here.
|
||||
|
||||
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
gives us access to a correctly-typed HTML element.
|
||||
|
||||
```rust
|
||||
@@ -97,11 +100,15 @@ view! { cx,
|
||||
<p>"Name is: " {name}</p>
|
||||
}
|
||||
```
|
||||
|
||||
The view should be pretty self-explanatory by now. Note two things:
|
||||
|
||||
1. Unlike in the controlled input example, we use `value` (not `prop:value`).
|
||||
This is because we’re just setting the initial value of the input, and letting
|
||||
This is because we’re just setting the initial value of the input, and letting
|
||||
the browser control its state. (We could use `prop:value` instead.)
|
||||
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
|
||||
They are the same thing, but `node_ref` has better rust-analyzer support.)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -282,4 +282,6 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -110,4 +110,6 @@ Not a number! Errors:
|
||||
If you fix the error, the error message will disappear and the content you’re wrapping in
|
||||
an `<ErrorBoundary/>` will appear again.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -285,4 +285,6 @@ in `<ButtonD/>` and a single text node in `<App/>`. It’s as if the components
|
||||
themselves don’t exist at all. And, well... at runtime, they don’t. It’s just
|
||||
signals and effects, all the way down.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -123,4 +123,6 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
|
||||
@@ -198,13 +198,13 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
source.subscribe("message").unwrap().map(|value| {
|
||||
value
|
||||
.expect("no message event")
|
||||
.1
|
||||
.data()
|
||||
.as_string()
|
||||
.expect("expected string value")
|
||||
}),
|
||||
match value {
|
||||
Ok(value) => {
|
||||
value.1.data().as_string().expect("expected string value")
|
||||
},
|
||||
Err(_) => "0".to_string(),
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
on_cleanup(cx, move || source.close());
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
@@ -1,48 +1,44 @@
|
||||
use leptos::{ev, html::*, *};
|
||||
|
||||
pub struct Props {
|
||||
/// The starting value for the counter
|
||||
pub initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
pub step: i32,
|
||||
}
|
||||
|
||||
/// A simple counter view.
|
||||
pub fn view(cx: Scope, props: Props) -> impl IntoView {
|
||||
let Props {
|
||||
initial_value,
|
||||
step,
|
||||
} = props;
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
// elements are created by calling a function with a Scope argument
|
||||
// the function name is the same as the HTML tag name
|
||||
div(cx)
|
||||
.child((
|
||||
cx,
|
||||
// children can be added with .child()
|
||||
// this takes any type that implements IntoView as its argument
|
||||
// for example, a string or an HtmlElement<_>
|
||||
.child(
|
||||
button(cx)
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_value.update(|value| *value = 0))
|
||||
.child((cx, "Clear")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("Clear"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value -= step)
|
||||
})
|
||||
.child((cx, "-1")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("-1"),
|
||||
)
|
||||
.child(
|
||||
span(cx)
|
||||
.child((cx, "Value: "))
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child((cx, move || value.get()))
|
||||
.child((cx, "!")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("!"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value += step)
|
||||
})
|
||||
.child((cx, "+1")),
|
||||
))
|
||||
.child("+1"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
use counter_without_macros as counter;
|
||||
use counter_without_macros::counter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
})
|
||||
mount_to_body(|cx| counter(cx, 0, 1))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -5,6 +5,6 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
|
||||
@@ -70,8 +70,8 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
</p>
|
||||
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
|
||||
<div>
|
||||
// note that the error boundries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribue to the
|
||||
// note that the error boundaries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribute to the
|
||||
// final status code sent by the server when producing ssr pages.
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
|
||||
@@ -9,7 +9,7 @@ leptos = { path = "../../leptos" }
|
||||
reqwasm = "0.5.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link =
|
||||
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
move |cx| pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
@@ -65,16 +65,20 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
<Transition
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
<span class="page-link"
|
||||
class:disabled=move || hide_more_link(cx)
|
||||
aria-hidden=move || hide_more_link(cx)
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</Transition>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
|
||||
@@ -12,7 +12,7 @@ leptos_router = { version = "0.2.0-alpha2", features = ["stable", "csr"] }
|
||||
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
gloo-net = "0.2"
|
||||
gloo-storage = "0.2"
|
||||
serde = "1.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[[proxy]]
|
||||
rewrite = "/api/"
|
||||
backend = "http://0.0.0.0:3000/"
|
||||
backend = "http://127.0.0.1:3000/"
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
web-sys = "0.3"
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
|
||||
@@ -28,19 +28,7 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=""
|
||||
view=move |cx| view! { cx, <ContactList/> }
|
||||
>
|
||||
<Route
|
||||
path=":id"
|
||||
view=move |cx| view! { cx, <Contact/> }
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
view=move |_| view! { cx, <p>"Select a contact."</p> }
|
||||
/>
|
||||
</Route>
|
||||
<ContactRoutes/>
|
||||
<Route
|
||||
path="about"
|
||||
view=move |cx| view! { cx, <About/> }
|
||||
@@ -59,6 +47,27 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
// You can define other routes in their own component.
|
||||
// Use a #[component(transparent)] that returns a <Route/>.
|
||||
#[component(transparent)]
|
||||
pub fn ContactRoutes(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<Route
|
||||
path=""
|
||||
view=move |cx| view! { cx, <ContactList/> }
|
||||
>
|
||||
<Route
|
||||
path=":id"
|
||||
view=move |cx| view! { cx, <Contact/> }
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
view=move |_| view! { cx, <p>"Select a contact."</p> }
|
||||
/>
|
||||
</Route>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
log::debug!("rendering <ContactList/>");
|
||||
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
|
||||
@@ -6,7 +6,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{Response, IntoResponse},
|
||||
routing::{post, get},
|
||||
extract::{Path, Extension},
|
||||
extract::{Path, Extension, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
@@ -22,11 +22,12 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_sessions_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
|
||||
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, request: Request<AxumBody>) -> impl IntoResponse {
|
||||
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
|
||||
request: Request<AxumBody>) -> impl IntoResponse {
|
||||
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(path, headers, move |cx| {
|
||||
handle_server_fns_with_context(path, headers, raw_query, move |cx| {
|
||||
provide_context(cx, auth_session.clone());
|
||||
provide_context(cx, pool.clone());
|
||||
}, request).await
|
||||
@@ -73,7 +74,7 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(server_fn_handler))
|
||||
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
|
||||
@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
|
||||
@@ -22,7 +22,7 @@ cfg-if = "1.0"
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
console_log = { version = "0.2", optional = true }
|
||||
console_log = { version = "1", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# dependecies for server (enable when ssr set)
|
||||
|
||||
@@ -16,6 +16,6 @@ gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
console_log = { version = "0.2"}
|
||||
console_log = { version = "1"}
|
||||
console_error_panic_hook = { version = "0.1"}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ actix-files = { version = "0.6.2", optional = true }
|
||||
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
|
||||
anyhow = "1.0.68"
|
||||
broadcaster = "1.0.0"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
futures = "0.3.25"
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
@@ -51,7 +51,8 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", default-features = false }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
uuid = { version = "1", features = ["v4", "js", "serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -18,9 +18,10 @@ use http::StatusCode;
|
||||
use leptos::{
|
||||
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
@@ -94,7 +95,7 @@ impl ResponseOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
|
||||
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
|
||||
pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
@@ -150,9 +151,9 @@ pub fn handle_server_fns() -> Route {
|
||||
handle_server_fns_with_context(|_cx| {})
|
||||
}
|
||||
|
||||
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
|
||||
/// Leptos server function arguments in the body, runs the server function if found,
|
||||
/// and returns the resulting [HttpResponse].
|
||||
/// An Actix [Route](actix_web::Route) that listens for `GET` or `POST` requests with
|
||||
/// Leptos server function arguments in the URL (`GET`) or body (`POST`),
|
||||
/// runs the server function if found, and returns the resulting [HttpResponse].
|
||||
///
|
||||
/// This provides the [HttpRequest] to the server [Scope](leptos::Scope).
|
||||
///
|
||||
@@ -168,7 +169,7 @@ pub fn handle_server_fns() -> Route {
|
||||
pub fn handle_server_fns_with_context(
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> Route {
|
||||
web::post().to(
|
||||
web::to(
|
||||
move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| {
|
||||
let additional_context = additional_context.clone();
|
||||
async move {
|
||||
@@ -194,7 +195,13 @@ pub fn handle_server_fns_with_context(
|
||||
provide_context(cx, req.clone());
|
||||
provide_context(cx, res_options.clone());
|
||||
|
||||
match server_fn(cx, body).await {
|
||||
let query = req.query_string().as_bytes();
|
||||
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
match (server_fn.trait_obj)(cx, data).await {
|
||||
Ok(serialized) => {
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
@@ -340,14 +347,15 @@ where
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in
|
||||
/// the documentation for that function.
|
||||
/// The HTML stream is rendered using
|
||||
/// [render_to_stream_in_order](leptos::ssr::render_to_stream_in_order),
|
||||
/// and includes everything described in the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
@@ -409,8 +417,8 @@ where
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to the app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_string_async], and includes everything described in
|
||||
/// the documentation for that function.
|
||||
/// The HTML stream is rendered using [render_to_string_async](leptos::ssr::render_to_string_async), and
|
||||
/// includes everything described in the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
@@ -728,7 +736,7 @@ async fn stream_app(
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| generate_head_metadata(cx).into(),
|
||||
move |cx| generate_head_metadata_separated(cx).1.into(),
|
||||
additional_context,
|
||||
);
|
||||
|
||||
@@ -745,7 +753,7 @@ async fn stream_app_in_order(
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| {
|
||||
generate_head_metadata(cx).into()
|
||||
generate_head_metadata_separated(cx).1.into()
|
||||
},
|
||||
additional_context,
|
||||
);
|
||||
@@ -762,7 +770,7 @@ async fn build_stream_response(
|
||||
) -> HttpResponse {
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
use axum::{
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
extract::Path,
|
||||
extract::{Path, RawQuery},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
HeaderMap, Request, StatusCode,
|
||||
@@ -24,11 +24,12 @@ use http::{header, method::Method, uri::Uri, version::Version, Response};
|
||||
use hyper::body;
|
||||
use leptos::{
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
ssr::*,
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts};
|
||||
use leptos_meta::{generate_head_metadata, MetaContext};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
use leptos_meta::{generate_head_metadata_separated, MetaContext};
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use std::{io, pin::Pin, sync::Arc};
|
||||
@@ -95,7 +96,7 @@ impl ResponseOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
|
||||
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
|
||||
pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
@@ -128,7 +129,7 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
/// and other data without consuming the body. Creates a new Request from the
|
||||
/// original parts for further processsing
|
||||
/// original parts for further processing
|
||||
pub async fn generate_request_and_parts(
|
||||
req: Request<Body>,
|
||||
) -> (Request<Body>, RequestParts) {
|
||||
@@ -147,8 +148,9 @@ pub async fn generate_request_and_parts(
|
||||
(request, request_parts)
|
||||
}
|
||||
|
||||
/// A struct to hold the http::request::Request and allow users to take ownership of it
|
||||
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
|
||||
/// A struct to hold the [`http::request::Request`] and allow users to take ownership of it
|
||||
/// Required by `Request` not being `Clone`. See
|
||||
/// [this issue](https://github.com/hyperium/http/pull/574) for eventual resolution:
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LeptosRequest<B>(Arc<RwLock<Option<Request<B>>>>);
|
||||
|
||||
@@ -158,12 +160,12 @@ impl<B> Clone for LeptosRequest<B> {
|
||||
}
|
||||
}
|
||||
impl<B> LeptosRequest<B> {
|
||||
/// Overwrite the contents of a LeptosRequest with a new Request<B>
|
||||
/// Overwrite the contents of a LeptosRequest with a new `Request<B>`
|
||||
pub fn overwrite(&self, req: Option<Request<B>>) {
|
||||
let mut writable = self.0.write();
|
||||
*writable = req
|
||||
}
|
||||
/// Consume the inner Request<B> inside the LeptosRequest and return it
|
||||
/// Consume the inner `Request<B>` inside the LeptosRequest and return it
|
||||
///```rust, ignore
|
||||
/// use axum::{
|
||||
/// RequestPartsExt,
|
||||
@@ -198,8 +200,9 @@ impl<B> LeptosRequest<B> {
|
||||
}
|
||||
}
|
||||
/// Generate a wrapper for the http::Request::Request type that allows one to
|
||||
/// processs it, access the body, and use axum Extractors on it.
|
||||
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
|
||||
/// process it, access the body, and use axum Extractors on it.
|
||||
/// Required by Request not being Clone. See
|
||||
/// [this issue](https://github.com/hyperium/http/pull/574) for eventual resolution:
|
||||
pub async fn generate_leptos_request<B>(req: Request<B>) -> LeptosRequest<B>
|
||||
where
|
||||
B: Default + std::fmt::Debug,
|
||||
@@ -246,9 +249,10 @@ where
|
||||
pub async fn handle_server_fns(
|
||||
Path(fn_name): Path<String>,
|
||||
headers: HeaderMap,
|
||||
RawQuery(query): RawQuery,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
handle_server_fns_inner(fn_name, headers, |_| {}, req).await
|
||||
handle_server_fns_inner(fn_name, headers, query, |_| {}, req).await
|
||||
}
|
||||
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
@@ -268,15 +272,18 @@ pub async fn handle_server_fns(
|
||||
pub async fn handle_server_fns_with_context(
|
||||
Path(fn_name): Path<String>,
|
||||
headers: HeaderMap,
|
||||
RawQuery(query): RawQuery,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
handle_server_fns_inner(fn_name, headers, additional_context, req).await
|
||||
handle_server_fns_inner(fn_name, headers, query, additional_context, req)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_server_fns_inner(
|
||||
fn_name: String,
|
||||
headers: HeaderMap,
|
||||
query: Option<String>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -310,7 +317,15 @@ async fn handle_server_fns_inner(
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
match server_fn(cx, &req_parts.body).await {
|
||||
let query: &Bytes =
|
||||
&query.unwrap_or("".to_string()).into();
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => {
|
||||
&req_parts.body
|
||||
}
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
match (server_fn.trait_obj)(cx, data).await {
|
||||
Ok(serialized) => {
|
||||
// If ResponseOptions are set, add the headers and status to the request
|
||||
let res_options =
|
||||
@@ -495,7 +510,7 @@ where
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
@@ -653,7 +668,7 @@ where
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata(cx).into(),
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
@@ -711,7 +726,7 @@ async fn forward_stream(
|
||||
) {
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
_ = tx.send(head).await;
|
||||
let mut shell = Box::pin(bundle);
|
||||
@@ -735,7 +750,7 @@ async fn forward_stream(
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
@@ -822,7 +837,7 @@ where
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata(cx).into(),
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
|
||||
@@ -3,25 +3,10 @@ use leptos::{use_context, RuntimeId, ScopeId};
|
||||
use leptos_config::LeptosOptions;
|
||||
use leptos_meta::MetaContext;
|
||||
|
||||
pub fn html_parts(
|
||||
options: &LeptosOptions,
|
||||
meta: Option<&MetaContext>,
|
||||
) -> (String, &'static str) {
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
fn autoreload(options: &LeptosOptions) -> String {
|
||||
let site_ip = &options.site_addr.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
@@ -52,7 +37,25 @@ pub fn html_parts(
|
||||
leptos_hot_reload::HOT_RELOAD_JS
|
||||
),
|
||||
false => "".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn html_parts(
|
||||
options: &LeptosOptions,
|
||||
meta: Option<&MetaContext>,
|
||||
) -> (String, &'static str) {
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let leptos_autoreload = autoreload(options);
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
@@ -72,6 +75,46 @@ pub fn html_parts(
|
||||
(head, tail)
|
||||
}
|
||||
|
||||
pub fn html_parts_separated(
|
||||
options: &LeptosOptions,
|
||||
meta: Option<&MetaContext>,
|
||||
) -> (String, &'static str) {
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
let mut wasm_output_name = output_name.clone();
|
||||
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let leptos_autoreload = autoreload(options);
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let head = meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html{html_metadata}>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
{head}
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
let tail = "</body></html>";
|
||||
(head, tail)
|
||||
}
|
||||
|
||||
pub async fn build_async_response(
|
||||
stream: impl Stream<Item = String> + 'static,
|
||||
options: &LeptosOptions,
|
||||
@@ -86,7 +129,7 @@ pub async fn build_async_response(
|
||||
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
// in async, we load the meta content *now*, after the suspenses have resolved
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
|
||||
@@ -14,11 +14,12 @@ use http::{header, method::Method, uri::Uri, version::Version, StatusCode};
|
||||
use hyper::body;
|
||||
use leptos::{
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
ssr::*,
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts};
|
||||
use leptos_meta::{generate_head_metadata, MetaContext};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
use leptos_meta::{generate_head_metadata_separated, MetaContext};
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
@@ -90,7 +91,7 @@ impl ResponseOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
|
||||
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
|
||||
pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
@@ -185,6 +186,7 @@ async fn handle_server_fns_inner(
|
||||
) -> Result<Response> {
|
||||
let fn_name = req.params::<String>()?;
|
||||
let headers = req.headers().clone();
|
||||
let query = req.query_string().unwrap_or("").to_owned().into();
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
spawn_blocking({
|
||||
move || {
|
||||
@@ -207,7 +209,14 @@ async fn handle_server_fns_inner(
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
match server_fn(cx, &req_parts.body).await {
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => {
|
||||
&req_parts.body
|
||||
}
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
|
||||
match (server_fn.trait_obj)(cx, data).await {
|
||||
Ok(serialized) => {
|
||||
// If ResponseOptions are set, add the headers and status to the request
|
||||
let res_options =
|
||||
@@ -385,7 +394,7 @@ where
|
||||
|
||||
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
@@ -536,7 +545,7 @@ where
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata(cx).into(),
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
@@ -593,7 +602,7 @@ async fn forward_stream(
|
||||
) {
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
_ = tx.send(head).await;
|
||||
let mut shell = Box::pin(bundle);
|
||||
@@ -617,7 +626,7 @@ async fn forward_stream(
|
||||
|
||||
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This version allows us to pass Viz State/Extractor or other infro from Viz or network
|
||||
@@ -700,7 +709,7 @@ where
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata(cx).into(),
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
|
||||
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
|
||||
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
|
||||
//! a fully-functional application that works as intended even before WASM has loaded and begun to run.
|
||||
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite),
|
||||
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum), and
|
||||
//! [`todo_app_sqlite_viz`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_viz)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use leptos::component;
|
||||
use leptos_dom::{Fragment, IntoView};
|
||||
use leptos_reactive::{create_memo, signal_prelude::*, Scope};
|
||||
use leptos_reactive::{create_memo, signal_prelude::*, Scope, ScopeDisposer};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
/// A component that will show its children when the `when` condition is `true`,
|
||||
/// and show the fallback when it is `false`, without rerendering every time
|
||||
@@ -45,9 +46,18 @@ where
|
||||
IV: IntoView,
|
||||
{
|
||||
let memoized_when = create_memo(cx, move |_| when());
|
||||
let prev_disposer = Rc::new(RefCell::new(None::<ScopeDisposer>));
|
||||
|
||||
move || match memoized_when.get() {
|
||||
true => children(cx).into_view(cx),
|
||||
false => fallback(cx).into_view(cx),
|
||||
move || {
|
||||
if let Some(disposer) = prev_disposer.take() {
|
||||
disposer.dispose();
|
||||
}
|
||||
let (view, disposer) =
|
||||
cx.run_child_scope(|cx| match memoized_when.get() {
|
||||
true => children(cx).into_view(cx),
|
||||
false => fallback(cx).into_view(cx),
|
||||
});
|
||||
*prev_disposer.borrow_mut() = Some(disposer);
|
||||
view
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +79,9 @@ where
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
if context.ready() {
|
||||
orig_child(cx).into_view(cx)
|
||||
Fragment::lazy(Box::new(|| vec![orig_child(cx).into_view(cx)])).into_view(cx)
|
||||
} else {
|
||||
fallback().into_view(cx)
|
||||
Fragment::lazy(Box::new(|| vec![fallback().into_view(cx)])).into_view(cx)
|
||||
}
|
||||
} else {
|
||||
use leptos_reactive::signal_prelude::*;
|
||||
@@ -108,10 +108,12 @@ where
|
||||
let orig_child = Rc::clone(&orig_child);
|
||||
move || {
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
DynChild::new(move || orig_child(cx))
|
||||
.into_view(cx)
|
||||
.render_to_string(cx)
|
||||
.to_string()
|
||||
Fragment::lazy(Box::new(move || {
|
||||
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
|
||||
}))
|
||||
.into_view(cx)
|
||||
.render_to_string(cx)
|
||||
.to_string()
|
||||
}
|
||||
},
|
||||
// in-order streaming
|
||||
@@ -119,14 +121,16 @@ where
|
||||
let current_id = current_id.clone();
|
||||
move || {
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
DynChild::new(move || orig_child(cx))
|
||||
.into_view(cx)
|
||||
.into_stream_chunks(cx)
|
||||
Fragment::lazy(Box::new(move || {
|
||||
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
|
||||
}))
|
||||
.into_view(cx)
|
||||
.into_stream_chunks(cx)
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// return the fallback for now, wrapped in fragment identifer
|
||||
// return the fallback for now, wrapped in fragment identifier
|
||||
fallback().into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
13
leptos/tests/test_examples/suspense-tests/.gitignore
vendored
Normal file
13
leptos/tests/test_examples/suspense-tests/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
pkg
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# node e2e test tools and outputs
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
78
leptos/tests/test_examples/suspense-tests/Cargo.toml
Normal file
78
leptos/tests/test_examples/suspense-tests/Cargo.toml
Normal file
@@ -0,0 +1,78 @@
|
||||
[package]
|
||||
name = "leptos_start"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../..", default-features = false, features = ["serde"] }
|
||||
leptos_actix = { path = "../../../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4"
|
||||
wasm-bindgen = "0.2"
|
||||
serde = "1.0.159"
|
||||
tokio = { version = "1.27.0", features = ["time"], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:leptos_actix",
|
||||
"leptos/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:tokio",
|
||||
]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "leptos_start"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||
# This binary name can be checked in Powershell with Get-Command npx
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
[workspace]
|
||||
21
leptos/tests/test_examples/suspense-tests/LICENSE
Normal file
21
leptos/tests/test_examples/suspense-tests/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 henrik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
61
leptos/tests/test_examples/suspense-tests/README.md
Normal file
61
leptos/tests/test_examples/suspense-tests/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
<picture>
|
||||
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
|
||||
</picture>
|
||||
|
||||
# Leptos Starter Template
|
||||
|
||||
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
|
||||
|
||||
## Creating your template repo
|
||||
|
||||
If you don't have `cargo-leptos` installed you can install it with
|
||||
|
||||
`cargo install cargo-leptos`
|
||||
|
||||
Then run
|
||||
|
||||
`cargo leptos new --git leptos-rs/start`
|
||||
|
||||
to generate a new project template.
|
||||
|
||||
`cd {projectname}`
|
||||
|
||||
to go to your newly created project.
|
||||
|
||||
Of course you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
|
||||
|
||||
## Running your project
|
||||
|
||||
`cargo leptos watch`
|
||||
|
||||
## Installing Additional Tools
|
||||
|
||||
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
|
||||
|
||||
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
|
||||
2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on
|
||||
3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
|
||||
4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
|
||||
5. `npm install -g sass` - install `dart-sass` (should be optional in future)
|
||||
|
||||
## Executing a Server on a Remote Machine Without the Toolchain
|
||||
After running a `cargo leptos build --release` the minimum files needed are:
|
||||
|
||||
1. The server binary located in `target/server/release`
|
||||
2. The `site` directory and all files within located in `target/site`
|
||||
|
||||
Copy these files to your remote server. The directory structure should be:
|
||||
```text
|
||||
leptos_start
|
||||
site/
|
||||
```
|
||||
Set the following enviornment variables (updating for your project as needed):
|
||||
```text
|
||||
LEPTOS_OUTPUT_NAME="leptos_start"
|
||||
LEPTOS_SITE_ROOT="site"
|
||||
LEPTOS_SITE_PKG_DIR="pkg"
|
||||
LEPTOS_SITE_ADDR="127.0.0.1:3000"
|
||||
LEPTOS_RELOAD_PORT="3001"
|
||||
```
|
||||
Finally, run the server binary.
|
||||
74
leptos/tests/test_examples/suspense-tests/end2end/package-lock.json
generated
Normal file
74
leptos/tests/test_examples/suspense-tests/end2end/package-lock.json
generated
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
import { devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./tests",
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: {
|
||||
...devices["Desktop Firefox"],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "webkit",
|
||||
use: {
|
||||
...devices["Desktop Safari"],
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
|
||||
await expect(page).toHaveTitle("Welcome to Leptos");
|
||||
|
||||
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
|
||||
});
|
||||
219
leptos/tests/test_examples/suspense-tests/src/app.rs
Normal file
219
leptos/tests/test_examples/suspense-tests/src/app.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[server(OneSecondFn "/api")]
|
||||
async fn one_second_fn(query: ()) -> Result<(), ServerFnError> {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(TwoSecondFn "/api")]
|
||||
async fn two_second_fn(query: ()) -> Result<(), ServerFnError> {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let style = r#"
|
||||
nav {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
[aria-current] {
|
||||
font-weight: bold;
|
||||
}
|
||||
"#;
|
||||
view! {
|
||||
cx,
|
||||
<style>{style}</style>
|
||||
<Router>
|
||||
<nav>
|
||||
<A href="/out-of-order">"Out-of-Order"</A>
|
||||
<A href="/in-order">"In-Order"</A>
|
||||
<A href="/async">"Async"</A>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=""
|
||||
view=|cx| view! { cx, <Redirect path="/out-of-order"/> }
|
||||
/>
|
||||
// out-of-order
|
||||
<Route
|
||||
path="out-of-order"
|
||||
view=|cx| view! { cx,
|
||||
<SecondaryNav/>
|
||||
<h1>"Out-of-Order"</h1>
|
||||
<Outlet/>
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
</Route>
|
||||
// in-order
|
||||
<Route
|
||||
path="in-order"
|
||||
ssr=SsrMode::InOrder
|
||||
view=|cx| view! { cx,
|
||||
<SecondaryNav/>
|
||||
<h1>"In-Order"</h1>
|
||||
<Outlet/>
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
</Route>
|
||||
// async
|
||||
<Route
|
||||
path="async"
|
||||
ssr=SsrMode::Async
|
||||
view=|cx| view! { cx,
|
||||
<SecondaryNav/>
|
||||
<h1>"Async"</h1>
|
||||
<Outlet/>
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SecondaryNav(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<nav>
|
||||
<A href="" exact=true>"Nested"</A>
|
||||
<A href="single">"Single"</A>
|
||||
<A href="parallel">"Parallel"</A>
|
||||
<A href="inside-component">"Inside Component"</A>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Nested(cx: Scope) -> impl IntoView {
|
||||
let one_second = create_resource(cx, || (), one_second_fn);
|
||||
let two_second = create_resource(cx, || (), two_second_fn);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<Suspense fallback=|| "Loading 1...">
|
||||
"One Second: "
|
||||
{move || {
|
||||
one_second.read(cx).map(|_| "Loaded 1!")
|
||||
}}
|
||||
<br/><br/>
|
||||
<Suspense fallback=|| "Loading 2...">
|
||||
"Two Second: "
|
||||
{move || {
|
||||
two_second.read(cx).map(|_| "Loaded 2!")
|
||||
}}
|
||||
</Suspense>
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Parallel(cx: Scope) -> impl IntoView {
|
||||
let one_second = create_resource(cx, || (), one_second_fn);
|
||||
let two_second = create_resource(cx, || (), two_second_fn);
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<Suspense fallback=|| "Loading 1...">
|
||||
"One Second: "
|
||||
{move || {
|
||||
one_second.read(cx).map(move |_| view! { cx,
|
||||
"Loaded 1"
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
<br/><br/>
|
||||
<Suspense fallback=|| "Loading 2...">
|
||||
"Two Second: "
|
||||
{move || {
|
||||
two_second.read(cx).map(move |_| view! { cx,
|
||||
"Loaded 2"
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Single(cx: Scope) -> impl IntoView {
|
||||
let one_second = create_resource(cx, || (), one_second_fn);
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<Suspense fallback=|| "Loading 1...">
|
||||
"One Second: "
|
||||
{move || {
|
||||
one_second.read(cx).map(|_| "Loaded 1!")
|
||||
}}
|
||||
</Suspense>
|
||||
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
|
||||
<div>
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn InsideComponent(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<p><code>"<Suspense/>"</code> " inside another component should work."</p>
|
||||
<InsideComponentChild/>
|
||||
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
|
||||
<div>
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn InsideComponentChild(cx: Scope) -> impl IntoView {
|
||||
let one_second = create_resource(cx, || (), one_second_fn);
|
||||
view! { cx,
|
||||
<Suspense fallback=|| "Loading 1...">
|
||||
"One Second: "
|
||||
{move || {
|
||||
one_second.read(cx).map(|_| "Loaded 1!")
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
23
leptos/tests/test_examples/suspense-tests/src/lib.rs
Normal file
23
leptos/tests/test_examples/suspense-tests/src/lib.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
pub mod app;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
use leptos::*;
|
||||
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(move |cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
42
leptos/tests/test_examples/suspense-tests/src/main.rs
Normal file
42
leptos/tests/test_examples/suspense-tests/src/main.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use leptos_start::app::*;
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> });
|
||||
|
||||
OneSecondFn::register().unwrap();
|
||||
TwoSecondFn::register().unwrap();
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
}
|
||||
@@ -10,7 +10,6 @@ readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
config = "0.13.3"
|
||||
fs = "0.0.5"
|
||||
regex = "1.7.0"
|
||||
serde = { version = "1.0.151", features = ["derive"] }
|
||||
thiserror = "1.0.38"
|
||||
|
||||
@@ -34,6 +34,11 @@ leptos = { path = "../leptos" }
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"DocumentFragment",
|
||||
"Element",
|
||||
"HtmlTemplateElement",
|
||||
"NodeList",
|
||||
"Window",
|
||||
"console",
|
||||
"Comment",
|
||||
"Document",
|
||||
@@ -45,6 +50,7 @@ features = [
|
||||
"TreeWalker",
|
||||
|
||||
# Events we cast to in leptos_macro -- added here so we don't force users to import them
|
||||
"AddEventListenerOptions",
|
||||
"AnimationEvent",
|
||||
"BeforeUnloadEvent",
|
||||
"ClipboardEvent",
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
@@ -28,9 +28,15 @@ pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
|
||||
event.event_delegation_key(),
|
||||
event_name,
|
||||
event_handler,
|
||||
&None,
|
||||
);
|
||||
} else {
|
||||
add_event_listener_undelegated(target, &event_name, event_handler);
|
||||
add_event_listener_undelegated(
|
||||
target,
|
||||
&event_name,
|
||||
event_handler,
|
||||
&None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +49,7 @@ pub fn add_event_listener<E>(
|
||||
event_name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
@@ -50,8 +57,10 @@ pub fn add_event_listener<E>(
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -59,7 +68,7 @@ pub fn add_event_listener<E>(
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = intern(&key);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
add_delegated_event_listener(&key, event_name);
|
||||
add_delegated_event_listener(&key, event_name, options);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -69,22 +78,35 @@ pub(crate) fn add_event_listener_undelegated<E>(
|
||||
event_name: &str,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
}
|
||||
}
|
||||
|
||||
let event_name = intern(event_name);
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
if let Some(options) = options {
|
||||
_ = target
|
||||
.add_event_listener_with_callback_and_add_event_listener_options(
|
||||
event_name,
|
||||
cb.unchecked_ref(),
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
_ = target
|
||||
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
}
|
||||
|
||||
// cf eventHandler in ryansolid/dom-expressions
|
||||
@@ -92,6 +114,7 @@ pub(crate) fn add_event_listener_undelegated<E>(
|
||||
pub(crate) fn add_delegated_event_listener(
|
||||
key: &str,
|
||||
event_name: Cow<'static, str>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) {
|
||||
GLOBAL_EVENTS.with(|global_events| {
|
||||
let mut events = global_events.borrow_mut();
|
||||
@@ -163,10 +186,19 @@ pub(crate) fn add_delegated_event_listener(
|
||||
|
||||
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
|
||||
let handler = Closure::wrap(handler).into_js_value();
|
||||
_ = crate::window().add_event_listener_with_callback(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
);
|
||||
if let Some(options) = options {
|
||||
_ = crate::window().add_event_listener_with_callback_and_add_event_listener_options(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
_ = crate::window().add_event_listener_with_callback(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// register that we've created handler
|
||||
events.insert(event_name);
|
||||
|
||||
@@ -22,6 +22,12 @@ pub trait EventDescriptor: Clone {
|
||||
fn bubbles(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Return the options for this type. This is only used when you create a [`Custom`] event
|
||||
/// handler.
|
||||
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
|
||||
&None
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides the [`EventDescriptor::bubbles`] method to always return
|
||||
@@ -49,6 +55,7 @@ impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
|
||||
/// A custom event.
|
||||
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
|
||||
name: Cow<'static, str>,
|
||||
options: Option<web_sys::AddEventListenerOptions>,
|
||||
_event_type: PhantomData<E>,
|
||||
}
|
||||
|
||||
@@ -56,6 +63,7 @@ impl<E: FromWasmAbi> Clone for Custom<E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
options: self.options.clone(),
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -75,6 +83,10 @@ impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
|
||||
&self.options
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> Custom<E> {
|
||||
@@ -84,9 +96,35 @@ impl<E: FromWasmAbi> Custom<E> {
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
options: None,
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the [`AddEventListenerOptions`] used for this event listener.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # let canvas_ref: NodeRef<html::Canvas> = create_node_ref(cx);
|
||||
/// let mut non_passive_wheel = ev::Custom::<ev::WheelEvent>::new("wheel");
|
||||
/// # if false {
|
||||
/// let options = non_passive_wheel.options_mut();
|
||||
/// options.passive(false);
|
||||
/// # }
|
||||
/// canvas_ref.on_load(cx, move |canvas: HtmlElement<html::Canvas>| {
|
||||
/// canvas.on(non_passive_wheel, move |_event| {
|
||||
/// // Handle _event
|
||||
/// });
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// [`AddEventListenerOptions`]: web_sys::AddEventListenerOptions
|
||||
pub fn options_mut(&mut self) -> &mut web_sys::AddEventListenerOptions {
|
||||
self.options
|
||||
.get_or_insert_with(web_sys::AddEventListenerOptions::new)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! generate_event_types {
|
||||
|
||||
@@ -203,8 +203,10 @@ pub fn set_timeout_with_handle(
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -252,8 +254,10 @@ pub fn debounce<T: 'static>(
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |value| {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb(value);
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -319,8 +323,10 @@ pub fn set_interval(
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -349,8 +355,10 @@ pub fn set_interval_with_handle(
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -377,8 +385,10 @@ pub fn window_event_listener(
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,13 +74,13 @@ pub trait ElementDescriptor: ElementDescriptorBounds {
|
||||
/// The name of the element, i.e., `div`, `p`, `custom-element`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
|
||||
/// Determains if the tag is void, i.e., `<input>` and `<br>`.
|
||||
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
|
||||
fn is_void(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// A unique `id` that should be generated for each new instance of
|
||||
/// this element, and be consistant for both SSR and CSR.
|
||||
/// this element, and be consistent for both SSR and CSR.
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
fn hydration_id(&self) -> &HydrationKey;
|
||||
}
|
||||
@@ -573,6 +573,23 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks to see if this element is mounted to the DOM as a child
|
||||
/// of `body`.
|
||||
///
|
||||
/// This method will always return [`None`] on non-wasm CSR targets.
|
||||
pub fn is_mounted(&self) -> bool {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
crate::document()
|
||||
.body()
|
||||
.unwrap()
|
||||
.contains(Some(self.element.as_ref()))
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
false
|
||||
}
|
||||
|
||||
/// Adds an attribute to this element.
|
||||
#[track_caller]
|
||||
pub fn attr(
|
||||
@@ -679,6 +696,104 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
this
|
||||
}
|
||||
|
||||
/// Sets the class on the element as the class signal changes.
|
||||
#[track_caller]
|
||||
pub fn dyn_classes<I, C>(
|
||||
self,
|
||||
classes_signal: impl Fn() -> I + 'static,
|
||||
) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = C>,
|
||||
C: Into<Cow<'static, str>>,
|
||||
{
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
use smallvec::SmallVec;
|
||||
|
||||
let class_list = self.element.as_ref().class_list();
|
||||
|
||||
leptos_reactive::create_effect(
|
||||
self.cx,
|
||||
move |prev_classes: Option<
|
||||
SmallVec<[Cow<'static, str>; 4]>,
|
||||
>| {
|
||||
let classes = classes_signal()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
|
||||
);
|
||||
|
||||
let mut new_classes = classes
|
||||
.iter()
|
||||
.flat_map(|classes| classes.split_whitespace());
|
||||
|
||||
if let Some(prev_classes) = prev_classes {
|
||||
let mut old_classes = prev_classes
|
||||
.iter()
|
||||
.flat_map(|classes| classes.split_whitespace());
|
||||
|
||||
// Remove old classes
|
||||
for prev_class in old_classes.clone() {
|
||||
if !new_classes.any(|c| c == prev_class) {
|
||||
class_list.remove_1(prev_class).unwrap_or_else(
|
||||
|err| {
|
||||
panic!(
|
||||
"failed to add class \
|
||||
`{prev_class}`, error: {err:#?}"
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new classes
|
||||
for class in new_classes {
|
||||
if !old_classes.any(|c| c == class) {
|
||||
class_list.add_1(class).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to remove class `{class}`, \
|
||||
error: {err:#?}"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let new_classes = new_classes
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
|
||||
for class in &new_classes {
|
||||
class_list.add_1(class).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to add class `{class}`, error: \
|
||||
{err:#?}"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
classes
|
||||
},
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
classes_signal()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.flat_map(|classes| {
|
||||
classes
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect::<SmallVec<[_; 4]>>()
|
||||
})
|
||||
.fold(self, |this, class| this.class(class, true))
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a property on an element.
|
||||
#[track_caller]
|
||||
pub fn prop(
|
||||
@@ -734,12 +849,14 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
key,
|
||||
event_name,
|
||||
event_handler,
|
||||
event.options(),
|
||||
);
|
||||
} else {
|
||||
add_event_listener_undelegated(
|
||||
self.element.as_ref(),
|
||||
&event_name,
|
||||
event_handler,
|
||||
event.options(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -681,12 +681,13 @@ impl View {
|
||||
match &self {
|
||||
Self::Element(el) => {
|
||||
if event.bubbles() {
|
||||
add_event_listener(&el.element, event.event_delegation_key(), event.name(), event_handler);
|
||||
add_event_listener(&el.element, event.event_delegation_key(), event.name(), event_handler, &None);
|
||||
} else {
|
||||
add_event_listener_undelegated(
|
||||
&el.element,
|
||||
&event.name(),
|
||||
event_handler,
|
||||
&None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,15 +91,10 @@ pub(crate) fn property_helper(
|
||||
match value {
|
||||
Property::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
create_render_effect(cx, move |_| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
if old.as_ref() != Some(&new)
|
||||
&& !(old.is_none()
|
||||
&& new == wasm_bindgen::JsValue::UNDEFINED)
|
||||
{
|
||||
property_expression(&el, prop_name, new.clone())
|
||||
}
|
||||
property_expression(&el, prop_name, new.clone());
|
||||
new
|
||||
});
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
let runtime = create_runtime();
|
||||
|
||||
let (
|
||||
(shell, prefix, pending_resources, pending_fragments, serializers),
|
||||
(shell, pending_resources, pending_fragments, serializers),
|
||||
scope,
|
||||
disposer,
|
||||
) = run_scope_undisposed(runtime, {
|
||||
@@ -146,34 +146,81 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
|
||||
let resources = cx.pending_resources();
|
||||
let pending_resources = serde_json::to_string(&resources).unwrap();
|
||||
let prefix = prefix(cx);
|
||||
|
||||
(
|
||||
shell,
|
||||
prefix,
|
||||
pending_resources,
|
||||
cx.pending_fragments(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
}
|
||||
});
|
||||
let cx = Scope { runtime, id: scope };
|
||||
|
||||
let blocking_fragments = FuturesUnordered::new();
|
||||
let fragments = FuturesUnordered::new();
|
||||
for (fragment_id, (fut, _)) in pending_fragments {
|
||||
fragments.push(async move { (fragment_id, fut.await) })
|
||||
|
||||
for (fragment_id, data) in pending_fragments {
|
||||
if data.should_block {
|
||||
blocking_fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.await) });
|
||||
} else {
|
||||
fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.await) });
|
||||
}
|
||||
}
|
||||
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
// TODO can remove id_before_suspense entirely now
|
||||
let fragments = fragments.map(|(fragment_id, html)| {
|
||||
let fragments = fragments_to_chunks(fragments);
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(serializers);
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments = fragments_to_chunks(blocking_fragments);
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
{blocking}
|
||||
"#
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources)
|
||||
// dispose of the root scope
|
||||
.chain(futures::stream::once(async move {
|
||||
disposer.dispose();
|
||||
Default::default()
|
||||
}));
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
fn fragments_to_chunks(
|
||||
fragments: impl Stream<Item = (String, String)>,
|
||||
) -> impl Stream<Item = String> {
|
||||
fragments.map(|(fragment_id, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
<script>
|
||||
var id = "{fragment_id}";
|
||||
var open;
|
||||
var close;
|
||||
var open = undefined;
|
||||
var close = undefined;
|
||||
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
||||
while(walker.nextNode()) {{
|
||||
if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{
|
||||
@@ -191,35 +238,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
});
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(serializers);
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources)
|
||||
// dispose of the root scope
|
||||
.chain(futures::stream::once(async move {
|
||||
disposer.dispose();
|
||||
Default::default()
|
||||
}));
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
impl View {
|
||||
@@ -230,7 +249,9 @@ impl View {
|
||||
|
||||
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
View::Text(node) => node.content,
|
||||
View::Text(node) => {
|
||||
html_escape::encode_safe(&node.content).to_string().into()
|
||||
}
|
||||
View::Component(node) => {
|
||||
let content = || {
|
||||
node.children
|
||||
@@ -509,12 +530,14 @@ pub(crate) fn render_serializers(
|
||||
) -> impl Stream<Item = String> {
|
||||
serializers.map(|(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
let json = json.replace('<', "\\u003c");
|
||||
format!(
|
||||
r#"<script>
|
||||
var val = {json:?};
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
|
||||
}} else {{
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, val);
|
||||
}}
|
||||
</script>"#,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ use leptos_reactive::{
|
||||
create_runtime, run_scope_undisposed, suspense::StreamChunk, RuntimeId,
|
||||
Scope, ScopeId,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, collections::VecDeque};
|
||||
|
||||
/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s
|
||||
/// loaded in `<Suspense/>` elements have finished loading.
|
||||
@@ -80,29 +80,48 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
|
||||
let ((chunks, prefix, pending_resources, serializers), scope_id, disposer) =
|
||||
run_scope_undisposed(runtime, |cx| {
|
||||
// add additional context
|
||||
additional_context(cx);
|
||||
let (
|
||||
(
|
||||
blocking_fragments_ready,
|
||||
chunks,
|
||||
prefix,
|
||||
pending_resources,
|
||||
serializers,
|
||||
),
|
||||
scope_id,
|
||||
disposer,
|
||||
) = run_scope_undisposed(runtime, |cx| {
|
||||
// add additional context
|
||||
additional_context(cx);
|
||||
|
||||
// render view and return chunks
|
||||
let view = view(cx);
|
||||
// render view and return chunks
|
||||
let view = view(cx);
|
||||
|
||||
let prefix = prefix(cx);
|
||||
(
|
||||
view.into_stream_chunks(cx),
|
||||
prefix,
|
||||
serde_json::to_string(&cx.pending_resources()).unwrap(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
});
|
||||
(
|
||||
cx.blocking_fragments_ready(),
|
||||
view.into_stream_chunks(cx),
|
||||
prefix,
|
||||
serde_json::to_string(&cx.pending_resources()).unwrap(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
});
|
||||
let cx = Scope {
|
||||
runtime,
|
||||
id: scope_id,
|
||||
};
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
let (prefix_tx, prefix_rx) = futures::channel::oneshot::channel();
|
||||
leptos_reactive::spawn_local(async move {
|
||||
handle_chunks(tx, chunks).await;
|
||||
blocking_fragments_ready.await;
|
||||
let remaining_chunks = handle_blocking_chunks(tx.clone(), chunks).await;
|
||||
let prefix = prefix(cx);
|
||||
prefix_tx.send(prefix).expect("to send prefix");
|
||||
handle_chunks(tx, remaining_chunks).await;
|
||||
});
|
||||
|
||||
let stream = futures::stream::once(async move {
|
||||
let prefix = prefix_rx.await.expect("to receive prefix");
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
@@ -126,18 +145,61 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_chunks(tx: UnboundedSender<String>, chunks: Vec<StreamChunk>) {
|
||||
async fn handle_blocking_chunks(
|
||||
tx: UnboundedSender<String>,
|
||||
mut queued_chunks: VecDeque<StreamChunk>,
|
||||
) -> VecDeque<StreamChunk> {
|
||||
let mut buffer = String::new();
|
||||
while let Some(chunk) = queued_chunks.pop_front() {
|
||||
match chunk {
|
||||
StreamChunk::Sync(sync) => buffer.push_str(&sync),
|
||||
StreamChunk::Async {
|
||||
chunks,
|
||||
should_block,
|
||||
} => {
|
||||
if should_block {
|
||||
// add static HTML before the Suspense and stream it down
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send async HTML chunk");
|
||||
|
||||
// send the inner stream
|
||||
let suspended = chunks.await;
|
||||
handle_blocking_chunks(tx.clone(), suspended).await;
|
||||
} else {
|
||||
// TODO: should probably first check if there are any *other* blocking chunks
|
||||
queued_chunks.push_front(StreamChunk::Async {
|
||||
chunks,
|
||||
should_block: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send final sync chunk
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send final HTML chunk");
|
||||
|
||||
queued_chunks
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_chunks(
|
||||
tx: UnboundedSender<String>,
|
||||
chunks: VecDeque<StreamChunk>,
|
||||
) {
|
||||
let mut buffer = String::new();
|
||||
for chunk in chunks {
|
||||
match chunk {
|
||||
StreamChunk::Sync(sync) => buffer.push_str(&sync),
|
||||
StreamChunk::Async(suspended) => {
|
||||
StreamChunk::Async { chunks, .. } => {
|
||||
// add static HTML before the Suspense and stream it down
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send async HTML chunk");
|
||||
|
||||
// send the inner stream
|
||||
let suspended = suspended.await;
|
||||
let suspended = chunks.await;
|
||||
handle_chunks(tx.clone(), suspended).await;
|
||||
}
|
||||
}
|
||||
@@ -149,8 +211,8 @@ async fn handle_chunks(tx: UnboundedSender<String>, chunks: Vec<StreamChunk>) {
|
||||
|
||||
impl View {
|
||||
/// Renders the view into a set of HTML chunks that can be streamed.
|
||||
pub fn into_stream_chunks(self, cx: Scope) -> Vec<StreamChunk> {
|
||||
let mut chunks = Vec::new();
|
||||
pub fn into_stream_chunks(self, cx: Scope) -> VecDeque<StreamChunk> {
|
||||
let mut chunks = VecDeque::new();
|
||||
self.into_stream_chunks_helper(cx, &mut chunks);
|
||||
chunks
|
||||
}
|
||||
@@ -158,37 +220,42 @@ impl View {
|
||||
fn into_stream_chunks_helper(
|
||||
self,
|
||||
cx: Scope,
|
||||
chunks: &mut Vec<StreamChunk>,
|
||||
chunks: &mut VecDeque<StreamChunk>,
|
||||
) {
|
||||
match self {
|
||||
View::Suspense(id, _) => {
|
||||
let id = id.to_string();
|
||||
if let Some((_, fragment)) = cx.take_pending_fragment(&id) {
|
||||
chunks.push(StreamChunk::Async(fragment));
|
||||
if let Some(data) = cx.take_pending_fragment(&id) {
|
||||
chunks.push_back(StreamChunk::Async {
|
||||
chunks: data.in_order,
|
||||
should_block: data.should_block,
|
||||
});
|
||||
}
|
||||
}
|
||||
View::Text(node) => chunks.push(StreamChunk::Sync(node.content)),
|
||||
View::Text(node) => {
|
||||
chunks.push_back(StreamChunk::Sync(node.content))
|
||||
}
|
||||
View::Component(node) => {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let name = crate::ssr::to_kebab_case(&node.name);
|
||||
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-start-->"#, HydrationCtx::to_string(&node.id, false)).into()));
|
||||
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-start-->"#, HydrationCtx::to_string(&node.id, false)).into()));
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
}
|
||||
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-end-->"#, HydrationCtx::to_string(&node.id, true)).into()));
|
||||
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-end-->"#, HydrationCtx::to_string(&node.id, true)).into()));
|
||||
} else {
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
}
|
||||
chunks.push(StreamChunk::Sync(format!(r#"<!--hk={}-->"#, HydrationCtx::to_string(&node.id, true)).into()))
|
||||
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}-->"#, HydrationCtx::to_string(&node.id, true)).into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(id) = &el.view_marker {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<!--leptos-view|{id}|open-->").into(),
|
||||
));
|
||||
}
|
||||
@@ -196,7 +263,7 @@ impl View {
|
||||
for chunk in el_chunks {
|
||||
match chunk {
|
||||
StringOrView::String(string) => {
|
||||
chunks.push(StreamChunk::Sync(string))
|
||||
chunks.push_back(StreamChunk::Sync(string))
|
||||
}
|
||||
StringOrView::View(view) => {
|
||||
view().into_stream_chunks_helper(cx, chunks);
|
||||
@@ -232,18 +299,18 @@ impl View {
|
||||
.join("");
|
||||
|
||||
if el.is_void {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<{tag_name}{attrs}/>").into(),
|
||||
));
|
||||
} else if let Some(inner_html) = inner_html {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!(
|
||||
"<{tag_name}{attrs}>{inner_html}</{tag_name}>"
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
} else {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<{tag_name}{attrs}>").into(),
|
||||
));
|
||||
|
||||
@@ -255,20 +322,20 @@ impl View {
|
||||
}
|
||||
}
|
||||
ElementChildren::InnerHtml(inner_html) => {
|
||||
chunks.push(StreamChunk::Sync(inner_html));
|
||||
chunks.push_back(StreamChunk::Sync(inner_html));
|
||||
}
|
||||
// handled above
|
||||
ElementChildren::Chunks(_) => unreachable!(),
|
||||
}
|
||||
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("</{tag_name}>").into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(id) = &el.view_marker {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<!--leptos-view|{id}|close-->").into(),
|
||||
));
|
||||
}
|
||||
@@ -280,10 +347,10 @@ impl View {
|
||||
u.id.clone(),
|
||||
"",
|
||||
false,
|
||||
Box::new(move |chunks: &mut Vec<StreamChunk>| {
|
||||
Box::new(move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-unit-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
@@ -293,7 +360,7 @@ impl View {
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
chunks.push(StreamChunk::Sync(
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
@@ -301,7 +368,7 @@ impl View {
|
||||
.into(),
|
||||
));
|
||||
})
|
||||
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
@@ -309,34 +376,39 @@ impl View {
|
||||
node.id,
|
||||
"dyn-child",
|
||||
true,
|
||||
Box::new(move |chunks: &mut Vec<StreamChunk>| {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
chunks.push(
|
||||
if !cfg!(debug_assertions) {
|
||||
StreamChunk::Sync(
|
||||
format!("<!>{}", t.content)
|
||||
Box::new(
|
||||
move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
chunks.push_back(
|
||||
if !cfg!(debug_assertions) {
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!>{}",
|
||||
html_escape::encode_safe(&t.content)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(t.content)
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
);
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(html_escape::encode_safe(&t.content).to_string().into())
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
|
||||
},
|
||||
)
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
@@ -345,33 +417,40 @@ impl View {
|
||||
node.id,
|
||||
"each",
|
||||
true,
|
||||
Box::new(move |chunks: &mut Vec<StreamChunk>| {
|
||||
for node in children.into_iter().flatten() {
|
||||
let id = node.id;
|
||||
Box::new(
|
||||
move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
for node in children.into_iter().flatten() {
|
||||
let id = node.id;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!(
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-start-->",
|
||||
HydrationCtx::to_string(&id, false)
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
node.child.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
);
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!(
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
node.child
|
||||
.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
);
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-end-->",
|
||||
HydrationCtx::to_string(&id, true)
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce(&mut Vec<StreamChunk>)>,
|
||||
},
|
||||
)
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -379,13 +458,13 @@ impl View {
|
||||
if wrap {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
chunks.push(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-start-->", HydrationCtx::to_string(&id, false)).into()));
|
||||
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-start-->", HydrationCtx::to_string(&id, false)).into()));
|
||||
content(chunks);
|
||||
chunks.push(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-end-->", HydrationCtx::to_string(&id, true)).into()));
|
||||
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-end-->", HydrationCtx::to_string(&id, true)).into()));
|
||||
} else {
|
||||
let _ = name;
|
||||
content(chunks);
|
||||
chunks.push(StreamChunk::Sync(format!("<!--hk={}-->", HydrationCtx::to_string(&id, true)).into()))
|
||||
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}-->", HydrationCtx::to_string(&id, true)).into()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -43,7 +43,7 @@ impl Parse for Model {
|
||||
"this method requires a `Scope` parameter";
|
||||
help = "try `fn {}(cx: Scope, /* ... */)`", item.sig.ident
|
||||
);
|
||||
} else if props[0].ty != parse_quote!(Scope) {
|
||||
} else if !is_valid_scope_type(&props[0].ty) {
|
||||
abort!(
|
||||
item.sig.inputs,
|
||||
"this method requires a `Scope` parameter";
|
||||
@@ -68,7 +68,7 @@ impl Parse for Model {
|
||||
});
|
||||
|
||||
// Make sure return type is correct
|
||||
if item.sig.output != parse_quote!(-> impl IntoView) {
|
||||
if !is_valid_into_view_return_type(&item.sig.output) {
|
||||
abort!(
|
||||
item.sig,
|
||||
"return type is incorrect";
|
||||
@@ -130,6 +130,7 @@ impl ToTokens for Model {
|
||||
let mut body = body.to_owned();
|
||||
|
||||
body.sig.ident = format_ident!("__{}", body.sig.ident);
|
||||
#[allow(clippy::redundant_clone)] // false positive
|
||||
let body_name = body.sig.ident.clone();
|
||||
|
||||
let (_, generics, where_clause) = body.sig.generics.split_for_impl();
|
||||
@@ -153,6 +154,7 @@ impl ToTokens for Model {
|
||||
if cfg!(feature = "tracing") {
|
||||
(
|
||||
quote! {
|
||||
#[allow(clippy::let_with_type_underscore)]
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
|
||||
@@ -204,7 +206,7 @@ impl ToTokens for Model {
|
||||
#tracing_instrument_attr
|
||||
#vis fn #name #generics (
|
||||
#[allow(unused_variables)]
|
||||
#scope_name: Scope,
|
||||
#scope_name: ::leptos::Scope,
|
||||
props: #props_name #generics
|
||||
) #ret #(+ #lifetimes)*
|
||||
#where_clause
|
||||
@@ -434,7 +436,7 @@ impl ToTokens for TypedBuilderOpts {
|
||||
fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
props
|
||||
.iter()
|
||||
.filter(|Prop { ty, .. }| *ty != parse_quote!(Scope))
|
||||
.filter(|Prop { ty, .. }| !is_valid_scope_type(ty))
|
||||
.map(|prop| {
|
||||
let Prop {
|
||||
docs,
|
||||
@@ -461,7 +463,7 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
fn prop_names(props: &[Prop]) -> TokenStream {
|
||||
props
|
||||
.iter()
|
||||
.filter(|Prop { ty, .. }| *ty != parse_quote!(Scope))
|
||||
.filter(|Prop { ty, .. }| !is_valid_scope_type(ty))
|
||||
.map(|Prop { name, .. }| quote! { #name, })
|
||||
.collect()
|
||||
}
|
||||
@@ -640,3 +642,23 @@ fn prop_to_doc(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_scope_type(ty: &Type) -> bool {
|
||||
[
|
||||
parse_quote!(Scope),
|
||||
parse_quote!(leptos::Scope),
|
||||
parse_quote!(::leptos::Scope),
|
||||
]
|
||||
.iter()
|
||||
.any(|test| ty == test)
|
||||
}
|
||||
|
||||
fn is_valid_into_view_return_type(ty: &ReturnType) -> bool {
|
||||
[
|
||||
parse_quote!(-> impl IntoView),
|
||||
parse_quote!(-> impl leptos::IntoView),
|
||||
parse_quote!(-> impl ::leptos::IntoView),
|
||||
]
|
||||
.iter()
|
||||
.any(|test| ty == test)
|
||||
}
|
||||
|
||||
@@ -201,13 +201,15 @@ mod template;
|
||||
/// ```
|
||||
///
|
||||
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
|
||||
/// [NodeRef](leptos_dom::NodeRef) to use later.
|
||||
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// use leptos::html::Input;
|
||||
///
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
/// let my_input = NodeRef::new(cx);
|
||||
/// let my_input = create_node_ref::<Input>(cx);
|
||||
/// view! { cx, <input type="text" _ref=my_input/> }
|
||||
/// // `my_input` now contains an `Element` that we can use anywhere
|
||||
/// # ;
|
||||
@@ -399,9 +401,9 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// The `#[component]` macro allows you to annotate plain Rust functions as components
|
||||
/// and use them within your Leptos [view](crate::view!) as if they were custom HTML elements. The
|
||||
/// component function takes a [Scope](leptos_reactive::Scope) and any number of other arguments.
|
||||
/// When you use the component somewhere else, the names of its arguments are the names
|
||||
/// of the properties you use in the [view](crate::view!) macro.
|
||||
/// component function takes a [Scope](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||
/// and any number of other arguments. When you use the component somewhere else,
|
||||
/// the names of its arguments are the names of the properties you use in the [view](crate::view!) macro.
|
||||
///
|
||||
/// Every component function should have the return type `-> impl IntoView`.
|
||||
///
|
||||
@@ -576,8 +578,10 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
/// You can use the `#[prop]` attribute on individual component properties (function arguments) to
|
||||
/// customize the types that component property can receive. You can use the following attributes:
|
||||
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
|
||||
/// you could apply `#[prop(into)]` to a prop that takes [Signal](leptos_reactive::Signal), which would
|
||||
/// allow users to pass a [ReadSignal](leptos_reactive::ReadSignal) or [RwSignal](leptos_reactive::RwSignal)
|
||||
/// you could apply `#[prop(into)]` to a prop that takes
|
||||
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
|
||||
/// allow users to pass a [ReadSignal](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) or
|
||||
/// [RwSignal](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html)
|
||||
/// and automatically convert it.)
|
||||
/// * `#[prop(optional)]`: If the user does not specify this property when they use the component,
|
||||
/// it will be set to its default value. If the property type is `Option<T>`, values should be passed
|
||||
@@ -640,8 +644,8 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Declares that a function is a [server function](leptos_server). This means that
|
||||
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
/// Declares that a function is a [server function](https://docs.rs/server_fn/latest/server_fn/index.html).
|
||||
/// This means that its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
///
|
||||
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
|
||||
/// are enabled), it will instead make a network request to the server.
|
||||
@@ -657,7 +661,8 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// work without WebAssembly, the encoding must be `"Url"`.
|
||||
///
|
||||
/// The server function itself can take any number of arguments, each of which should be serializable
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope),
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos
|
||||
/// [Scope](https://docs.rs/leptos/latest/leptos/struct.Scope.html),
|
||||
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
|
||||
/// server-side context into the server function.
|
||||
///
|
||||
@@ -680,7 +685,7 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must be [Serializable](leptos_reactive::Serializable).**
|
||||
/// - **Return types must be [Serializable](https://docs.rs/leptos/latest/leptos/trait.Serializable.html).**
|
||||
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
/// need to deserialize the result to return it to the client.
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
@@ -688,10 +693,11 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
/// - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
|
||||
/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a Leptos `Scope`. This scope can be used to inject dependencies like the HTTP request
|
||||
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
#[proc_macro_attribute]
|
||||
#[proc_macro_error]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let context = ServerContext {
|
||||
ty: syn::parse_quote!(Scope),
|
||||
|
||||
@@ -365,6 +365,7 @@ enum SsrElementChunks {
|
||||
View(TokenStream),
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn element_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
@@ -817,7 +818,22 @@ fn element_to_tokens(
|
||||
};
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
Some(attribute_to_tokens(cx, node))
|
||||
if node.key.to_string().trim().starts_with("class:") {
|
||||
None
|
||||
} else {
|
||||
Some(attribute_to_tokens(cx, node))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let class_attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
if node.key.to_string().trim().starts_with("class:") {
|
||||
Some(attribute_to_tokens(cx, node))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -875,6 +891,7 @@ fn element_to_tokens(
|
||||
quote! {
|
||||
#name
|
||||
#(#attrs)*
|
||||
#(#class_attrs)*
|
||||
#global_class_expr
|
||||
#(#children)*
|
||||
#view_marker
|
||||
@@ -1375,7 +1392,7 @@ fn is_math_ml_element(tag: &str) -> bool {
|
||||
}
|
||||
|
||||
fn is_ambiguous_element(tag: &str) -> bool {
|
||||
tag == "a" || tag == "script"
|
||||
tag == "a" || tag == "script" || tag == "title"
|
||||
}
|
||||
|
||||
fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
|
||||
@@ -20,7 +20,7 @@ fn Component(
|
||||
#[test]
|
||||
fn component() {
|
||||
let cp = ComponentProps::builder().into("").strip_option(9).build();
|
||||
assert_eq!(cp.optional, false);
|
||||
assert!(!cp.optional);
|
||||
assert_eq!(cp.optional_no_strip, None);
|
||||
assert_eq!(cp.strip_option, Some(9));
|
||||
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/ui/*.rs");
|
||||
t.compile_fail("tests/ui/component.rs");
|
||||
t.compile_fail("tests/ui/component_absolute.rs");
|
||||
}
|
||||
|
||||
52
leptos_macro/tests/ui/component_absolute.rs
Normal file
52
leptos_macro/tests/ui/component_absolute.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
#[::leptos::component]
|
||||
fn missing_scope() {}
|
||||
|
||||
#[::leptos::component]
|
||||
fn missing_return_type(cx: ::leptos::Scope) {}
|
||||
|
||||
#[::leptos::component]
|
||||
fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
|
||||
|
||||
#[::leptos::component]
|
||||
fn optional_and_optional_no_strip(
|
||||
cx: Scope,
|
||||
#[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn optional_and_strip_option(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(optional, strip_option)] conflicting: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn optional_no_strip_and_strip_option(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn default_without_value(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(default)] default: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
fn default_with_invalid_value(
|
||||
cx: ::leptos::Scope,
|
||||
#[prop(default= |)] default: bool,
|
||||
) -> impl ::leptos::IntoView {
|
||||
}
|
||||
|
||||
#[::leptos::component]
|
||||
pub fn using_the_view_macro(cx: ::leptos::Scope) -> impl ::leptos::IntoView {
|
||||
::leptos::view! { cx,
|
||||
"ok"
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
53
leptos_macro/tests/ui/component_absolute.stderr
Normal file
53
leptos_macro/tests/ui/component_absolute.stderr
Normal file
@@ -0,0 +1,53 @@
|
||||
error: this method requires a `Scope` parameter
|
||||
--> tests/ui/component_absolute.rs:2:1
|
||||
|
|
||||
2 | fn missing_scope() {}
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: try `fn missing_scope(cx: Scope, /* ... */)`
|
||||
|
||||
error: return type is incorrect
|
||||
--> tests/ui/component_absolute.rs:5:1
|
||||
|
|
||||
5 | fn missing_return_type(cx: ::leptos::Scope) {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: return signature must be `-> impl IntoView`
|
||||
|
||||
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default` and `into`
|
||||
--> tests/ui/component_absolute.rs:8:52
|
||||
|
|
||||
8 | fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
|
||||
| ^^^^^
|
||||
|
||||
error: `optional` conflicts with mutually exclusive `optional_no_strip`
|
||||
--> tests/ui/component_absolute.rs:13:12
|
||||
|
|
||||
13 | #[prop(optional, optional_no_strip)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: `optional` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component_absolute.rs:20:12
|
||||
|
|
||||
20 | #[prop(optional, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
|
||||
--> tests/ui/component_absolute.rs:27:12
|
||||
|
|
||||
27 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: unexpected end of input, expected assignment `=`
|
||||
--> tests/ui/component_absolute.rs:34:19
|
||||
|
|
||||
34 | #[prop(default)] default: bool,
|
||||
| ^
|
||||
|
||||
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
|
||||
|
||||
= help: try `#[prop(default=5 * 10)]`
|
||||
--> tests/ui/component_absolute.rs:41:22
|
||||
|
|
||||
41 | #[prop(default= |)] default: bool,
|
||||
| ^
|
||||
@@ -10,9 +10,9 @@ description = "Reactive system for the Leptos web framework."
|
||||
[dependencies]
|
||||
slotmap = { version = "1", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde-lite = { version = "0.3", optional = true }
|
||||
serde-lite = { version = "0.4", optional = true }
|
||||
futures = { version = "0.3" }
|
||||
js-sys = "0.3"
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
miniserde = { version = "0.1", optional = true }
|
||||
rkyv = { version = "0.7.39", features = [
|
||||
"validation",
|
||||
@@ -31,17 +31,18 @@ base64 = "0.21"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["rt"], optional = true }
|
||||
tracing = "0.1"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = [
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4", optional = true }
|
||||
web-sys = { version = "0.3", optional = true, features = [
|
||||
"DocumentFragment",
|
||||
"Element",
|
||||
"HtmlTemplateElement",
|
||||
"NodeList",
|
||||
"Window",
|
||||
] }
|
||||
cfg-if = "1.0.0"
|
||||
indexmap = "1.9.2"
|
||||
cfg-if = "1"
|
||||
indexmap = "1"
|
||||
ouroboros = { version = "0.15.6", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4"
|
||||
@@ -50,8 +51,18 @@ leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
csr = []
|
||||
hydrate = []
|
||||
csr = [
|
||||
"dep:js-sys",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:wasm-bindgen-futures",
|
||||
"dep:web-sys",
|
||||
]
|
||||
hydrate = [
|
||||
"dep:js-sys",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:wasm-bindgen-futures",
|
||||
"dep:web-sys",
|
||||
]
|
||||
ssr = ["dep:tokio"]
|
||||
stable = []
|
||||
serde = []
|
||||
|
||||
77
leptos_reactive/src/diagnostics.rs
Normal file
77
leptos_reactive/src/diagnostics.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
// The point of these diagnostics is to give useful error messages when someone
|
||||
// tries to access a reactive variable outside the reactive scope. They track when
|
||||
// you create a signal/memo, and where you access it non-reactively.
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[allow(dead_code)] // allowed for SSR
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct AccessDiagnostics {
|
||||
pub defined_at: &'static std::panic::Location<'static>,
|
||||
pub called_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub(crate) struct AccessDiagnostics {}
|
||||
|
||||
/// This just tracks whether we're currently in a context in which it really doesn't
|
||||
/// matter whether something is reactive: for example, in an event listener or timeout.
|
||||
/// Entering this zone basically turns off the warnings, and exiting it turns them back on.
|
||||
/// All of this is a no-op in release mode.
|
||||
#[doc(hidden)]
|
||||
pub struct SpecialNonReactiveZone {}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
use std::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
static IS_SPECIAL_ZONE: Cell<bool> = Cell::new(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecialNonReactiveZone {
|
||||
#[allow(dead_code)] // allowed for SSR
|
||||
pub(crate) fn is_inside() -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
IS_SPECIAL_ZONE.with(|val| val.get())
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
false
|
||||
}
|
||||
|
||||
pub fn enter() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
IS_SPECIAL_ZONE.with(|val| val.set(true))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exit() {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
IS_SPECIAL_ZONE.with(|val| val.set(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! diagnostics {
|
||||
($this:ident) => {{
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
AccessDiagnostics {
|
||||
defined_at: $this.defined_at,
|
||||
called_at: std::panic::Location::caller()
|
||||
}
|
||||
} else {
|
||||
AccessDiagnostics { }
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
@@ -1,20 +1,26 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{runtime::PinnedFuture, suspense::StreamChunk, ResourceId};
|
||||
use cfg_if::cfg_if;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
pub struct SharedContext {
|
||||
pub events: Vec<()>,
|
||||
pub pending_resources: HashSet<ResourceId>,
|
||||
pub resolved_resources: HashMap<ResourceId, String>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
// index String is the fragment ID: tuple is
|
||||
// `(
|
||||
// Future of <Suspense/> HTML when resolved (out-of-order)
|
||||
// Future of additional stream chunks when resolved (in-order)
|
||||
// )`
|
||||
pub pending_fragments:
|
||||
HashMap<String, (PinnedFuture<String>, PinnedFuture<Vec<StreamChunk>>)>,
|
||||
pub pending_fragments: HashMap<String, FragmentData>,
|
||||
}
|
||||
|
||||
/// Represents its pending `<Suspense/>` fragment.
|
||||
pub struct FragmentData {
|
||||
/// Future that represents how it should be render for an out-of-order stream.
|
||||
pub out_of_order: PinnedFuture<String>,
|
||||
/// Future that represents how it should be render for an in-order stream.
|
||||
pub in_order: PinnedFuture<VecDeque<StreamChunk>>,
|
||||
/// Whether the stream should wait for this fragment before sending any data.
|
||||
pub should_block: bool,
|
||||
/// Future that will resolve when the fragment is ready.
|
||||
pub is_ready: Option<PinnedFuture<()>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SharedContext {
|
||||
|
||||
@@ -72,6 +72,8 @@ extern crate tracing;
|
||||
#[macro_use]
|
||||
mod signal;
|
||||
mod context;
|
||||
#[macro_use]
|
||||
mod diagnostics;
|
||||
mod effect;
|
||||
mod hydration;
|
||||
mod memo;
|
||||
@@ -90,6 +92,7 @@ mod stored_value;
|
||||
pub mod suspense;
|
||||
|
||||
pub use context::*;
|
||||
pub use diagnostics::SpecialNonReactiveZone;
|
||||
pub use effect::*;
|
||||
pub use memo::*;
|
||||
pub use resource::*;
|
||||
@@ -127,8 +130,11 @@ mod macros {
|
||||
}
|
||||
|
||||
pub(crate) fn console_warn(s: &str) {
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
eprintln!("{s}");
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(s));
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
|
||||
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(s));
|
||||
} else {
|
||||
eprintln!("{s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_effect, node::NodeId, on_cleanup, with_runtime, AnyComputation,
|
||||
RuntimeId, Scope, SignalDispose, SignalGet, SignalGetUntracked,
|
||||
SignalStream, SignalWith, SignalWithUntracked,
|
||||
create_effect, diagnostics::AccessDiagnostics, node::NodeId, on_cleanup,
|
||||
with_runtime, AnyComputation, RuntimeId, Scope, SignalDispose, SignalGet,
|
||||
SignalGetUntracked, SignalStream, SignalWith, SignalWithUntracked,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
|
||||
|
||||
/// Creates an efficient derived reactive value based on other reactive values.
|
||||
@@ -70,6 +71,7 @@ use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_memo<T>(
|
||||
cx: Scope,
|
||||
f: impl Fn(Option<&T>) -> T + 'static,
|
||||
@@ -191,7 +193,8 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
)]
|
||||
fn get_untracked(&self) -> T {
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
match self.id.try_with_no_subscription(runtime, T::clone) {
|
||||
let f = move |maybe_value: &Option<T>| maybe_value.clone().unwrap();
|
||||
match self.id.try_with_no_subscription(runtime, f) {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -305,6 +308,7 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
fn get(&self) -> T {
|
||||
self.with(T::clone)
|
||||
}
|
||||
@@ -322,6 +326,7 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
fn try_get(&self) -> Option<T> {
|
||||
self.try_with(T::clone)
|
||||
}
|
||||
@@ -341,6 +346,7 @@ impl<T> SignalWith<T> for Memo<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
match self.try_with(f) {
|
||||
Some(t) => t,
|
||||
@@ -364,13 +370,16 @@ impl<T> SignalWith<T> for Memo<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
// memo is stored as Option<T>, but will always have T available
|
||||
// after latest_value() called, so we can unwrap safely
|
||||
let f = move |maybe_value: &Option<T>| f(maybe_value.as_ref().unwrap());
|
||||
|
||||
let diagnostics = diagnostics!(self);
|
||||
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
self.id.subscribe(runtime);
|
||||
self.id.subscribe(runtime, diagnostics);
|
||||
self.id.try_with_no_subscription(runtime, f).ok()
|
||||
})
|
||||
.ok()
|
||||
|
||||
@@ -25,4 +25,7 @@ pub(crate) enum ReactiveNodeState {
|
||||
Clean,
|
||||
Check,
|
||||
Dirty,
|
||||
|
||||
/// Dirty and Marked as visited
|
||||
DirtyMarked,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
@@ -105,6 +106,73 @@ pub fn create_resource_with_initial_value<S, T, Fu>(
|
||||
fetcher: impl Fn(S) -> Fu + 'static,
|
||||
initial_value: Option<T>,
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
create_resource_helper(
|
||||
cx,
|
||||
source,
|
||||
fetcher,
|
||||
initial_value,
|
||||
ResourceSerialization::Serializable,
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a “blocking” [Resource](crate::Resource). When server-side rendering is used,
|
||||
/// this resource will cause any `<Suspense/>` you read it under to block the initial
|
||||
/// chunk of HTML from being sent to the client. This means that if you set things like
|
||||
/// HTTP headers or `<head>` metadata in that `<Suspense/>`, that header material will
|
||||
/// be included in the server’s original response.
|
||||
///
|
||||
/// This causes a slow time to first byte (TTFB) but is very useful for loading data that
|
||||
/// is essential to the first load. For example, a blog post page that needs to include
|
||||
/// the title of the blog post in the page’s initial HTML `<title>` tag for SEO reasons
|
||||
/// might use a blocking resource to load blog post metadata, which will prevent the page from
|
||||
/// returning until that data has loaded.
|
||||
///
|
||||
/// **Note**: This is not “blocking” in the sense that it blocks the current thread. Rather,
|
||||
/// it is blocking in the sense that it blocks the server from sending a response.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>(),
|
||||
signal_ty = %std::any::type_name::<S>(),
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_blocking_resource<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: impl Fn() -> S + 'static,
|
||||
fetcher: impl Fn(S) -> Fu + 'static,
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
create_resource_helper(
|
||||
cx,
|
||||
source,
|
||||
fetcher,
|
||||
None,
|
||||
ResourceSerialization::Blocking,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_resource_helper<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: impl Fn() -> S + 'static,
|
||||
fetcher: impl Fn(S) -> Fu + 'static,
|
||||
initial_value: Option<T>,
|
||||
serializable: ResourceSerialization,
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
@@ -131,7 +199,7 @@ where
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable: true,
|
||||
serializable,
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
@@ -255,7 +323,7 @@ where
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable: false,
|
||||
serializable: ResourceSerialization::Local,
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
@@ -377,13 +445,15 @@ where
|
||||
///
|
||||
/// If you want to get the value without cloning it, use [Resource::with].
|
||||
/// (`value.read(cx)` is equivalent to `value.with(cx, T::clone)`.)
|
||||
#[track_caller]
|
||||
pub fn read(&self, cx: Scope) -> Option<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.read(cx)
|
||||
resource.read(cx, location)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
@@ -397,10 +467,12 @@ where
|
||||
///
|
||||
/// If you want to get the value by cloning it, you can use
|
||||
/// [Resource::read].
|
||||
#[track_caller]
|
||||
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
let location = std::panic::Location::caller();
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.with(cx, f)
|
||||
resource.with(cx, f, location)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
@@ -555,7 +627,19 @@ where
|
||||
resolved: Rc<Cell<bool>>,
|
||||
scheduled: Rc<Cell<bool>>,
|
||||
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
|
||||
serializable: bool,
|
||||
serializable: ResourceSerialization,
|
||||
}
|
||||
|
||||
/// Whether and how the resource can be serialized.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum ResourceSerialization {
|
||||
/// Not serializable.
|
||||
Local,
|
||||
/// Can be serialized.
|
||||
Serializable,
|
||||
/// Can be serialized, and cause the first chunk to be blocked until
|
||||
/// their suspense has resolved.
|
||||
Blocking,
|
||||
}
|
||||
|
||||
impl<S, T> ResourceState<S, T>
|
||||
@@ -563,14 +647,25 @@ where
|
||||
S: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
pub fn read(&self, cx: Scope) -> Option<T>
|
||||
#[track_caller]
|
||||
pub fn read(
|
||||
&self,
|
||||
cx: Scope,
|
||||
location: &'static Location<'static>,
|
||||
) -> Option<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.with(cx, T::clone)
|
||||
self.with(cx, T::clone, location)
|
||||
}
|
||||
|
||||
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
#[track_caller]
|
||||
pub fn with<U>(
|
||||
&self,
|
||||
cx: Scope,
|
||||
f: impl FnOnce(&T) -> U,
|
||||
location: &'static Location<'static>,
|
||||
) -> Option<U> {
|
||||
let suspense_cx = use_context::<SuspenseContext>(cx);
|
||||
|
||||
let v = self
|
||||
@@ -584,9 +679,26 @@ where
|
||||
|
||||
let serializable = self.serializable;
|
||||
if let Some(suspense_cx) = &suspense_cx {
|
||||
if serializable {
|
||||
if serializable != ResourceSerialization::Local {
|
||||
suspense_cx.has_local_only.set_value(false);
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(all(feature = "hydrate", debug_assertions)))]
|
||||
{
|
||||
_ = location;
|
||||
}
|
||||
#[cfg(all(feature = "hydrate", debug_assertions))]
|
||||
crate::macros::debug_warn!(
|
||||
"At {location}, you are reading a resource in `hydrate` mode \
|
||||
outside a <Suspense/> or <Transition/>. This can cause \
|
||||
hydration mismatch errors and loses out on a significant \
|
||||
performance optimization. To fix this issue, you can either: \
|
||||
\n1. Wrap the place where you read the resource in a \
|
||||
<Suspense/> or <Transition/> component, or \n2. Switch to \
|
||||
using create_local_resource(), which will wait to load the \
|
||||
resource until the app is hydrated on the client side. (This \
|
||||
will have worse performance in most cases.)",
|
||||
);
|
||||
}
|
||||
|
||||
let increment = move |_: Option<()>| {
|
||||
@@ -600,7 +712,12 @@ where
|
||||
// because the context has been tracked here
|
||||
// on the first read, resource is already loading without having incremented
|
||||
if !has_value {
|
||||
s.increment(serializable);
|
||||
s.increment(
|
||||
serializable != ResourceSerialization::Local,
|
||||
);
|
||||
if serializable == ResourceSerialization::Blocking {
|
||||
s.should_block.set_value(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -641,7 +758,12 @@ where
|
||||
let suspense_contexts = self.suspense_contexts.clone();
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
suspense_context.increment(self.serializable);
|
||||
suspense_context.increment(
|
||||
self.serializable != ResourceSerialization::Local,
|
||||
);
|
||||
if self.serializable == ResourceSerialization::Blocking {
|
||||
suspense_context.should_block.set_value(true);
|
||||
}
|
||||
}
|
||||
|
||||
// run the Future
|
||||
@@ -659,7 +781,9 @@ where
|
||||
set_loading.update(|n| *n = false);
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
suspense_context.decrement(serializable);
|
||||
suspense_context.decrement(
|
||||
serializable != ResourceSerialization::Local,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user