mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
39 Commits
server-fn-
...
error
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7344ca50 | ||
|
|
921bb616b4 | ||
|
|
5bb7122b93 | ||
|
|
ec2aa3e7a4 | ||
|
|
43959a96c7 | ||
|
|
7925fc4245 | ||
|
|
2bd1ad0f11 | ||
|
|
dd730aa4ac | ||
|
|
c55067ab7c | ||
|
|
9da4084561 | ||
|
|
1d7235d4ca | ||
|
|
2cb8171105 | ||
|
|
bbc7799b7c | ||
|
|
a9cbcce8b2 | ||
|
|
3531ca64bb | ||
|
|
e402b85dd6 | ||
|
|
8ae5cf0ccf | ||
|
|
5c34c3fc77 | ||
|
|
3a570dc0d9 | ||
|
|
3c6748b30d | ||
|
|
24945f67bf | ||
|
|
edddab1e51 | ||
|
|
acfc86d2a4 | ||
|
|
651868dec9 | ||
|
|
18bc03e660 | ||
|
|
5f0013e482 | ||
|
|
10c0a2de65 | ||
|
|
b24be2566d | ||
|
|
77439b5db5 | ||
|
|
23594a43ea | ||
|
|
601db7aa86 | ||
|
|
d15ba11104 | ||
|
|
d45d92433f | ||
|
|
97127a90c6 | ||
|
|
55bb63edea | ||
|
|
15a4e54435 | ||
|
|
3a522aef5d | ||
|
|
a98885a123 | ||
|
|
2b7923261b |
72
.github/workflows/check-examples.yml
vendored
72
.github/workflows/check-examples.yml
vendored
@@ -1,46 +1,46 @@
|
||||
name: Check examples
|
||||
name: Check Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
setup:
|
||||
name: Get Examples
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
- name: Install JQ Tool
|
||||
uses: mbround18/install-jq@v1
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v examples/README.md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
run: cargo make check-examples
|
||||
matrix-job:
|
||||
name: Check
|
||||
needs: [setup]
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-example-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
name: Verify Examples
|
||||
name: Run Example Task
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
workflow_call:
|
||||
inputs:
|
||||
directory:
|
||||
required: true
|
||||
type: string
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Verify examples ${{ matrix.os }} (using rustc ${{ matrix.rust }}
|
||||
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -21,6 +25,12 @@ jobs:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Setup environment
|
||||
- name: Install playwright browser dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
@@ -63,7 +73,6 @@ jobs:
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -75,5 +84,12 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Run verify-flow on all web examples
|
||||
run: cargo make --profile=github-actions verify-examples
|
||||
# Verify project
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
run: |
|
||||
if [ "${{ inputs.directory }}" = "INTERNAL" ]; then
|
||||
echo No verification required
|
||||
else
|
||||
cd ${{ inputs.directory }}
|
||||
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
|
||||
fi
|
||||
47
.github/workflows/verify-all-examples.yml
vendored
Normal file
47
.github/workflows/verify-all-examples.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Verify All Examples
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
# Run once a day at 3:00 AM EST
|
||||
- cron: "0 8 * * *"
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Get Examples
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install JQ Tool
|
||||
uses: mbround18/install-jq@v1
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v examples/README.md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
matrix-job:
|
||||
name: Verify
|
||||
needs: [setup]
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-example-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "verify-flow"
|
||||
71
.github/workflows/verify-changed-examples.yml
vendored
Normal file
71
.github/workflows/verify-changed-examples.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Verify Changed Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Get Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get all example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
files: |
|
||||
examples
|
||||
|
||||
- name: List all example files that changed
|
||||
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
files: |
|
||||
examples
|
||||
!examples/cargo-make
|
||||
!examples/gtk
|
||||
!examples/Makefile.toml
|
||||
!examples/README.md
|
||||
json: true
|
||||
quotepath: false
|
||||
|
||||
- name: List example project directories that changed
|
||||
run: echo '${{ steps.changed-dirs.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
if [ ${{ steps.changed-files.outputs.any_changed }} == 'true' ]; then
|
||||
# Create matrix with changed directories
|
||||
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create matrix with one item to prevent an empty vector error
|
||||
echo "matrix={\"directory\":[\"INTERNAL\"]}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
matrix-job:
|
||||
name: Verify
|
||||
needs: [setup]
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-example-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "verify-flow"
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
_This Code of Conduct is based on the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct)
|
||||
and the [Bevy Code of Conduct](https://raw.githubusercontent.com/bevyengine/bevy/main/CODE_OF_CONDUCT.md),
|
||||
which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
|
||||
which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
|
||||
and the [Contributor Covenant](https://www.contributor-covenant.org)._
|
||||
|
||||
## Our Pledge
|
||||
|
||||
@@ -22,7 +22,7 @@ Leptos, as a framework, reflects certain technical values:
|
||||
- **Expose primitives rather than imposing patterns.** Provide building blocks
|
||||
that users can combine together to build up more complex behavior, rather than
|
||||
requiring users follow certain templates, file formats, etc. e.g., components
|
||||
are defined as functions, rather than a bespoke single-file comonent format.
|
||||
are defined as functions, rather than a bespoke single-file component format.
|
||||
The reactive system feeds into the rendering system, rather than being defined
|
||||
by it.
|
||||
- **Bottom-up over top-down.** If you envision a user’s application as a tree
|
||||
@@ -42,7 +42,7 @@ Leptos, as a framework, reflects certain technical values:
|
||||
- **Embrace Rust semantics.** Especially in things like UI templating, use Rust
|
||||
semantics or extend them in a predictable way with control-flow components
|
||||
rather than overloading the meaning of Rust terms like `if` or `for` in a
|
||||
framework-speciic way.
|
||||
framework-specific way.
|
||||
- **Enhance ergonomics without obfuscating what’s happening.** This is by far
|
||||
the hardest to achieve. It’s often the case that adding additional layers to
|
||||
improve DX (like a custom build tool and starter templates) comes across as
|
||||
@@ -67,7 +67,7 @@ are a few guidelines that will make it a better experience for everyone:
|
||||
- Our CI tests every PR against all the existing examples, sometimes requiring
|
||||
compilation for both server and client side, etc. It’s thorough but slow. If
|
||||
you want to run CI locally to reduce frustration, you can do that by installing
|
||||
`cargo-make` and using `cargo make check && cargo make test && cargo make
|
||||
`cargo-make` and using `cargo make check && cargo make test && cargo make
|
||||
check-examples`.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -45,6 +45,8 @@ dependencies = [
|
||||
{ name = "check", path = "examples/fetch" },
|
||||
{ name = "check", path = "examples/hackernews" },
|
||||
{ name = "check", path = "examples/hackernews_axum" },
|
||||
{ name = "check", path = "examples/js-framework-benchmark" },
|
||||
{ name = "check", path = "examples/leptos-tailwind-axum" },
|
||||
{ name = "check", path = "examples/login_with_token_csr_only" },
|
||||
{ name = "check", path = "examples/parent_child" },
|
||||
{ name = "check", path = "examples/router" },
|
||||
@@ -54,6 +56,7 @@ dependencies = [
|
||||
{ name = "check", path = "examples/ssr_modes_axum" },
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
{ name = "check", path = "examples/tailwind_csr_trunk" },
|
||||
{ name = "check", path = "examples/timer" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_axum" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_viz" },
|
||||
@@ -102,6 +105,7 @@ args = ["make", "test-unit-and-web"]
|
||||
|
||||
[tasks.verify-examples]
|
||||
description = "Run all quality checks and tests for examples"
|
||||
env = { CLEAN_AFTER_VERIFY = "true" }
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "verify-flow"]
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
[](https://discord.gg/YdRAhS7eQB)
|
||||
[](https://matrix.to/#/#leptos:matrix.org)
|
||||
|
||||
|
||||
[Website](https://leptos.dev) | [Book](https://leptos-rs.github.io/leptos/) | [Docs.rs](https://docs.rs/leptos/latest/leptos/) | [Playground](https://codesandbox.io/p/sandbox/leptos-rtfggt?file=%2Fsrc%2Fmain.rs%3A1%2C1) | [Discord](https://discord.gg/YdRAhS7eQB)
|
||||
|
||||
# Leptos
|
||||
|
||||
@@ -47,6 +47,12 @@ Note that if you're using this with SSR too, the same Cargo profile will be appl
|
||||
target = "x86_64-unknown-linux-gnu" # or whatever
|
||||
```
|
||||
|
||||
Also note that in some cases, the cfg feature `has_std` will not be set, which may cause build errors with some dependencies which check for `has_std`. You may fix any build errors due to this by adding:
|
||||
```toml
|
||||
[build]
|
||||
rustflags = ["--cfg=has_std"]
|
||||
```
|
||||
|
||||
And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`. Note that this applies the same `build-std` and panic settings to your server binary, which may not be desirable. Some further exploration is probably needed here.
|
||||
|
||||
5. One of the sources of binary size in WASM binaries can be `serde` serialization/deserialization code. Leptos uses `serde` by default to serialize and deserialize resources created with `create_resource`. You might try experimenting with the `miniserde` and `serde-lite` features, which allow you to use those crates for serialization and deserialization instead; each only implements a subset of `serde`’s functionality, but typically optimizes for size over speed.
|
||||
|
||||
@@ -11,7 +11,7 @@ Actions and resources seem similar, but they represent fundamentally different t
|
||||
Say we have some `async` function we want to run.
|
||||
|
||||
```rust
|
||||
async fn add_todo(new_title: &str) -> Uuid {
|
||||
async fn add_todo_request(new_title: &str) -> Uuid {
|
||||
/* do some stuff on the server to add a new todo */
|
||||
}
|
||||
```
|
||||
@@ -41,16 +41,16 @@ async fn add_todo(new_title: &str) -> Uuid {
|
||||
So in this case, all we need to do to create an action is
|
||||
|
||||
```rust
|
||||
let add_todo = create_action(cx, |input: &String| {
|
||||
let add_todo_action = create_action(cx, |input: &String| {
|
||||
let input = input.to_owned();
|
||||
async move { add_todo(&input).await }
|
||||
async move { add_todo_request(&input).await }
|
||||
});
|
||||
```
|
||||
|
||||
Rather than calling `add_todo` directly, we’ll call it with `.dispatch()`, as in
|
||||
Rather than calling `add_todo_action` directly, we’ll call it with `.dispatch()`, as in
|
||||
|
||||
```rust
|
||||
add_todo.dispatch("Some value".to_string());
|
||||
add_todo_action.dispatch("Some value".to_string());
|
||||
```
|
||||
|
||||
You can do this from an event listener, a timeout, or anywhere; because `.dispatch()` isn’t an `async` function, it can be called from a synchronous context.
|
||||
@@ -58,9 +58,9 @@ You can do this from an event listener, a timeout, or anywhere; because `.dispat
|
||||
Actions provide access to a few signals that synchronize between the asynchronous action you’re calling and the synchronous reactive system:
|
||||
|
||||
```rust
|
||||
let submitted = add_todo.input(); // RwSignal<Option<String>>
|
||||
let pending = add_todo.pending(); // ReadSignal<bool>
|
||||
let todo_id = add_todo.value(); // RwSignal<Option<Uuid>>
|
||||
let submitted = add_todo_action.input(); // RwSignal<Option<String>>
|
||||
let pending = add_todo_action.pending(); // ReadSignal<bool>
|
||||
let todo_id = add_todo_action.value(); // RwSignal<Option<Uuid>>
|
||||
```
|
||||
|
||||
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
|
||||
@@ -73,7 +73,7 @@ view! { cx,
|
||||
on:submit=move |ev| {
|
||||
ev.prevent_default(); // don't reload the page...
|
||||
let input = input_ref.get().expect("input to exist");
|
||||
add_todo.dispatch(input.value());
|
||||
add_todo_action.dispatch(input.value());
|
||||
}
|
||||
>
|
||||
<label>
|
||||
|
||||
@@ -29,9 +29,9 @@ where
|
||||
}
|
||||
```
|
||||
|
||||
This is pretty straightforward: when the user is logged in, we want to show `children`. Until if the user is not logged in, we want to show `fallback`. And while we’re waiting to find out, we just render `()`, i.e., nothing.
|
||||
This is pretty straightforward: when the user is logged in, we want to show `children`. If the user is not logged in, we want to show `fallback`. And while we’re waiting to find out, we just render `()`, i.e., nothing.
|
||||
|
||||
In other words, we want to pass the children of `<WhenLoaded/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
|
||||
In other words, we want to pass the children of `<LoggedIn/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
|
||||
|
||||
This won’t compile.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ That’s where the [`leptos_meta`](https://docs.rs/leptos_meta/latest/leptos_met
|
||||
|
||||
`leptos_meta` also provides a [`<Script/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Script.html) component, and it’s worth pausing here for a second. All of the other components we’ve considered inject `<head>`-only elements in the `<head>`. But a `<script>` can also be included in the body.
|
||||
|
||||
There’s a very simple way to determine whether you should use a capital-S `<Script/>` component or a lowercase-s `<script>` element: the `<Script/>` component will be rendered in the `<head>`, and the `<script>` element will be rendered wherever in your the `<body>` of your user interface you put in, alongside other normal HTML elements. These cause JavaScript to load and run at different times, so use whichever is appropriate to your needs.
|
||||
There’s a very simple way to determine whether you should use a capital-S `<Script/>` component or a lowercase-s `<script>` element: the `<Script/>` component will be rendered in the `<head>`, and the `<script>` element will be rendered wherever in the `<body>` of your user interface you put it in, alongside other normal HTML elements. These cause JavaScript to load and run at different times, so use whichever is appropriate to your needs.
|
||||
|
||||
## `<Body/>` and `<Html/>`
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ There are four basic signal operations:
|
||||
1. [`.get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) clones the current value of the signal and tracks any future changes to the value reactively.
|
||||
2. [`.with()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalWith%3CT%3E-for-ReadSignal%3CT%3E) takes a function, which receives the current value of the signal by reference (`&T`), and tracks any future changes.
|
||||
3. [`.set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalSet%3CT%3E-for-WriteSignal%3CT%3E) replaces the current value of the signal and notifies any subscribers that they need to update.
|
||||
4. [`.update()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalUpdate%3CT%3E-for-WriteSignal%3CT%3E) takes a function, which receives a mutable reference to the current value of the signal (`&T`), and notifies any subscribers that they need to update. (`.update()` doesn’t return the value returned by the closure, but you can use [`.try_update()`](https://docs.rs/leptos/latest/leptos/trait.SignalUpdate.html#tymethod.try_update) if you need to; for example, if you’re removing an item from a `Vec<_>` and want the removed item.)
|
||||
4. [`.update()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalUpdate%3CT%3E-for-WriteSignal%3CT%3E) takes a function, which receives a mutable reference to the current value of the signal (`&mut T`), and notifies any subscribers that they need to update. (`.update()` doesn’t return the value returned by the closure, but you can use [`.try_update()`](https://docs.rs/leptos/latest/leptos/trait.SignalUpdate.html#tymethod.try_update) if you need to; for example, if you’re removing an item from a `Vec<_>` and want the removed item.)
|
||||
|
||||
Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `WriteSignal` as a function is syntax sugar for `.set()`. So
|
||||
|
||||
@@ -99,7 +99,7 @@ let clear_handler = move |_| {
|
||||
### If you really must...
|
||||
**4) Create an effect to write to B whenever A changes.** This is officially discouraged, for several reasons:
|
||||
a) It will always be less efficient, as it means every time A updates you do two full trips through the reactive process. (You set A, which causes the effect to run, as well as any other effects that depend on A. Then you set B, which causes any effects that depend on B to run.)
|
||||
b) It increases your chances of accidentally creating things like infinite loops or over-re-running effects. This is the kind of ping-ponging, reactive spaghetti code that was common in the early 2010s and that we try to avoid with things like read-write segregation and discouraging writing to signals frome effects.
|
||||
b) It increases your chances of accidentally creating things like infinite loops or over-re-running effects. This is the kind of ping-ponging, reactive spaghetti code that was common in the early 2010s and that we try to avoid with things like read-write segregation and discouraging writing to signals from effects.
|
||||
|
||||
In most situations, it’s best to rewrite things such that there’s a clear, top-down data flow based on derived signals or memos. But this isn’t the end of the world.
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
Routing drives most websites. A router is the answer to the question, “Given this URL, what should appear on the page?”
|
||||
|
||||
A URL consists of many parts. For example, the URL `https://leptos.dev/blog/search?q=Search#results` consists of
|
||||
A URL consists of many parts. For example, the URL `https://my-cool-blog.com/blog/search?q=Search#results` consists of
|
||||
|
||||
- a _scheme_: `https`
|
||||
- a _domain_: `leptos.dev`
|
||||
- a _domain_: `my-cool-blog.com`
|
||||
- a **path**: `/blog/search`
|
||||
- a **query** (or **search**): `?q=Search`
|
||||
- a _hash_: `#results`
|
||||
|
||||
@@ -28,7 +28,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
set_count(3);
|
||||
}
|
||||
>
|
||||
"Click me: "
|
||||
@@ -142,7 +142,7 @@ in a function, telling the framework to update the view every time `count` chang
|
||||
`{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!
|
||||
|
||||
Let’s make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Let’s replacing “set this value to 3” with “increment this value by 1”:
|
||||
Let’s make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Let’s replace “set this value to 3” with “increment this value by 1”:
|
||||
|
||||
```rust
|
||||
move |_| {
|
||||
|
||||
@@ -98,18 +98,6 @@ notice that you can easily tell the difference between an element and a componen
|
||||
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:
|
||||
>
|
||||
> `use progress_bar::{ProgressBar, ProgressBarProps};`
|
||||
>
|
||||
> **Note**: This is still true as of `0.2.5`, but the requirement has been removed on `main`
|
||||
> and will not apply to later versions.
|
||||
|
||||
### Reactive and Static Props
|
||||
|
||||
You’ll notice that throughout this example, `progress` takes a reactive
|
||||
|
||||
@@ -15,16 +15,33 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"fetch",
|
||||
"hackernews",
|
||||
"hackernews_axum",
|
||||
"js-framework-benchmark",
|
||||
"leptos-tailwind-axum",
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"session_auth_axum",
|
||||
"slots",
|
||||
"ssr_modes",
|
||||
"ssr_modes_axum",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
]
|
||||
|
||||
[tasks.gen-members]
|
||||
workspace = false
|
||||
description = "Generate the list of workspace members"
|
||||
script = '''
|
||||
examples=$(ls |
|
||||
grep -v README.md |
|
||||
grep -v Makefile.toml |
|
||||
grep -v cargo-make |
|
||||
grep -v gtk |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
|
||||
'''
|
||||
|
||||
@@ -6,7 +6,8 @@ description = "Check for style violations"
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.check-format]
|
||||
args = ["fmt", "--", "--check", "--config-path", "../../"]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
description = "Runs the cargo clean command."
|
||||
|
||||
@@ -15,7 +15,11 @@ dependencies = ["test-flow", "test-e2e-flow"]
|
||||
[tasks.pre-verify]
|
||||
|
||||
[tasks.post-verify]
|
||||
dependencies = ["clean-all"]
|
||||
dependencies = ["maybe-clean-all"]
|
||||
|
||||
[tasks.maybe-clean-all]
|
||||
description = "Used to clean up locally after call to verify-examples"
|
||||
condition = { env_true = ["CLEAN_AFTER_VERIFY"] }
|
||||
|
||||
[tasks.test-e2e-flow]
|
||||
description = "Provides pre and post hooks for test-e2e"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
[tasks.test]
|
||||
env = { RUN_CARGO_TEST = false }
|
||||
condition = { env_true = ["RUN_CARGO_TEST"] }
|
||||
|
||||
[tasks.post-test]
|
||||
dependencies = ["test-wasm"]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use counter_without_macros::counter;
|
||||
use leptos::*;
|
||||
|
||||
/// Show the counter
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
@@ -34,10 +34,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ExampleErrors/>
|
||||
}/>
|
||||
<Route path="" view=|cx| view! { cx, <ExampleErrors/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -66,7 +63,7 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
// 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/>}>
|
||||
<ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::{error::Result, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -8,30 +8,24 @@ pub struct Cat {
|
||||
}
|
||||
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum FetchError {
|
||||
pub enum CatError {
|
||||
#[error("Please request more than zero cats.")]
|
||||
NonZeroCats,
|
||||
#[error("Error loading data from serving.")]
|
||||
Request,
|
||||
#[error("Error deserializaing cat data from request.")]
|
||||
Json,
|
||||
}
|
||||
|
||||
type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, FetchError> {
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
if count > 0 {
|
||||
// make the request
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={count}",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| FetchError::Request)?
|
||||
.await?
|
||||
// convert it to JSON
|
||||
.json::<Vec<Cat>>()
|
||||
.await
|
||||
.map_err(|_| FetchError::Json)?
|
||||
.await?
|
||||
// extract the URL field for each cat
|
||||
.into_iter()
|
||||
.take(count)
|
||||
@@ -39,7 +33,7 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>, FetchError> {
|
||||
.collect::<Vec<_>>();
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(FetchError::NonZeroCats)
|
||||
Err(CatError::NonZeroCats.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ extend = [
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
args = ["+nightly", "build-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
args = ["+nightly", "check-all-features", "--target", "wasm32-unknown-unknown"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features --target wasm32-unknown-unknown -- -D warnings" }
|
||||
|
||||
@@ -11,10 +11,12 @@ axum = { version = "0.6.18", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
leptos = { version = "0.3", default-features = false, features = ["serde"] }
|
||||
leptos_axum = { version = "0.3", optional = true }
|
||||
leptos_meta = { version = "0.3", default-features = false }
|
||||
leptos_router = { version = "0.3", default-features = false }
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4"
|
||||
tokio = { version = "1.28.1", optional = true }
|
||||
|
||||
11
examples/leptos-tailwind-axum/Makefile.toml
Normal file
11
examples/leptos-tailwind-axum/Makefile.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -20,26 +20,25 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
fn Home(cx: Scope) -> impl IntoView {
|
||||
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
// thanks to https://tailwindcomponents.com/component/blue-buttons-example for the showcase layout
|
||||
view! { cx,
|
||||
<Title text="Leptos + Tailwindcss"/>
|
||||
<main>
|
||||
<div class="bg-gradient-to-tl from-blue-800 to-blue-500 text-white font-mono flex flex-col min-h-screen">
|
||||
<div class="flex flex-row-reverse flex-wrap m-auto">
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
"+"
|
||||
</button>
|
||||
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
|
||||
{value}
|
||||
</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
"-"
|
||||
</button>
|
||||
</div>
|
||||
<main>
|
||||
<div class="bg-gradient-to-tl from-blue-800 to-blue-500 text-white font-mono flex flex-col min-h-screen">
|
||||
<div class="flex flex-row-reverse flex-wrap m-auto">
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
"+"
|
||||
</button>
|
||||
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
|
||||
{value}
|
||||
</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
"-"
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
use cfg_if::cfg_if;
|
||||
use http::status::StatusCode;
|
||||
use leptos::*;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A basic function to display errors served by the error boundaries.
|
||||
// Feel free to do more complicated things here than just displaying the error.
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
cx: Scope,
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => create_rw_signal(cx, e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
},
|
||||
};
|
||||
// Get Errors from Signal
|
||||
let errors = errors.get();
|
||||
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<AppError> = errors
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.collect();
|
||||
println!("Errors: {errors:#?}");
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! { if #[cfg(feature="ssr")] {
|
||||
let response = use_context::<ResponseOptions>(cx);
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}}
|
||||
|
||||
view! {cx,
|
||||
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view= move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
cx,
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,27 @@ use cfg_if::cfg_if;
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::*;
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::error_template::AppError;
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::app::App;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(AppError::NotFound);
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move |cx| view!{ cx, <App/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use cfg_if::cfg_if;
|
||||
pub mod app;
|
||||
pub mod error_template;
|
||||
pub mod fileserv;
|
||||
pub mod fallback;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{extract::Extension, routing::post, Router};
|
||||
use axum::{routing::post, Router};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use leptos_tailwind::{app::*, fallback::file_and_error_handler};
|
||||
use log::info;
|
||||
use leptos_tailwind::app::*;
|
||||
use leptos_tailwind::fileserv::file_and_error_handler;
|
||||
use std::sync::Arc;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
|
||||
|
||||
simple_logger::init_with_level(log::Level::Info)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
||||
// For deployment these variables are:
|
||||
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
||||
// Alternately a file can be specified such as Some("Cargo.toml")
|
||||
// The file would need to be included with the executable when moved to deployment
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
|
||||
.leptos_routes(&leptos_options, routes, |cx| view! { cx, <App/> })
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../../" }
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../../" }
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../../" }
|
||||
|
||||
@@ -163,12 +163,11 @@ pub async fn login(
|
||||
|
||||
let user: User = User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or("User does not exist.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
.ok_or_else(|| {
|
||||
ServerFnError::ServerError("User does not exist.".into())
|
||||
})?;
|
||||
|
||||
match verify(password, &user.password)
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
match verify(password, &user.password)? {
|
||||
true => {
|
||||
auth.login_user(user.id);
|
||||
auth.remember_user(remember.is_some());
|
||||
@@ -204,13 +203,16 @@ pub async fn signup(
|
||||
.bind(username.clone())
|
||||
.bind(password_hashed)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
.await?;
|
||||
|
||||
let user = User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or("Signup failed: User does not exist.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
let user =
|
||||
User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
ServerFnError::ServerError(
|
||||
"Signup failed: User does not exist.".into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
auth.login_user(user.id);
|
||||
auth.remember_user(remember.is_some());
|
||||
|
||||
@@ -21,14 +21,12 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
pub fn pool(cx: Scope) -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>(cx)
|
||||
.ok_or("Pool missing.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
|
||||
pub fn auth(cx: Scope) -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>(cx)
|
||||
.ok_or("Auth session missing.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
@@ -64,11 +62,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
|
||||
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
@@ -117,12 +111,11 @@ pub async fn add_todo(cx: Scope, title: String) -> Result<(), ServerFnError> {
|
||||
pub async fn delete_todo(cx: Scope, id: u16) -> Result<(), ServerFnError> {
|
||||
let pool = pool(cx)?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -177,12 +170,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
<hr/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
}/> //Route
|
||||
<Route path="" view=|cx| view! { cx, <Todos/> }/> //Route
|
||||
<Route path="signup" view=move |cx| view! {
|
||||
cx,
|
||||
<Signup action=signup/>
|
||||
@@ -226,69 +214,71 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
": Created at "
|
||||
{todo.created_at}
|
||||
" by "
|
||||
{
|
||||
todo.user.unwrap_or_default().username
|
||||
}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
<ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
": Created at "
|
||||
{todo.created_at}
|
||||
" by "
|
||||
{
|
||||
todo.user.unwrap_or_default().username
|
||||
}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -87,22 +87,27 @@ fn Post(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
let post_view = move || {
|
||||
post.with(cx, |post| {
|
||||
post.clone().map(|post| {
|
||||
view! { cx,
|
||||
// render content
|
||||
<h1>{&post.title}</h1>
|
||||
<p>{&post.content}</p>
|
||||
// this view needs to take the `Scope` from the `<Suspense/>`, not
|
||||
// from the parent component, so we take that as an argument and
|
||||
// pass it in under the `<Suspense/>` so that it is correct
|
||||
let post_view = move |cx| {
|
||||
move || {
|
||||
post.with(cx, |post| {
|
||||
post.clone().map(|post| {
|
||||
view! { cx,
|
||||
// render content
|
||||
<h1>{&post.title}</h1>
|
||||
<p>{&post.content}</p>
|
||||
|
||||
// since we're using async rendering for this page,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
}
|
||||
// since we're using async rendering for this page,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
@@ -121,7 +126,7 @@ fn Post(cx: Scope) -> impl IntoView {
|
||||
</div>
|
||||
}
|
||||
}>
|
||||
{post_view}
|
||||
{post_view(cx)}
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
}
|
||||
|
||||
@@ -108,4 +108,4 @@ Many thanks to GreatGreg for putting together this guide. You can find the origi
|
||||
## Playwright Testing
|
||||
|
||||
- Run `cargo make setup` to install dependencies
|
||||
- Run `cargo leptos test` or `cargo leptos end-to-end` to run the tests
|
||||
- Run `cargo leptos test` or `cargo leptos end-to-end` to run the test
|
||||
|
||||
@@ -9,7 +9,7 @@ cfg_if! {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -43,11 +43,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut todos = Vec::new();
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
@@ -76,12 +72,11 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
||||
@@ -11,7 +11,7 @@ cfg_if! {
|
||||
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -47,11 +47,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut todos = Vec::new();
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
@@ -93,12 +89,11 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -115,9 +110,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
<Route path="" view=|cx| view! { cx, <Todos/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -11,7 +11,7 @@ cfg_if! {
|
||||
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -47,11 +47,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut todos = Vec::new();
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
@@ -93,12 +89,11 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -117,9 +112,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
}/> //Route
|
||||
</Routes>
|
||||
</main>
|
||||
@@ -151,63 +144,65 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors/>}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1105,19 +1105,19 @@ where
|
||||
pub trait Extractor<T> {
|
||||
type Future;
|
||||
|
||||
fn call(&self, args: T) -> Self::Future;
|
||||
fn call(self, args: T) -> Self::Future;
|
||||
}
|
||||
macro_rules! factory_tuple ({ $($param:ident)* } => {
|
||||
impl<Func, Fut, $($param,)*> Extractor<($($param,)*)> for Func
|
||||
where
|
||||
Func: Fn($($param),*) -> Fut + Clone + 'static,
|
||||
Func: FnOnce($($param),*) -> Fut + Clone + 'static,
|
||||
Fut: Future,
|
||||
{
|
||||
type Future = Fut;
|
||||
|
||||
#[inline]
|
||||
#[allow(non_snake_case)]
|
||||
fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future {
|
||||
fn call(self, ($($param,)*): ($($param,)*)) -> Self::Future {
|
||||
(self)($($param,)*)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1337,19 +1337,19 @@ pub trait Extractor<T, U>
|
||||
where
|
||||
T: FromRequestParts<()>,
|
||||
{
|
||||
fn call(&self, args: T) -> Pin<Box<dyn Future<Output = U>>>;
|
||||
fn call(self, args: T) -> Pin<Box<dyn Future<Output = U>>>;
|
||||
}
|
||||
|
||||
macro_rules! factory_tuple ({ $($param:ident)* } => {
|
||||
impl<Func, Fut, U, $($param,)*> Extractor<($($param,)*), U> for Func
|
||||
where
|
||||
$($param: FromRequestParts<()> + Send,)*
|
||||
Func: Fn($($param),*) -> Fut + 'static,
|
||||
Func: FnOnce($($param),*) -> Fut + 'static,
|
||||
Fut: Future<Output = U> + 'static,
|
||||
{
|
||||
#[inline]
|
||||
#[allow(non_snake_case)]
|
||||
fn call(&self, ($($param,)*): ($($param,)*)) -> Pin<Box<dyn Future<Output = U>>> {
|
||||
fn call(self, ($($param,)*): ($($param,)*)) -> Pin<Box<dyn Future<Output = U>>> {
|
||||
Box::pin((self)($($param,)*))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::Children;
|
||||
use leptos_dom::{Errors, IntoView};
|
||||
use leptos_dom::{Errors, HydrationCtx, IntoView};
|
||||
use leptos_macro::{component, view};
|
||||
use leptos_reactive::{
|
||||
create_rw_signal, provide_context, signal_prelude::*, RwSignal, Scope,
|
||||
@@ -28,7 +28,25 @@ use leptos_reactive::{
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
///
|
||||
/// ## Interaction with `<Suspense/>`
|
||||
/// If you use this with a `<Suspense/>` or `<Transition/>` component, note that the
|
||||
/// `<ErrorBoundary/>` should go inside the `<Suspense/>`, not the other way around,
|
||||
/// if there’s a chance that the `<ErrorBoundary/>` will begin in the error state.
|
||||
/// This is a limitation of the current design of the two components and the way they
|
||||
/// hydrate. Placing the `<ErrorBoundary/>` outside the `<Suspense/>` means that
|
||||
/// it is rendered on the server without any knowledge of the suspended view, so it
|
||||
/// will always be rendered on the server as if there were no errors, but might need
|
||||
/// to be hydrated with errors, depending on the actual result.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// view! { cx,
|
||||
/// <Suspense fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
/// <ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||
/// {move || {
|
||||
/// /* etc. */
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn ErrorBoundary<F, IV>(
|
||||
cx: Scope,
|
||||
/// The components inside the tag which will get rendered
|
||||
@@ -40,12 +58,31 @@ where
|
||||
F: Fn(Scope, RwSignal<Errors>) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
_ = HydrationCtx::next_component();
|
||||
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
|
||||
|
||||
provide_context(cx, errors);
|
||||
|
||||
// Run children so that they render and execute resources
|
||||
let children = children(cx).into_view(cx);
|
||||
let children = children(cx);
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "hydrate"))]
|
||||
{
|
||||
use leptos_dom::View;
|
||||
if children.nodes.iter().any(|child| {
|
||||
matches!(child, View::Suspense(_, _))
|
||||
|| matches!(child, View::Component(repr) if repr.name() == "Transition")
|
||||
}) {
|
||||
crate::debug_warn!("You are using a <Suspense/> or \
|
||||
<Transition/> as the direct child of an <ErrorBoundary/>. To ensure correct \
|
||||
hydration, these should be reorganized so that the <ErrorBoundary/> is a child \
|
||||
of the <Suspense/> or <Transition/> instead: \n\
|
||||
\nview! {{ cx,\
|
||||
\n <Suspense fallback=todo!()>\n <ErrorBoundary fallback=todo!()>\n {{move || {{ /* etc. */")
|
||||
}
|
||||
}
|
||||
|
||||
let children = children.into_view(cx);
|
||||
let errors_empty = create_memo(cx, move |_| errors.with(Errors::is_empty));
|
||||
|
||||
move || {
|
||||
|
||||
@@ -171,6 +171,11 @@ pub use leptos_dom::{
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
|
||||
/// Types to make it easier to handle errors in your application.
|
||||
pub mod error {
|
||||
pub use server_fn::error::{Error, Result};
|
||||
}
|
||||
#[cfg(not(any(target_arch = "wasm32", feature = "template_macro")))]
|
||||
pub use leptos_macro::view as template;
|
||||
pub use leptos_macro::{component, server, slot, view, Params};
|
||||
@@ -178,6 +183,7 @@ pub use leptos_reactive::*;
|
||||
pub use leptos_server::{
|
||||
self, create_action, create_multi_action, create_server_action,
|
||||
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
|
||||
ServerFnErrorErr,
|
||||
};
|
||||
pub use server_fn::{self, ServerFn as _};
|
||||
pub use typed_builder;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use leptos_dom::{Fragment, IntoView, View};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{use_context, Scope, SignalSetter, SuspenseContext};
|
||||
use leptos_reactive::{
|
||||
create_isomorphic_effect, use_context, Scope, SignalGet, SignalSetter,
|
||||
SuspenseContext,
|
||||
};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
@@ -97,9 +100,6 @@ where
|
||||
is_first_run(&first_run, &suspense_context);
|
||||
first_run.set(is_first_run);
|
||||
|
||||
if let Some(set_pending) = &set_pending {
|
||||
set_pending.set(true);
|
||||
}
|
||||
if let Some(prev_children) = &*prev_child.borrow() {
|
||||
if is_first_run {
|
||||
fallback().into_view(cx)
|
||||
@@ -132,9 +132,12 @@ where
|
||||
}
|
||||
child_runs.set(child_runs.get() + 1);
|
||||
|
||||
if let Some(set_pending) = &set_pending {
|
||||
set_pending.set(false);
|
||||
}
|
||||
let pending = suspense_context.pending_resources;
|
||||
create_isomorphic_effect(cx, move |_| {
|
||||
if let Some(set_pending) = set_pending {
|
||||
set_pending.set(pending.get() > 0)
|
||||
}
|
||||
});
|
||||
frag
|
||||
}))
|
||||
.build(),
|
||||
|
||||
@@ -17,7 +17,7 @@ leptos_actix = { path = "../../../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4"
|
||||
wasm-bindgen = "0.2.85"
|
||||
wasm-bindgen = "0.2.87"
|
||||
serde = "1.0.159"
|
||||
tokio = { version = "1.27.0", features = ["time"], optional = true }
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
@@ -69,6 +70,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
@@ -85,6 +87,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
@@ -101,6 +104,7 @@ fn SecondaryNav(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<nav>
|
||||
<A href="" exact=true>"Nested"</A>
|
||||
<A href="inside" exact=true>"Nested (resource created inside)"</A>
|
||||
<A href="single">"Single"</A>
|
||||
<A href="parallel">"Parallel"</A>
|
||||
<A href="inside-component">"Inside Component"</A>
|
||||
@@ -139,6 +143,43 @@ fn Nested(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NestedResourceInside(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(|_| {
|
||||
let two_second = create_resource(cx, || (), move |_| async move {
|
||||
leptos::log!("creating two_second resource");
|
||||
two_second_fn(()).await
|
||||
});
|
||||
view! { cx,
|
||||
<p>{move || one_second.read(cx).map(|_| "Loaded 1!")}</p>
|
||||
<Suspense fallback=|| "Loading 2...">
|
||||
"Two Second: "
|
||||
{move || {
|
||||
two_second.read(cx).map(|x| view! { cx,
|
||||
"Loaded 2 (created inside first suspense)!: "
|
||||
{format!("{x:?}")}
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Parallel(cx: Scope) -> impl IntoView {
|
||||
let one_second = create_resource(cx, || (), one_second_fn);
|
||||
|
||||
@@ -18,6 +18,7 @@ indexmap = "1.9"
|
||||
itertools = "0.10"
|
||||
js-sys = "0.3"
|
||||
leptos_reactive = { workspace = true }
|
||||
server_fn = { workspace = true }
|
||||
once_cell = "1"
|
||||
pad-adapter = "0.1"
|
||||
paste = "1"
|
||||
|
||||
@@ -26,117 +26,118 @@ fn main() {
|
||||
mount_to_body(view_fn);
|
||||
}
|
||||
|
||||
// fn view_fn(cx: Scope) -> impl IntoView {
|
||||
// view! { cx,
|
||||
// <h2>"Passing Tests"</h2>
|
||||
// <ul>
|
||||
// /* These work! */
|
||||
// <Test from=[1] to=[] />
|
||||
// <Test from=[1, 2] to=[] />
|
||||
// <Test from=[1, 2, 3] to=[] />
|
||||
// <hr/>
|
||||
// <Test from=[] to=[1] />
|
||||
// <Test from=[1, 2] to=[1] />
|
||||
// <Test from=[2, 1] to=[1] />
|
||||
// <hr/>
|
||||
// <Test from=[1, 2, 3] to=[1, 2] />
|
||||
// <Test from=[2] to=[1, 2] />
|
||||
// <Test from=[1] to=[1, 2] />
|
||||
// <Test from=[] to=[1, 2, 3] />
|
||||
// <Test from=[2] to=[1, 2, 3] />
|
||||
// <Test from=[1] to=[1, 2, 3] />
|
||||
// <Test from=[1, 3, 2] to=[1, 2, 3] />
|
||||
// <Test from=[2, 1, 3] to=[1, 2, 3] />
|
||||
// </ul>
|
||||
// <h2>"Broken Tests"</h2>
|
||||
// <ul>
|
||||
// <Test from=[3] to=[1, 2, 3] />
|
||||
// <Test from=[3, 1] to=[1, 2, 3] />
|
||||
// <Test from=[3, 2, 1] to=[1, 2, 3] />
|
||||
// <hr/>
|
||||
// <Test from=[1, 4, 2, 3] to=[1, 2, 3, 4] />
|
||||
// <hr/>
|
||||
// <Test from=[1, 4, 3, 2, 5] to=[1, 2, 3, 4, 5] />
|
||||
// <Test from=[4, 5, 3, 1, 2] to=[1, 2, 3, 4, 5] />
|
||||
// </ul>
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[component]
|
||||
// fn Test<From, To>(cx: Scope, from: From, to: To) -> impl IntoView
|
||||
// where
|
||||
// From: IntoIterator<Item = usize>,
|
||||
// To: IntoIterator<Item = usize>,
|
||||
// {
|
||||
// let from = from.into_iter().collect::<Vec<_>>();
|
||||
// let to = to.into_iter().collect::<Vec<_>>();
|
||||
|
||||
// let (list, set_list) = create_signal(cx, from.clone());
|
||||
// request_animation_frame({
|
||||
// let to = to.clone();
|
||||
// move || {
|
||||
// set_list(to);
|
||||
// }
|
||||
// });
|
||||
|
||||
// view! { cx,
|
||||
// <li>
|
||||
// "from: [" {move ||
|
||||
// from
|
||||
// .iter()
|
||||
// .map(ToString::to_string)
|
||||
// .intersperse(", ".to_string())
|
||||
// .collect::<String>()
|
||||
// } "]"
|
||||
// <br />
|
||||
// "to: [" {move ||
|
||||
// to
|
||||
// .iter()
|
||||
// .map(ToString::to_string)
|
||||
// .intersperse(", ".to_string())
|
||||
// .collect::<String>()
|
||||
// } "]"
|
||||
// <br />
|
||||
// "result: ["
|
||||
// <For
|
||||
// each=list
|
||||
// key=|i| *i
|
||||
// view=|cx, i| {
|
||||
// view! { cx, <span>{i} ", "</span> }
|
||||
// }
|
||||
// /> "]"
|
||||
// /* <p>
|
||||
// "Pre | "
|
||||
// <For
|
||||
// each=list
|
||||
// key=|i| *i
|
||||
// view=|cx, i| {
|
||||
// view! { cx, <span>{i}</span> }
|
||||
// }
|
||||
// />
|
||||
// " | Post"
|
||||
// </p> */
|
||||
// </li>
|
||||
// }
|
||||
// }
|
||||
|
||||
fn view_fn(cx: Scope) -> impl IntoView {
|
||||
let (should_show_a, sett_should_show_a) = create_signal(cx, true);
|
||||
|
||||
let a = vec![1, 2, 3, 4];
|
||||
let b = vec![1, 2, 3];
|
||||
|
||||
view! { cx,
|
||||
<button on:click=move |_| sett_should_show_a.update(|show| *show = !*show)>"Toggle"</button>
|
||||
|
||||
<For
|
||||
each={move || if should_show_a.get() {
|
||||
a.clone()
|
||||
} else {
|
||||
b.clone()
|
||||
}}
|
||||
key=|i| *i
|
||||
view=|cx, i| view! { cx, <h1>{i}</h1> }
|
||||
/>
|
||||
<h2>"Passing Tests"</h2>
|
||||
<ul>
|
||||
<Test from=[1] to=[]/>
|
||||
<Test from=[1, 2] to=[3, 2] then=vec![2]/>
|
||||
<Test from=[1, 2] to=[]/>
|
||||
<Test from=[1, 2, 3] to=[]/>
|
||||
<hr/>
|
||||
<Test from=[] to=[1]/>
|
||||
<Test from=[1, 2] to=[1]/>
|
||||
<Test from=[2, 1] to=[1]/>
|
||||
<hr/>
|
||||
<Test from=[1, 2, 3] to=[1, 2]/>
|
||||
<Test from=[2] to=[1, 2]/>
|
||||
<Test from=[1] to=[1, 2]/>
|
||||
<Test from=[] to=[1, 2, 3]/>
|
||||
<Test from=[2] to=[1, 2, 3]/>
|
||||
<Test from=[1] to=[1, 2, 3]/>
|
||||
<Test from=[1, 3, 2] to=[1, 2, 3]/>
|
||||
<Test from=[2, 1, 3] to=[1, 2, 3]/>
|
||||
<Test from=[3] to=[1, 2, 3]/>
|
||||
<Test from=[3, 1] to=[1, 2, 3]/>
|
||||
<Test from=[3, 2, 1] to=[1, 2, 3]/>
|
||||
<hr/>
|
||||
<Test from=[1, 4, 2, 3] to=[1, 2, 3, 4]/>
|
||||
<hr/>
|
||||
<Test from=[1, 4, 3, 2, 5] to=[1, 2, 3, 4, 5]/>
|
||||
<Test from=[4, 5, 3, 1, 2] to=[1, 2, 3, 4, 5]/>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Test<From, To>(
|
||||
cx: Scope,
|
||||
from: From,
|
||||
to: To,
|
||||
#[prop(optional)] then: Option<Vec<usize>>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
From: IntoIterator<Item = usize>,
|
||||
To: IntoIterator<Item = usize>,
|
||||
{
|
||||
let from = from.into_iter().collect::<Vec<_>>();
|
||||
let to = to.into_iter().collect::<Vec<_>>();
|
||||
|
||||
let (list, set_list) = create_signal(cx, from.clone());
|
||||
request_animation_frame({
|
||||
let to = to.clone();
|
||||
let then = then.clone();
|
||||
move || {
|
||||
set_list(to);
|
||||
|
||||
if let Some(then) = then {
|
||||
request_animation_frame({
|
||||
move || {
|
||||
set_list(then);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
"from: [" {move || {
|
||||
from
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.intersperse(", ".to_string())
|
||||
.collect::<String>()
|
||||
}} "]" <br/> "to: [" {
|
||||
let then = then.clone();
|
||||
move || {
|
||||
then
|
||||
.clone()
|
||||
.unwrap_or(to.iter().copied().collect())
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.intersperse(", ".to_string())
|
||||
.collect::<String>()
|
||||
}
|
||||
} "]" <br/> "result: ["
|
||||
<For
|
||||
each=list
|
||||
key=|i| *i
|
||||
view=|cx, i| {
|
||||
view! { cx, <span>{i} ", "</span> }
|
||||
}
|
||||
/> "]"
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
// fn view_fn(cx: Scope) -> impl IntoView {
|
||||
// let (should_show_a, sett_should_show_a) = create_signal(cx, true);
|
||||
|
||||
// let a = vec![2];
|
||||
// let b = vec![1, 2, 3];
|
||||
|
||||
// view! { cx,
|
||||
// <button on:click=move |_| sett_should_show_a.update(|show| *show = !*show)>"Toggle"</button>
|
||||
|
||||
// <For
|
||||
// each={move || if should_show_a.get() {
|
||||
// a.clone()
|
||||
// } else {
|
||||
// b.clone()
|
||||
// }}
|
||||
// key=|i| *i
|
||||
// view=|cx, i| view! { cx, <h1>{i}</h1> }
|
||||
// />
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -221,6 +221,12 @@ impl ComponentRepr {
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
/// Returns the name of the component.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
/// A user-defined `leptos` component.
|
||||
|
||||
@@ -273,7 +273,8 @@ where
|
||||
// I can imagine some edge case that the child changes while
|
||||
// hydration is ongoing
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
if !was_child_moved && child != new_child {
|
||||
let same_child = child == new_child;
|
||||
if !was_child_moved && !same_child {
|
||||
// Remove the child
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
@@ -308,10 +309,13 @@ where
|
||||
}
|
||||
|
||||
// Mount the new child
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
// If it's the same child, don't re-mount
|
||||
if !same_child {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We want to reuse text nodes, so hold onto it if
|
||||
|
||||
@@ -653,6 +653,7 @@ fn apply_opts<K: Eq + Hash>(
|
||||
&& cmds.moved.is_empty()
|
||||
{
|
||||
cmds.clear = true;
|
||||
cmds.removed.clear();
|
||||
|
||||
cmds.added
|
||||
.iter_mut()
|
||||
@@ -800,8 +801,11 @@ fn apply_diff<T, EF, V>(
|
||||
// The order of cmds needs to be:
|
||||
// 1. Clear
|
||||
// 2. Removals
|
||||
// 3. Remove holes left from removals
|
||||
// 4. Moves + Add
|
||||
// 3. Move out
|
||||
// 4. Resize
|
||||
// 5. Move in
|
||||
// 6. Additions
|
||||
// 7. Removes holes
|
||||
if diff.clear {
|
||||
if opening.previous_sibling().is_none()
|
||||
&& closing.next_sibling().is_none()
|
||||
@@ -818,13 +822,17 @@ fn apply_diff<T, EF, V>(
|
||||
#[cfg(not(debug_assertions))]
|
||||
parent.append_with_node_1(closing).unwrap();
|
||||
} else {
|
||||
range.set_start_before(opening).unwrap();
|
||||
range.set_start_after(opening).unwrap();
|
||||
range.set_end_before(closing).unwrap();
|
||||
|
||||
range.delete_contents().unwrap();
|
||||
}
|
||||
|
||||
return;
|
||||
children.clear();
|
||||
|
||||
if diff.added.is_empty() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for DiffOpRemove { at } in &diff.removed {
|
||||
@@ -833,17 +841,6 @@ fn apply_diff<T, EF, V>(
|
||||
item_to_remove.prepare_for_move();
|
||||
}
|
||||
|
||||
// Now, remove the holes that might have been left from removing
|
||||
// items
|
||||
#[allow(unstable_name_collisions)]
|
||||
children.drain_filter(|c| c.is_none());
|
||||
|
||||
// Resize children if needed
|
||||
if let Some(added) = diff.added.len().checked_sub(diff.removed.len()) {
|
||||
let target_size = children.len() + added;
|
||||
children.resize_with(target_size, || None);
|
||||
}
|
||||
|
||||
let (move_cmds, add_cmds) = unpack_moves(&diff);
|
||||
|
||||
let mut moved_children = move_cmds
|
||||
@@ -859,6 +856,8 @@ fn apply_diff<T, EF, V>(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
children.resize_with(children.len() + diff.added.len(), || None);
|
||||
|
||||
for (i, DiffOpMove { to, .. }) in move_cmds
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -906,6 +905,9 @@ fn apply_diff<T, EF, V>(
|
||||
|
||||
children[at] = Some(each_item);
|
||||
}
|
||||
|
||||
#[allow(unstable_name_collisions)]
|
||||
children.drain_filter(|c| c.is_none());
|
||||
}
|
||||
|
||||
/// Unpacks adds and moves into a sequence of interleaved
|
||||
@@ -924,20 +926,17 @@ fn unpack_moves(diff: &Diff) -> (Vec<DiffOpMove>, Vec<DiffOpAdd>) {
|
||||
let mut adds_next = adds_iter.next();
|
||||
let mut moves_next = moves_iter.next().copied();
|
||||
|
||||
let mut from_offset: usize = 0;
|
||||
|
||||
for i in 0..diff.items_to_move + diff.added.len() + diff.removed.len() {
|
||||
if let Some(DiffOpRemove { at, .. }) = removes_next {
|
||||
if i == *at {
|
||||
removes_next = removes_iter.next();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match (adds_next, &mut moves_next) {
|
||||
(Some(add), Some(move_)) => {
|
||||
if let Some(DiffOpRemove { at }) = removes_next {
|
||||
if *at == i {
|
||||
from_offset += 1;
|
||||
removes_next = removes_iter.next();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if add.at == i {
|
||||
adds.push(*add);
|
||||
|
||||
@@ -945,7 +944,6 @@ fn unpack_moves(diff: &Diff) -> (Vec<DiffOpMove>, Vec<DiffOpAdd>) {
|
||||
} else {
|
||||
let mut single_move = *move_;
|
||||
single_move.len = 1;
|
||||
single_move.from -= from_offset;
|
||||
|
||||
moves.push(single_move);
|
||||
|
||||
@@ -964,18 +962,8 @@ fn unpack_moves(diff: &Diff) -> (Vec<DiffOpMove>, Vec<DiffOpAdd>) {
|
||||
adds_next = adds_iter.next();
|
||||
}
|
||||
(None, Some(move_)) => {
|
||||
if let Some(DiffOpRemove { at }) = removes_next {
|
||||
if *at == i {
|
||||
from_offset += 1;
|
||||
removes_next = removes_iter.next();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut single_move = *move_;
|
||||
single_move.len = 1;
|
||||
single_move.from -= from_offset;
|
||||
|
||||
moves.push(single_move);
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::{HydrationCtx, IntoView};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::{signal_prelude::*, use_context, RwSignal};
|
||||
use std::{borrow::Cow, collections::HashMap, error::Error, sync::Arc};
|
||||
use server_fn::error::Error;
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
/// A struct to hold all the possible errors that could be provided by child Views
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[repr(transparent)]
|
||||
pub struct Errors(HashMap<ErrorKey, Arc<dyn Error + Send + Sync>>);
|
||||
pub struct Errors(HashMap<ErrorKey, Error>);
|
||||
|
||||
/// A unique key for an error that occurs at a particular location in the user interface.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -24,7 +25,7 @@ where
|
||||
}
|
||||
|
||||
impl IntoIterator for Errors {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
type Item = (ErrorKey, Error);
|
||||
type IntoIter = IntoIter;
|
||||
|
||||
#[inline(always)]
|
||||
@@ -35,15 +36,10 @@ impl IntoIterator for Errors {
|
||||
|
||||
/// An owning iterator over all the errors contained in the [Errors] struct.
|
||||
#[repr(transparent)]
|
||||
pub struct IntoIter(
|
||||
std::collections::hash_map::IntoIter<
|
||||
ErrorKey,
|
||||
Arc<dyn Error + Send + Sync>,
|
||||
>,
|
||||
);
|
||||
pub struct IntoIter(std::collections::hash_map::IntoIter<ErrorKey, Error>);
|
||||
|
||||
impl Iterator for IntoIter {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
type Item = (ErrorKey, Error);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(
|
||||
@@ -55,16 +51,10 @@ impl Iterator for IntoIter {
|
||||
|
||||
/// An iterator over all the errors contained in the [Errors] struct.
|
||||
#[repr(transparent)]
|
||||
pub struct Iter<'a>(
|
||||
std::collections::hash_map::Iter<
|
||||
'a,
|
||||
ErrorKey,
|
||||
Arc<dyn Error + Send + Sync>,
|
||||
>,
|
||||
);
|
||||
pub struct Iter<'a>(std::collections::hash_map::Iter<'a, ErrorKey, Error>);
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = (&'a ErrorKey, &'a Arc<dyn Error + Send + Sync>);
|
||||
type Item = (&'a ErrorKey, &'a Error);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(
|
||||
@@ -77,10 +67,10 @@ impl<'a> Iterator for Iter<'a> {
|
||||
impl<T, E> IntoView for Result<T, E>
|
||||
where
|
||||
T: IntoView + 'static,
|
||||
E: Error + Send + Sync + 'static,
|
||||
E: Into<Error>,
|
||||
{
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
let id = ErrorKey(HydrationCtx::peek().id.to_string().into());
|
||||
let id = ErrorKey(HydrationCtx::peek().fragment.to_string().into());
|
||||
let errors = use_context::<RwSignal<Errors>>(cx);
|
||||
match self {
|
||||
Ok(stuff) => {
|
||||
@@ -92,6 +82,7 @@ where
|
||||
stuff.into_view(cx)
|
||||
}
|
||||
Err(error) => {
|
||||
let error = error.into();
|
||||
match errors {
|
||||
Some(errors) => {
|
||||
errors.update({
|
||||
@@ -133,6 +124,7 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Errors {
|
||||
/// Returns `true` if there are no errors.
|
||||
#[inline(always)]
|
||||
@@ -143,24 +135,21 @@ impl Errors {
|
||||
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn insert<E>(&mut self, key: ErrorKey, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
E: Into<Error>,
|
||||
{
|
||||
self.0.insert(key, Arc::new(error));
|
||||
self.0.insert(key, error.into());
|
||||
}
|
||||
|
||||
/// Add an error with the default key for errors outside the reactive system
|
||||
pub fn insert_with_default_key<E>(&mut self, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
E: Into<Error>,
|
||||
{
|
||||
self.0.insert(Default::default(), Arc::new(error));
|
||||
self.0.insert(Default::default(), error.into());
|
||||
}
|
||||
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove(
|
||||
&mut self,
|
||||
key: &ErrorKey,
|
||||
) -> Option<Arc<dyn Error + Send + Sync>> {
|
||||
pub fn remove(&mut self, key: &ErrorKey) -> Option<Error> {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ cfg_if! {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
// We can tell if we start in hydration mode by checking to see if the
|
||||
// id "_0-0-0" is present in the DOM. If it is, we know we are hydrating from
|
||||
// id "_0-1" is present in the DOM. If it is, we know we are hydrating from
|
||||
// the server, if not, we are starting off in CSR
|
||||
thread_local! {
|
||||
static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
|
||||
|
||||
@@ -221,66 +221,105 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
||||
blocking_fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.await) });
|
||||
} else {
|
||||
fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.await) });
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id.clone(), data.out_of_order.await)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = (String, String)>>>);
|
||||
}
|
||||
}
|
||||
|
||||
let stream = futures::stream::once(
|
||||
// HTML for the view function and script to store resources
|
||||
async move {
|
||||
let resolvers = format!(
|
||||
"<script>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
|
||||
if replace_blocks {
|
||||
let mut blocks = Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close = format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) =
|
||||
shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell = format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
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!("{prefix}{shell}{resolvers}{blocking}")
|
||||
}
|
||||
},
|
||||
)
|
||||
.chain(ooo_body_stream_recurse(cx, fragments, serializers));
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
fn ooo_body_stream_recurse(
|
||||
cx: Scope,
|
||||
fragments: FuturesUnordered<PinnedFuture<(String, String)>>,
|
||||
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
|
||||
) -> Pin<Box<dyn Stream<Item = String>>> {
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
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 resolvers = format!(
|
||||
"<script>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
|
||||
if replace_blocks {
|
||||
let mut blocks = Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close = format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) = shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell = format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
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!("{prefix}{shell}{resolvers}{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);
|
||||
|
||||
(stream, runtime, scope)
|
||||
Box::pin(
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
fragments.chain(resources).chain(
|
||||
futures::stream::once(async move {
|
||||
let pending = cx.pending_fragments();
|
||||
if pending.len() > 0 {
|
||||
let fragments = FuturesUnordered::new();
|
||||
let serializers = cx.serialization_resolvers();
|
||||
for (fragment_id, data) in pending {
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id.clone(), data.out_of_order.await)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = (String, String)>>>);
|
||||
}
|
||||
Box::pin(ooo_body_stream_recurse(
|
||||
cx,
|
||||
fragments,
|
||||
serializers,
|
||||
))
|
||||
as Pin<Box<dyn Stream<Item = String>>>
|
||||
} else {
|
||||
Box::pin(futures::stream::once(async move {
|
||||
Default::default()
|
||||
}))
|
||||
}
|
||||
})
|
||||
.flatten(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
|
||||
@@ -94,13 +94,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
let runtime = create_runtime();
|
||||
|
||||
let (
|
||||
(
|
||||
blocking_fragments_ready,
|
||||
chunks,
|
||||
prefix,
|
||||
pending_resources,
|
||||
serializers,
|
||||
),
|
||||
(blocking_fragments_ready, chunks, prefix, pending_resources),
|
||||
scope_id,
|
||||
_,
|
||||
) = run_scope_undisposed(runtime, |cx| {
|
||||
@@ -115,7 +109,6 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view.into_stream_chunks(cx),
|
||||
prefix,
|
||||
serde_json::to_string(&cx.pending_resources()).unwrap(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
});
|
||||
let cx = Scope {
|
||||
@@ -130,7 +123,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
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;
|
||||
handle_chunks(cx, tx, remaining_chunks).await;
|
||||
});
|
||||
|
||||
let stream = futures::stream::once(async move {
|
||||
@@ -147,7 +140,13 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
)
|
||||
})
|
||||
.chain(rx)
|
||||
.chain(render_serializers(serializers));
|
||||
.chain(
|
||||
futures::stream::once(async move {
|
||||
let serializers = cx.serialization_resolvers();
|
||||
render_serializers(serializers)
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
(stream, runtime, scope_id)
|
||||
}
|
||||
@@ -196,6 +195,7 @@ async fn handle_blocking_chunks(
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_chunks(
|
||||
cx: Scope,
|
||||
tx: UnboundedSender<String>,
|
||||
chunks: VecDeque<StreamChunk>,
|
||||
) {
|
||||
@@ -210,7 +210,7 @@ async fn handle_chunks(
|
||||
|
||||
// send the inner stream
|
||||
let suspended = chunks.await;
|
||||
handle_chunks(tx.clone(), suspended).await;
|
||||
handle_chunks(cx, tx.clone(), suspended).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,9 @@ pub fn block_to_primitive_expression(block: &syn::Block) -> Option<&syn::Expr> {
|
||||
return None;
|
||||
}
|
||||
match &block.stmts[0] {
|
||||
syn::Stmt::Expr(e, None) => return Some(e),
|
||||
_ => {}
|
||||
syn::Stmt::Expr(e, None) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Converts simple literals to its string representation.
|
||||
|
||||
@@ -847,7 +847,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]`
|
||||
/// macro to specify an alternate encoding:
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// #[server(AddTodo, "/api", "Url")]
|
||||
/// #[server(AddTodo, "/api", "GetJson")]
|
||||
/// #[server(AddTodo, "/api", "Cbor")]
|
||||
|
||||
@@ -10,7 +10,7 @@ use rstml::node::{
|
||||
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
|
||||
use syn::{spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprPath, Lit};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum TagType {
|
||||
@@ -427,14 +427,14 @@ fn element_to_tokens_ssr(
|
||||
{#component}.into_view(#cx)
|
||||
}));
|
||||
} else {
|
||||
let tag_name = node
|
||||
.name()
|
||||
.to_string()
|
||||
.replace("svg::", "")
|
||||
.replace("math::", "");
|
||||
let tag_name = node.name().to_string();
|
||||
let tag_name = tag_name
|
||||
.trim_start_matches("svg::")
|
||||
.trim_start_matches("math::")
|
||||
.trim_end_matches('_');
|
||||
let is_script_or_style = tag_name == "script" || tag_name == "style";
|
||||
template.push('<');
|
||||
template.push_str(&tag_name);
|
||||
template.push_str(tag_name);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
stmts_for_ide.save_element_completion(node);
|
||||
@@ -1964,41 +1964,39 @@ fn fancy_class_name<'a>(
|
||||
// special case for complex class names:
|
||||
// e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)`
|
||||
if name == "class" {
|
||||
if let Some(expr) = node.value() {
|
||||
if let syn::Expr::Tuple(tuple) = expr {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let class = quote_spanned! {
|
||||
span => .class
|
||||
};
|
||||
let class_name = &tuple.elems[0];
|
||||
let class_name = if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) = class_name
|
||||
{
|
||||
s.value()
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
class_name.span(),
|
||||
"class name must be a string literal"
|
||||
);
|
||||
Default::default()
|
||||
};
|
||||
let value = &tuple.elems[1];
|
||||
return Some((
|
||||
quote! {
|
||||
#class(#class_name, (#cx, #value))
|
||||
},
|
||||
class_name,
|
||||
value,
|
||||
));
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let class = quote_spanned! {
|
||||
span => .class
|
||||
};
|
||||
let class_name = &tuple.elems[0];
|
||||
let class_name = if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) = class_name
|
||||
{
|
||||
s.value()
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
tuple.span(),
|
||||
"class tuples must have two elements."
|
||||
)
|
||||
}
|
||||
class_name.span(),
|
||||
"class name must be a string literal"
|
||||
);
|
||||
Default::default()
|
||||
};
|
||||
let value = &tuple.elems[1];
|
||||
return Some((
|
||||
quote! {
|
||||
#class(#class_name, (#cx, #value))
|
||||
},
|
||||
class_name,
|
||||
value,
|
||||
));
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
tuple.span(),
|
||||
"class tuples must have two elements."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2012,41 +2010,39 @@ fn fancy_style_name<'a>(
|
||||
) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex dynamic style names:
|
||||
if name == "style" {
|
||||
if let Some(expr) = node.value() {
|
||||
if let syn::Expr::Tuple(tuple) = expr {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let style = quote_spanned! {
|
||||
span => .style
|
||||
};
|
||||
let style_name = &tuple.elems[0];
|
||||
let style_name = if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) = style_name
|
||||
{
|
||||
s.value()
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
style_name.span(),
|
||||
"style name must be a string literal"
|
||||
);
|
||||
Default::default()
|
||||
};
|
||||
let value = &tuple.elems[1];
|
||||
return Some((
|
||||
quote! {
|
||||
#style(#style_name, (#cx, #value))
|
||||
},
|
||||
style_name,
|
||||
value,
|
||||
));
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let style = quote_spanned! {
|
||||
span => .style
|
||||
};
|
||||
let style_name = &tuple.elems[0];
|
||||
let style_name = if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) = style_name
|
||||
{
|
||||
s.value()
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
tuple.span(),
|
||||
"style tuples must have two elements."
|
||||
)
|
||||
}
|
||||
style_name.span(),
|
||||
"style name must be a string literal"
|
||||
);
|
||||
Default::default()
|
||||
};
|
||||
let value = &tuple.elems[1];
|
||||
return Some((
|
||||
quote! {
|
||||
#style(#style_name, (#cx, #value))
|
||||
},
|
||||
style_name,
|
||||
value,
|
||||
));
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
tuple.span(),
|
||||
"style tuples must have two elements."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ indexmap = "1"
|
||||
self_cell = "1.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.4.0", features = ["html_reports"] }
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
reactive-signals = { version = "0.1.0-alpha.4", features = ["profile"] }
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
|
||||
@@ -96,6 +96,7 @@ mod trigger;
|
||||
pub use context::*;
|
||||
pub use diagnostics::SpecialNonReactiveZone;
|
||||
pub use effect::*;
|
||||
pub use hydration::FragmentData;
|
||||
pub use memo::*;
|
||||
pub use resource::*;
|
||||
use runtime::*;
|
||||
|
||||
@@ -7,6 +7,16 @@ use crate::{
|
||||
};
|
||||
use std::{any::Any, cell::RefCell, fmt, marker::PhantomData, rc::Rc};
|
||||
|
||||
// IMPLEMENTATION NOTE:
|
||||
// Memos are implemented "lazily," i.e., the inner computation is not run
|
||||
// when the memo is created or when its value is marked as stale, but on demand
|
||||
// when it is accessed, if the value is stale. This means that the value is stored
|
||||
// internally as Option<T>, even though it can always be accessed by the user as T.
|
||||
// This means the inner value can be unwrapped in circumstances in which we know
|
||||
// `Runtime::update_if_necessary()` has already been called, e.g., in the
|
||||
// `.try_with_no_subscription()` calls below that are unwrapped with
|
||||
// `.expect("invariant: must have already been initialized")`.
|
||||
|
||||
/// Creates an efficient derived reactive value based on other reactive values.
|
||||
///
|
||||
/// Unlike a "derived signal," a memo comes with two guarantees:
|
||||
@@ -199,6 +209,17 @@ impl<T> PartialEq for Memo<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn forward_ref_to<T, O, F: FnOnce(&T) -> O>(
|
||||
f: F,
|
||||
) -> impl FnOnce(&Option<T>) -> O {
|
||||
|maybe_value: &Option<T>| {
|
||||
let ref_t = maybe_value
|
||||
.as_ref()
|
||||
.expect("invariant: must have already been initialized");
|
||||
f(ref_t)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
@@ -215,7 +236,11 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
)]
|
||||
fn get_untracked(&self) -> T {
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
let f = move |maybe_value: &Option<T>| maybe_value.clone().unwrap();
|
||||
let f = |maybe_value: &Option<T>| {
|
||||
maybe_value
|
||||
.clone()
|
||||
.expect("invariant: must have already been initialized")
|
||||
};
|
||||
match self.id.try_with_no_subscription(runtime, f) {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic_getting_dead_memo(
|
||||
@@ -261,10 +286,8 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
||||
)
|
||||
)]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
// Unwrapping here is fine for the same reasons as <Memo as
|
||||
// UntrackedSignal>::get_untracked
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
match self.id.try_with_no_subscription(runtime, |v: &T| f(v)) {
|
||||
match self.id.try_with_no_subscription(runtime, forward_ref_to(f)) {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic_getting_dead_memo(
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
@@ -394,15 +417,13 @@ 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, diagnostics);
|
||||
self.id.try_with_no_subscription(runtime, f).ok()
|
||||
self.id
|
||||
.try_with_no_subscription(runtime, forward_ref_to(f))
|
||||
.ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
|
||||
@@ -13,6 +13,32 @@ fn basic_memo() {
|
||||
.dispose()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn signal_with_untracked() {
|
||||
use leptos_reactive::SignalWithUntracked;
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let m = create_memo(cx, move |_| 5);
|
||||
let copied_out = m.with_untracked(|value| *value);
|
||||
assert_eq!(copied_out, 5);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn signal_get_untracked() {
|
||||
use leptos_reactive::SignalGetUntracked;
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let m = create_memo(cx, move |_| "memo".to_owned());
|
||||
let cloned_out = m.get_untracked();
|
||||
assert_eq!(cloned_out, "memo".to_owned());
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn memo_with_computed_value() {
|
||||
|
||||
@@ -3,7 +3,7 @@ use leptos_reactive::{
|
||||
create_rw_signal, signal_prelude::*, spawn_local, store_value, ReadSignal,
|
||||
RwSignal, Scope, StoredValue,
|
||||
};
|
||||
use std::{future::Future, pin::Pin, rc::Rc};
|
||||
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
|
||||
|
||||
/// An action synchronizes an imperative `async` call to the synchronous reactive system.
|
||||
///
|
||||
@@ -106,13 +106,30 @@ where
|
||||
self.0.with_value(|a| a.pending.read_only())
|
||||
}
|
||||
|
||||
/// Updates whether the action is currently pending.
|
||||
/// Updates whether the action is currently pending. If the action has been dispatched
|
||||
/// multiple times, and some of them are still pending, it will *not* update the `pending`
|
||||
/// signal.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn set_pending(&self, pending: bool) {
|
||||
self.0.try_with_value(|a| a.pending.set(pending));
|
||||
self.0.try_with_value(|a| {
|
||||
let pending_dispatches = &a.pending_dispatches;
|
||||
let still_pending = {
|
||||
pending_dispatches.set(if pending {
|
||||
pending_dispatches.get().wrapping_add(1)
|
||||
} else {
|
||||
pending_dispatches.get().saturating_sub(1)
|
||||
});
|
||||
pending_dispatches.get()
|
||||
};
|
||||
if still_pending == 0 {
|
||||
a.pending.set(false);
|
||||
} else {
|
||||
a.pending.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The URL associated with the action (typically as part of a server function.)
|
||||
@@ -186,6 +203,7 @@ where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
cx: Scope,
|
||||
/// How many times the action has successfully resolved.
|
||||
pub version: RwSignal<usize>,
|
||||
/// The current argument that was dispatched to the `async` function.
|
||||
@@ -195,6 +213,8 @@ where
|
||||
pub value: RwSignal<Option<O>>,
|
||||
pending: RwSignal<bool>,
|
||||
url: Option<String>,
|
||||
/// How many dispatched actions are still pending.
|
||||
pending_dispatches: Rc<Cell<usize>>,
|
||||
#[allow(clippy::complexity)]
|
||||
action_fn: Rc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
|
||||
}
|
||||
@@ -215,14 +235,23 @@ where
|
||||
let input = self.input;
|
||||
let version = self.version;
|
||||
let pending = self.pending;
|
||||
let pending_dispatches = Rc::clone(&self.pending_dispatches);
|
||||
let value = self.value;
|
||||
let cx = self.cx;
|
||||
pending.set(true);
|
||||
pending_dispatches.set(pending_dispatches.get().saturating_sub(1));
|
||||
spawn_local(async move {
|
||||
let new_value = fut.await;
|
||||
value.set(Some(new_value));
|
||||
input.set(None);
|
||||
pending.set(false);
|
||||
version.update(|n| *n += 1);
|
||||
cx.batch(move || {
|
||||
value.set(Some(new_value));
|
||||
input.set(None);
|
||||
version.update(|n| *n += 1);
|
||||
pending_dispatches
|
||||
.set(pending_dispatches.get().saturating_sub(1));
|
||||
if pending_dispatches.get() == 0 {
|
||||
pending.set(false);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -316,6 +345,7 @@ where
|
||||
let input = create_rw_signal(cx, None);
|
||||
let value = create_rw_signal(cx, None);
|
||||
let pending = create_rw_signal(cx, false);
|
||||
let pending_dispatches = Rc::new(Cell::new(0));
|
||||
let action_fn = Rc::new(move |input: &I| {
|
||||
let fut = action_fn(input);
|
||||
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
|
||||
@@ -324,11 +354,13 @@ where
|
||||
Action(store_value(
|
||||
cx,
|
||||
ActionState {
|
||||
cx,
|
||||
version,
|
||||
url: None,
|
||||
input,
|
||||
value,
|
||||
pending,
|
||||
pending_dispatches,
|
||||
action_fn,
|
||||
},
|
||||
))
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
//! of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]`
|
||||
//! macro to specify an alternate encoding:
|
||||
//!
|
||||
//! ```rust
|
||||
//! ```rust,ignore
|
||||
//! #[server(AddTodo, "/api", "Url")]
|
||||
//! #[server(AddTodo, "/api", "GetJson")]
|
||||
//! #[server(AddTodo, "/api", "Cbor")]
|
||||
@@ -116,7 +116,9 @@
|
||||
//! your app is not available.
|
||||
|
||||
use leptos_reactive::*;
|
||||
pub use server_fn::{Encoding, Payload, ServerFnError};
|
||||
pub use server_fn::{
|
||||
error::ServerFnErrorErr, Encoding, Payload, ServerFnError,
|
||||
};
|
||||
|
||||
mod action;
|
||||
mod multi_action;
|
||||
@@ -161,7 +163,7 @@ impl ServerFnTraitObj {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ssr"))]
|
||||
#[cfg(feature = "ssr")]
|
||||
inventory::collect!(ServerFnTraitObj);
|
||||
|
||||
#[allow(unused)]
|
||||
|
||||
@@ -10,7 +10,7 @@ description = "Router for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
leptos = { workspace = true }
|
||||
cached = { version = "0.43.0", optional = true }
|
||||
cached = { version = "0.44.0", optional = true }
|
||||
cfg-if = "1"
|
||||
common_macros = "0.1"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
@@ -8,6 +8,7 @@ use web_sys::RequestRedirect;
|
||||
|
||||
type OnFormData = Rc<dyn Fn(&web_sys::FormData)>;
|
||||
type OnResponse = Rc<dyn Fn(&web_sys::Response)>;
|
||||
type OnError = Rc<dyn Fn(&gloo_net::Error)>;
|
||||
|
||||
/// An HTML [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) progressively
|
||||
/// enhanced to use client-side routing.
|
||||
@@ -47,6 +48,9 @@ pub fn Form<A>(
|
||||
/// to a form submission.
|
||||
#[prop(optional)]
|
||||
on_response: Option<OnResponse>,
|
||||
/// A callback will be called if the attempt to submit the form results in an error.
|
||||
#[prop(optional)]
|
||||
on_error: Option<OnError>,
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
#[prop(optional)]
|
||||
node_ref: Option<NodeRef<html::Form>>,
|
||||
@@ -68,196 +72,216 @@ where
|
||||
error: Option<RwSignal<Option<Box<dyn Error>>>>,
|
||||
on_form_data: Option<OnFormData>,
|
||||
on_response: Option<OnResponse>,
|
||||
on_error: Option<OnError>,
|
||||
class: Option<Attribute>,
|
||||
children: Children,
|
||||
node_ref: Option<NodeRef<html::Form>>,
|
||||
attributes: Option<MaybeSignal<AdditionalAttributes>>,
|
||||
) -> HtmlElement<html::Form> {
|
||||
let action_version = version;
|
||||
let on_submit = move |ev: web_sys::SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
let navigate = use_navigate(cx);
|
||||
let on_submit = {
|
||||
move |ev: web_sys::SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
let navigate = use_navigate(cx);
|
||||
|
||||
let (form, method, action, enctype) = extract_form_attributes(&ev);
|
||||
let (form, method, action, enctype) =
|
||||
extract_form_attributes(&ev);
|
||||
|
||||
let form_data =
|
||||
web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
if let Some(on_form_data) = on_form_data.clone() {
|
||||
on_form_data(&form_data);
|
||||
}
|
||||
let params =
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(
|
||||
&form_data,
|
||||
)
|
||||
.unwrap_throw();
|
||||
let action = use_resolved_path(cx, move || action.clone())
|
||||
.get_untracked()
|
||||
.unwrap_or_default();
|
||||
// multipart POST (setting Context-Type breaks the request)
|
||||
if method == "post" && enctype == "multipart/form-data" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
spawn_local(async move {
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.redirect(RequestRedirect::Follow)
|
||||
.body(form_data)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(error) = error {
|
||||
error.set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
// Check all the logical 3xx responses that might
|
||||
// get returned from a server function
|
||||
if resp.redirected() {
|
||||
let resp_url = &resp.url();
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= window()
|
||||
.location()
|
||||
.origin()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
.set_href(resp_url.as_str());
|
||||
} else {
|
||||
request_animation_frame(
|
||||
move || {
|
||||
if let Err(e) = navigate(
|
||||
&format!(
|
||||
"{}{}{}",
|
||||
url.pathname,
|
||||
if url
|
||||
.search
|
||||
.is_empty()
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
Default::default(),
|
||||
) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// POST
|
||||
else if method == "post" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
spawn_local(async move {
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", &enctype)
|
||||
.redirect(RequestRedirect::Follow)
|
||||
.body(params)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(error) = error {
|
||||
error.set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
// Check all the logical 3xx responses that might
|
||||
// get returned from a server function
|
||||
if resp.redirected() {
|
||||
let resp_url = &resp.url();
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= window()
|
||||
.location()
|
||||
.hostname()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
.set_href(resp_url.as_str());
|
||||
} else {
|
||||
request_animation_frame(
|
||||
move || {
|
||||
if let Err(e) = navigate(
|
||||
&format!(
|
||||
"{}{}{}",
|
||||
url.pathname,
|
||||
if url
|
||||
.search
|
||||
.is_empty()
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
Default::default(),
|
||||
) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// otherwise, GET
|
||||
else {
|
||||
let params = params.to_string().as_string().unwrap_or_default();
|
||||
if navigate(&format!("{action}?{params}"), Default::default())
|
||||
.is_ok()
|
||||
{
|
||||
let form_data =
|
||||
web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
if let Some(on_form_data) = on_form_data.clone() {
|
||||
on_form_data(&form_data);
|
||||
}
|
||||
let params =
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(
|
||||
&form_data,
|
||||
)
|
||||
.unwrap_throw();
|
||||
let action = use_resolved_path(cx, move || action.clone())
|
||||
.get_untracked()
|
||||
.unwrap_or_default();
|
||||
// multipart POST (setting Context-Type breaks the request)
|
||||
if method == "post" && enctype == "multipart/form-data" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
let on_error = on_error.clone();
|
||||
spawn_local(async move {
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.redirect(RequestRedirect::Follow)
|
||||
.body(form_data)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(on_error) = on_error {
|
||||
on_error(&e);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
// Check all the logical 3xx responses that might
|
||||
// get returned from a server function
|
||||
if resp.redirected() {
|
||||
let resp_url = &resp.url();
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= window()
|
||||
.location()
|
||||
.origin()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
.set_href(
|
||||
resp_url.as_str(),
|
||||
);
|
||||
} else {
|
||||
request_animation_frame(
|
||||
move || {
|
||||
if let Err(e) = navigate(
|
||||
&format!(
|
||||
"{}{}{}",
|
||||
url.pathname,
|
||||
if url
|
||||
.search
|
||||
.is_empty()
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
Default::default(),
|
||||
) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// POST
|
||||
else if method == "post" {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
let on_error = on_error.clone();
|
||||
spawn_local(async move {
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", &enctype)
|
||||
.redirect(RequestRedirect::Follow)
|
||||
.body(params)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(on_error) = on_error {
|
||||
on_error(&e);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.try_set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
// Check all the logical 3xx responses that might
|
||||
// get returned from a server function
|
||||
if resp.redirected() {
|
||||
let resp_url = &resp.url();
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= window()
|
||||
.location()
|
||||
.hostname()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
.set_href(
|
||||
resp_url.as_str(),
|
||||
);
|
||||
} else {
|
||||
request_animation_frame(
|
||||
move || {
|
||||
if let Err(e) = navigate(
|
||||
&format!(
|
||||
"{}{}{}",
|
||||
url.pathname,
|
||||
if url
|
||||
.search
|
||||
.is_empty()
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
url.search,
|
||||
),
|
||||
Default::default(),
|
||||
) {
|
||||
warn!("{}", e);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// otherwise, GET
|
||||
else {
|
||||
let params =
|
||||
params.to_string().as_string().unwrap_or_default();
|
||||
if navigate(
|
||||
&format!("{action}?{params}"),
|
||||
Default::default(),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -296,6 +320,7 @@ where
|
||||
error,
|
||||
on_form_data,
|
||||
on_response,
|
||||
on_error,
|
||||
class,
|
||||
children,
|
||||
node_ref,
|
||||
@@ -355,14 +380,37 @@ where
|
||||
let value = action.value();
|
||||
let input = action.input();
|
||||
|
||||
let on_error = Rc::new(move |e: &gloo_net::Error| {
|
||||
cx.batch(move || {
|
||||
action.set_pending(false);
|
||||
let e = ServerFnError::Request(e.to_string());
|
||||
value.try_set(Some(Err(e.clone())));
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(ServerFnErrorErr::from(e))));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let on_form_data = Rc::new(move |form_data: &web_sys::FormData| {
|
||||
let data = I::from_form_data(form_data);
|
||||
match data {
|
||||
Ok(data) => {
|
||||
input.set(Some(data));
|
||||
action.set_pending(true);
|
||||
cx.batch(move || {
|
||||
input.try_set(Some(data));
|
||||
action.set_pending(true);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
let e = ServerFnError::Serialization(e.to_string());
|
||||
cx.batch(move || {
|
||||
value.try_set(Some(Err(e.clone())));
|
||||
if let Some(error) = error {
|
||||
error
|
||||
.try_set(Some(Box::new(ServerFnErrorErr::from(e))));
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => error!("{e}"),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -370,7 +418,6 @@ where
|
||||
let resp = resp.clone().expect("couldn't get Response");
|
||||
spawn_local(async move {
|
||||
let redirected = resp.redirected();
|
||||
|
||||
if !redirected {
|
||||
let body = JsFuture::from(
|
||||
resp.text().expect("couldn't get .text() from Response"),
|
||||
@@ -426,7 +473,7 @@ where
|
||||
error!("{e:?}");
|
||||
if let Some(error) = error {
|
||||
error.try_set(Some(Box::new(
|
||||
ServerFnError::Request(
|
||||
ServerFnErrorErr::Request(
|
||||
e.as_string().unwrap_or_default(),
|
||||
),
|
||||
)));
|
||||
@@ -434,8 +481,10 @@ where
|
||||
}
|
||||
};
|
||||
}
|
||||
input.try_set(None);
|
||||
action.set_pending(false);
|
||||
cx.batch(move || {
|
||||
input.try_set(None);
|
||||
action.set_pending(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
let class = class.map(|bx| bx.into_attribute_boxed(cx));
|
||||
@@ -444,6 +493,7 @@ where
|
||||
.version(version)
|
||||
.on_form_data(on_form_data)
|
||||
.on_response(on_response)
|
||||
.on_error(on_error)
|
||||
.method("post")
|
||||
.class(class)
|
||||
.children(children)
|
||||
@@ -507,7 +557,7 @@ where
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
if let Some(error) = error {
|
||||
error.set(Some(Box::new(e)));
|
||||
error.try_set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(input) => {
|
||||
@@ -515,7 +565,7 @@ where
|
||||
ev.stop_propagation();
|
||||
multi_action.dispatch(input);
|
||||
if let Some(error) = error {
|
||||
error.set(None);
|
||||
error.try_set(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -692,6 +742,6 @@ where
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
|
||||
.unwrap_throw();
|
||||
let data = data.to_string().as_string().unwrap_or_default();
|
||||
serde_qs::from_str::<Self>(&data)
|
||||
serde_qs::Config::new(5, false).deserialize_str::<Self>(&data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,9 +278,12 @@ impl RouterContextInner {
|
||||
let global_suspense =
|
||||
expect_context::<GlobalSuspenseContext>(cx);
|
||||
let path_stack = self.path_stack;
|
||||
path_stack.update_value(|stack| {
|
||||
stack.push(resolved_to.clone())
|
||||
});
|
||||
let is_navigating_back = self.is_back.get_untracked();
|
||||
if !is_navigating_back {
|
||||
path_stack.update_value(|stack| {
|
||||
stack.push(resolved_to.clone())
|
||||
});
|
||||
}
|
||||
|
||||
let set_is_routing = use_context::<SetIsRouting>(cx);
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
|
||||
@@ -64,7 +64,9 @@ impl History for BrowserIntegration {
|
||||
|
||||
let is_navigating_back = path_stack.with_value(|stack| {
|
||||
stack.len() == 1
|
||||
|| stack.get(stack.len() - 2) == Some(&change.value)
|
||||
|| (stack.len() >= 2
|
||||
&& stack.get(stack.len() - 2)
|
||||
== Some(&change.value))
|
||||
});
|
||||
if is_navigating_back {
|
||||
path_stack.update_value(|stack| {
|
||||
|
||||
@@ -18,7 +18,7 @@ lazy_static::lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ssr"))]
|
||||
#[cfg(feature = "ssr")]
|
||||
inventory::collect!(DefaultServerFnTraitObj);
|
||||
|
||||
/// Attempts to find a server function registered at the given path.
|
||||
|
||||
167
server_fn/src/error.rs
Normal file
167
server_fn/src/error.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{error, fmt, ops, sync::Arc};
|
||||
use thiserror::Error;
|
||||
|
||||
/// This is a result type into which any error can be converted,
|
||||
/// and which can be used directly in your `view`.
|
||||
///
|
||||
/// All errors will be stored as [`Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct Error(Arc<dyn error::Error + Send + Sync>);
|
||||
|
||||
impl Error {
|
||||
/// Converts the wrapper into the inner reference-counted error.
|
||||
pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> {
|
||||
Arc::clone(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Error {
|
||||
type Target = Arc<dyn error::Error + Send + Sync>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Error
|
||||
where
|
||||
T: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Error(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerFnError> for Error {
|
||||
fn from(e: ServerFnError) -> Self {
|
||||
Error(Arc::new(ServerFnErrorErr::from(e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for errors that can occur when using server functions.
|
||||
///
|
||||
/// Unlike [`ServerFnErrorErr`], this does not implement [`std::error::Error`].
|
||||
/// This means that other error types can easily be converted into it using the
|
||||
/// `?` operator.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServerFnError {
|
||||
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
|
||||
Registration(String),
|
||||
/// Occurs on the client if there is a network error while trying to run function on server.
|
||||
Request(String),
|
||||
/// Occurs when there is an error while actually running the function on the server.
|
||||
ServerError(String),
|
||||
/// Occurs on the client if there is an error deserializing the server's response.
|
||||
Deserialization(String),
|
||||
/// Occurs on the client if there is an error serializing the server function arguments.
|
||||
Serialization(String),
|
||||
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
|
||||
Args(String),
|
||||
/// Occurs on the server if there's a missing argument.
|
||||
MissingArg(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServerFnError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
ServerFnError::Registration(s) => format!(
|
||||
"error while trying to register the server function: {s}"
|
||||
),
|
||||
ServerFnError::Request(s) => format!(
|
||||
"error reaching server to call server function: {s}"
|
||||
),
|
||||
ServerFnError::ServerError(s) =>
|
||||
format!("error running server function: {s}"),
|
||||
ServerFnError::Deserialization(s) =>
|
||||
format!("error deserializing server function results: {s}"),
|
||||
ServerFnError::Serialization(s) =>
|
||||
format!("error serializing server function arguments: {s}"),
|
||||
ServerFnError::Args(s) => format!(
|
||||
"error deserializing server function arguments: {s}"
|
||||
),
|
||||
ServerFnError::MissingArg(s) => format!("missing argument {s}"),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<E> for ServerFnError
|
||||
where
|
||||
E: std::error::Error,
|
||||
{
|
||||
fn from(e: E) -> Self {
|
||||
ServerFnError::ServerError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for errors that can occur when using server functions.
|
||||
///
|
||||
/// Unlike [`ServerFnErrorErr`], this implements [`std::error::Error`]. This means
|
||||
/// it can be used in situations in which the `Error` trait is required, but it’s
|
||||
/// not possible to create a blanket implementation that converts other errors into
|
||||
/// this type.
|
||||
///
|
||||
/// [`ServerFnError`] and [`ServerFnErrorErr`] mutually implement [`From`], so
|
||||
/// it is easy to convert between the two types.
|
||||
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServerFnErrorErr {
|
||||
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
|
||||
#[error("error while trying to register the server function: {0}")]
|
||||
Registration(String),
|
||||
/// Occurs on the client if there is a network error while trying to run function on server.
|
||||
#[error("error reaching server to call server function: {0}")]
|
||||
Request(String),
|
||||
/// Occurs when there is an error while actually running the function on the server.
|
||||
#[error("error running server function: {0}")]
|
||||
ServerError(String),
|
||||
/// Occurs on the client if there is an error deserializing the server's response.
|
||||
#[error("error deserializing server function results: {0}")]
|
||||
Deserialization(String),
|
||||
/// Occurs on the client if there is an error serializing the server function arguments.
|
||||
#[error("error serializing server function arguments: {0}")]
|
||||
Serialization(String),
|
||||
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
|
||||
#[error("error deserializing server function arguments: {0}")]
|
||||
Args(String),
|
||||
/// Occurs on the server if there's a missing argument.
|
||||
#[error("missing argument {0}")]
|
||||
MissingArg(String),
|
||||
}
|
||||
|
||||
impl From<ServerFnError> for ServerFnErrorErr {
|
||||
fn from(value: ServerFnError) -> Self {
|
||||
match value {
|
||||
ServerFnError::Registration(value) => {
|
||||
ServerFnErrorErr::Registration(value)
|
||||
}
|
||||
ServerFnError::Request(value) => ServerFnErrorErr::Request(value),
|
||||
ServerFnError::ServerError(value) => {
|
||||
ServerFnErrorErr::ServerError(value)
|
||||
}
|
||||
ServerFnError::Deserialization(value) => {
|
||||
ServerFnErrorErr::Deserialization(value)
|
||||
}
|
||||
ServerFnError::Serialization(value) => {
|
||||
ServerFnErrorErr::Serialization(value)
|
||||
}
|
||||
ServerFnError::Args(value) => ServerFnErrorErr::Args(value),
|
||||
ServerFnError::MissingArg(value) => {
|
||||
ServerFnErrorErr::MissingArg(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@
|
||||
#[doc(hidden)]
|
||||
pub use const_format;
|
||||
// used by the macro
|
||||
#[cfg(any(feature = "ssr"))]
|
||||
#[cfg(feature = "ssr")]
|
||||
#[doc(hidden)]
|
||||
pub use inventory;
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
@@ -90,15 +90,17 @@ use quote::TokenStreamExt;
|
||||
// used by the macro
|
||||
#[doc(hidden)]
|
||||
pub use serde;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
pub use server_fn_macro_default::server;
|
||||
use std::{future::Future, pin::Pin, str::FromStr};
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
use syn::parse_quote;
|
||||
use thiserror::Error;
|
||||
// used by the macro
|
||||
#[doc(hidden)]
|
||||
pub use xxhash_rust;
|
||||
/// Error types used in server functions.
|
||||
pub mod error;
|
||||
pub use error::ServerFnError;
|
||||
|
||||
/// Default server function registry
|
||||
pub mod default;
|
||||
@@ -375,7 +377,8 @@ where
|
||||
// decode the args
|
||||
let value = match Self::encoding() {
|
||||
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => {
|
||||
serde_qs::from_bytes(data)
|
||||
serde_qs::Config::new(5, false)
|
||||
.deserialize_bytes(data)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
Encoding::Cbor => ciborium::de::from_reader(data)
|
||||
@@ -450,32 +453,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for errors that can occur when using server functions.
|
||||
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServerFnError {
|
||||
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
|
||||
#[error("error while trying to register the server function: {0}")]
|
||||
Registration(String),
|
||||
/// Occurs on the client if there is a network error while trying to run function on server.
|
||||
#[error("error reaching server to call server function: {0}")]
|
||||
Request(String),
|
||||
/// Occurs when there is an error while actually running the function on the server.
|
||||
#[error("error running server function: {0}")]
|
||||
ServerError(String),
|
||||
/// Occurs on the client if there is an error deserializing the server's response.
|
||||
#[error("error deserializing server function results {0}")]
|
||||
Deserialization(String),
|
||||
/// Occurs on the client if there is an error serializing the server function arguments.
|
||||
#[error("error serializing server function arguments {0}")]
|
||||
Serialization(String),
|
||||
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
|
||||
#[error("error deserializing server function arguments {0}")]
|
||||
Args(String),
|
||||
/// Occurs on the server if there's a missing argument.
|
||||
#[error("missing argument {0}")]
|
||||
MissingArg(String),
|
||||
}
|
||||
|
||||
/// Executes the HTTP call to call a server function from the client, given its URL and argument type.
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub async fn call_server_fn<T, C: 'static>(
|
||||
@@ -648,7 +625,7 @@ where
|
||||
// Lazily initialize the client to be reused for all server function calls.
|
||||
#[cfg(any(all(not(feature = "ssr"), not(target_arch = "wasm32")), doc))]
|
||||
static CLIENT: once_cell::sync::Lazy<reqwest::Client> =
|
||||
once_cell::sync::Lazy::new(|| reqwest::Client::new());
|
||||
once_cell::sync::Lazy::new(reqwest::Client::new);
|
||||
|
||||
#[cfg(any(all(not(feature = "ssr"), not(target_arch = "wasm32")), doc))]
|
||||
static ROOT_URL: once_cell::sync::OnceCell<&'static str> =
|
||||
|
||||
Reference in New Issue
Block a user