mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 11:21:55 -05:00
Compare commits
274 Commits
fix-error-
...
v0.5.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e03c00a36 | ||
|
|
809652474a | ||
|
|
36672c07b8 | ||
|
|
f3498ddbf5 | ||
|
|
c5122865a3 | ||
|
|
406403dda9 | ||
|
|
f88085ad1a | ||
|
|
bcce4d116e | ||
|
|
b898e2f309 | ||
|
|
a230157d54 | ||
|
|
9a365668b7 | ||
|
|
4a2a3cc213 | ||
|
|
97bdbde2d6 | ||
|
|
d8f9678619 | ||
|
|
85f567dd87 | ||
|
|
030f0a971a | ||
|
|
df4bc0a533 | ||
|
|
8a6d4bea89 | ||
|
|
e07eff19da | ||
|
|
0de52d6ef4 | ||
|
|
924ab445fb | ||
|
|
f1634db7d5 | ||
|
|
5c0011d99b | ||
|
|
213d8ec0a9 | ||
|
|
fd3310c06f | ||
|
|
901cf87ef0 | ||
|
|
06ff34d8d2 | ||
|
|
a128f1e458 | ||
|
|
ea637a446f | ||
|
|
7c5c921a59 | ||
|
|
f157a6e9ec | ||
|
|
e16f2bd720 | ||
|
|
4f15c3ec69 | ||
|
|
a2e13ed73e | ||
|
|
b4de4bb114 | ||
|
|
abd118d1bb | ||
|
|
f222d3bbc6 | ||
|
|
ca00b84aea | ||
|
|
9c90f06438 | ||
|
|
d727aed5d6 | ||
|
|
febf1a11a5 | ||
|
|
fcea83f718 | ||
|
|
9aa8b6705e | ||
|
|
6bf2a0a8c5 | ||
|
|
67fd08d5ab | ||
|
|
96cef1a48a | ||
|
|
8602ca3a6f | ||
|
|
a6eab0ec3c | ||
|
|
4c8f9bf834 | ||
|
|
d56cdc792f | ||
|
|
e352ed5d0d | ||
|
|
7fa104efbc | ||
|
|
a516cc8b71 | ||
|
|
8af0481936 | ||
|
|
6310244d69 | ||
|
|
8997c2f420 | ||
|
|
5b60d32464 | ||
|
|
fd69248cd5 | ||
|
|
c97c300656 | ||
|
|
be8390b77c | ||
|
|
587abfc299 | ||
|
|
fb62fb2707 | ||
|
|
ec7b986cfb | ||
|
|
e0349c102d | ||
|
|
a67cca986e | ||
|
|
559ebbfc4a | ||
|
|
36ce979258 | ||
|
|
0d55c61b0a | ||
|
|
7dba8f29d1 | ||
|
|
b81cb24ded | ||
|
|
0698b38280 | ||
|
|
5592174152 | ||
|
|
1441bf9532 | ||
|
|
7af4235247 | ||
|
|
07718e9c24 | ||
|
|
34f522291c | ||
|
|
bbcde97315 | ||
|
|
a98752240a | ||
|
|
4130ecb782 | ||
|
|
b417960dfb | ||
|
|
3d6c7022dd | ||
|
|
201a302cc5 | ||
|
|
1e13ad8fee | ||
|
|
e0c9a9523a | ||
|
|
0726a3034d | ||
|
|
a88d047eff | ||
|
|
4001561987 | ||
|
|
2f860b37bd | ||
|
|
b86009b9d0 | ||
|
|
54733e1b34 | ||
|
|
56f01888b7 | ||
|
|
8320f16716 | ||
|
|
0b16e5992d | ||
|
|
248beb4a55 | ||
|
|
c9f608d030 | ||
|
|
f837d3e6a2 | ||
|
|
8847d5fc42 | ||
|
|
7819a6fac0 | ||
|
|
c199185808 | ||
|
|
e0b5738606 | ||
|
|
f3e3880a57 | ||
|
|
d44b90c16d | ||
|
|
cc32a3e863 | ||
|
|
5740c9b76b | ||
|
|
80fa6ad3eb | ||
|
|
7bc1ad2b4f | ||
|
|
82a2fe7cbe | ||
|
|
40bf944957 | ||
|
|
7ef7546fa9 | ||
|
|
5e26e84d77 | ||
|
|
e67bc2083a | ||
|
|
a3cb3f7f77 | ||
|
|
daeb47e72e | ||
|
|
8c5ab99fa7 | ||
|
|
984a7388f1 | ||
|
|
274b105676 | ||
|
|
a689d1b4c0 | ||
|
|
1581e91317 | ||
|
|
20f4034c1c | ||
|
|
9fb1c4b67c | ||
|
|
2e559d6a06 | ||
|
|
71de6c395b | ||
|
|
b09f9e4814 | ||
|
|
ec4bfb0e8a | ||
|
|
39bf38d1e4 | ||
|
|
e6fd1379b8 | ||
|
|
1d9931a5a8 | ||
|
|
06164d34b5 | ||
|
|
f3de288e19 | ||
|
|
62bf315059 | ||
|
|
011c97e3a4 | ||
|
|
2ca3d2c7a4 | ||
|
|
cc52c94348 | ||
|
|
4b8cc96dfa | ||
|
|
338d2ab839 | ||
|
|
54fc6da24e | ||
|
|
825b3fb858 | ||
|
|
fd0212a142 | ||
|
|
3b397cb39c | ||
|
|
1e002c2c2f | ||
|
|
8f45daeca8 | ||
|
|
105ef989b7 | ||
|
|
9e7c31d1e4 | ||
|
|
771dfa6b68 | ||
|
|
fb52cfa73e | ||
|
|
b2c75d215b | ||
|
|
951607de74 | ||
|
|
122fd2bc74 | ||
|
|
f102125d3c | ||
|
|
14bda76b30 | ||
|
|
3af115a663 | ||
|
|
a344804734 | ||
|
|
c1c49ce53b | ||
|
|
d8eaa5c004 | ||
|
|
e8aa9b24f1 | ||
|
|
3036cd223e | ||
|
|
1ae5150b08 | ||
|
|
47148f2033 | ||
|
|
4d4d15436b | ||
|
|
7f4741b3a3 | ||
|
|
85644a7c1c | ||
|
|
f40ae6af30 | ||
|
|
708e1a5aab | ||
|
|
55613c9a31 | ||
|
|
e6590c7d31 | ||
|
|
5af2f4e98d | ||
|
|
8e68699435 | ||
|
|
77580401da | ||
|
|
7902e7edb7 | ||
|
|
4ad223277d | ||
|
|
6f5da11c72 | ||
|
|
3eed86fbf3 | ||
|
|
10d51a854a | ||
|
|
6c60bad757 | ||
|
|
79f666b5da | ||
|
|
3ea3a40395 | ||
|
|
193aa79956 | ||
|
|
3481a6ee53 | ||
|
|
d1ef5fce9f | ||
|
|
5d48911f01 | ||
|
|
8a90f97959 | ||
|
|
e9665b34e5 | ||
|
|
d4b1ceda90 | ||
|
|
a0fae88f7d | ||
|
|
03a8609680 | ||
|
|
3e40f9cc66 | ||
|
|
576bb078f7 | ||
|
|
3cdcc85c87 | ||
|
|
ec3a26dfbc | ||
|
|
c755dae6ee | ||
|
|
b67d51e019 | ||
|
|
7a34d6026f | ||
|
|
548eac8e60 | ||
|
|
05ac8e861f | ||
|
|
7a4d475cca | ||
|
|
eea8e60518 | ||
|
|
f6a272498d | ||
|
|
aef7c4ce8e | ||
|
|
b29eb8e032 | ||
|
|
da9183f4b5 | ||
|
|
ae3ddcb0e6 | ||
|
|
c6b8f0e8ed | ||
|
|
bab9f40a81 | ||
|
|
c2cfdf3678 | ||
|
|
8967eadc02 | ||
|
|
4cc65f837f | ||
|
|
22706e7371 | ||
|
|
9f9302662c | ||
|
|
6b90e1babd | ||
|
|
7e540a8f49 | ||
|
|
f06ffd72aa | ||
|
|
83d3d7579c | ||
|
|
39edb6eb45 | ||
|
|
d81c1a929e | ||
|
|
f69c28df18 | ||
|
|
66f54e7f1a | ||
|
|
81e416b085 | ||
|
|
a5f73b441c | ||
|
|
0f1ebccad5 | ||
|
|
2f01df6185 | ||
|
|
c4982319fe | ||
|
|
8fb4e88439 | ||
|
|
e821efca07 | ||
|
|
568f7b21ae | ||
|
|
d3c0f5320c | ||
|
|
5adc88bf50 | ||
|
|
67300adf41 | ||
|
|
4a3a67bf37 | ||
|
|
8150847218 | ||
|
|
8cb95b4646 | ||
|
|
df4ce904a0 | ||
|
|
1cc3a43268 | ||
|
|
d5a862a406 | ||
|
|
33c83c3e62 | ||
|
|
171adcd09e | ||
|
|
13f7cb9a9a | ||
|
|
ee7dbafc85 | ||
|
|
f5cfe4e8a2 | ||
|
|
c3e45d19d7 | ||
|
|
966100c2d6 | ||
|
|
bce1dea11b | ||
|
|
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 | ||
|
|
b043f829a6 | ||
|
|
f415f7b146 |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -16,10 +16,10 @@ Please copy and paste the Leptos dependencies and features from your `Cargo.toml
|
||||
|
||||
For example:
|
||||
```toml
|
||||
leptos = { version = "0.3", default-features = false, features = ["serde"] }
|
||||
leptos = { version = "0.3", 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_meta = { version = "0.3"}
|
||||
leptos_router = { version = "0.3"}
|
||||
```
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
96
.github/workflows/check-examples.yml
vendored
96
.github/workflows/check-examples.yml
vendored
@@ -1,46 +1,74 @@
|
||||
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 }}
|
||||
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
- 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"
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
files: |
|
||||
integrations
|
||||
leptos
|
||||
leptos_config
|
||||
leptos_dom
|
||||
leptos_hot_reload
|
||||
leptos_macro
|
||||
leptos_reactive
|
||||
leptos_server
|
||||
meta
|
||||
router
|
||||
server_fn
|
||||
server_fn_macro
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
- name: Set source_changed
|
||||
id: set-source-changed
|
||||
run: |
|
||||
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- 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]
|
||||
if: needs.setup.outputs.source_changed == 'true'
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
|
||||
46
.github/workflows/check.yml
vendored
46
.github/workflows/check.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run `cargo check` ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- 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 libraries
|
||||
run: cargo make --profile=github-actions check
|
||||
75
.github/workflows/ci.yml
vendored
Normal file
75
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
files: |
|
||||
integrations
|
||||
leptos
|
||||
leptos_config
|
||||
leptos_dom
|
||||
leptos_hot_reload
|
||||
leptos_macro
|
||||
leptos_reactive
|
||||
leptos_server
|
||||
meta
|
||||
router
|
||||
server_fn
|
||||
server_fn_macro
|
||||
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set source_changed
|
||||
id: set-source-changed
|
||||
run: |
|
||||
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
matrix-job:
|
||||
name: CI
|
||||
needs: [setup]
|
||||
if: needs.setup.outputs.source_changed == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
directory:
|
||||
[
|
||||
integrations/actix,
|
||||
integrations/axum,
|
||||
integrations/viz,
|
||||
integrations/utils,
|
||||
leptos,
|
||||
leptos_config,
|
||||
leptos_dom,
|
||||
leptos_hot_reload,
|
||||
leptos_macro,
|
||||
leptos_reactive,
|
||||
leptos_server,
|
||||
meta,
|
||||
router,
|
||||
server_fn,
|
||||
server_fn/server_fn_macro_default,
|
||||
server_fn_macro,
|
||||
]
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
34
.github/workflows/fmt.yml
vendored
34
.github/workflows/fmt.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run rustfmt
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
@@ -1,17 +1,21 @@
|
||||
name: Verify Examples
|
||||
name: Run 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
|
||||
# Run Cargo Make Task
|
||||
- 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
|
||||
46
.github/workflows/test.yml
vendored
46
.github/workflows/test.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- 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 tests with all features
|
||||
run: cargo make --profile=github-actions test
|
||||
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-cargo-make-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-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "verify-flow"
|
||||
@@ -220,8 +220,8 @@ for reference: they include large amounts of manual SSR route handling, etc.
|
||||
## `cargo-leptos` helpers
|
||||
|
||||
`leptos_config` and `leptos_hot_reload` exist to support two different features
|
||||
of `cargo-leptos`, namely its configuration and its view-patching/hot-
|
||||
reloading features.
|
||||
of `cargo-leptos`, namely its configuration and its view-patching/hot-reloading
|
||||
features.
|
||||
|
||||
It’s important to say that the main feature `cargo-leptos` remains its ability
|
||||
to conveniently tie together different build tooling, compiling your app to
|
||||
|
||||
@@ -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,9 +67,28 @@ 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`.
|
||||
|
||||
## Before Submitting a PR
|
||||
|
||||
We have a fairly extensive CI setup that runs both lints (like `rustfmt` and `clippy`)
|
||||
and tests on PRs. You can run most of these locally if you have `cargo-make` installed.
|
||||
|
||||
If you added an example, make sure to add it to the list in `examples/Makefile.toml`.
|
||||
|
||||
From the root directory of the repo, run
|
||||
- `cargo +nightly fmt`
|
||||
- `cargo +nightly make check`
|
||||
- `cargo +nightly make test`
|
||||
- `cargo +nightly make check-examples`
|
||||
- `cargo +nightly make --profile=github-actions ci`
|
||||
|
||||
If you modified an example:
|
||||
- `cd examples/your_example`
|
||||
- `cargo +nightly fmt -- --config-path ../..`
|
||||
- `cargo +nightly make --profile=github-actions verify-flow`
|
||||
|
||||
## Architecture
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md).
|
||||
|
||||
30
Cargo.toml
30
Cargo.toml
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
resolver="2"
|
||||
resolver = "2"
|
||||
members = [
|
||||
# core
|
||||
"leptos",
|
||||
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.0"
|
||||
version = "0.5.0-beta"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.0" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0" }
|
||||
leptos_router = { path = "./router", version = "0.3.0" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0" }
|
||||
leptos = { path = "./leptos", version = "0.5.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.0-beta" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.0-beta" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.0-beta" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.0-beta" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-beta" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-beta" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.5.0-beta" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.0-beta" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-beta" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
115
Makefile.toml
115
Makefile.toml
@@ -3,118 +3,37 @@
|
||||
# cargo install --force cargo-make
|
||||
############
|
||||
|
||||
[config]
|
||||
# make tasks run at the workspace root
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = [
|
||||
"check-all",
|
||||
"check-wasm",
|
||||
"check-all-release",
|
||||
"check-wasm-release",
|
||||
]
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-wasm]
|
||||
clear = true
|
||||
dependencies = [{ name = "check-wasm", path = "leptos" }]
|
||||
|
||||
[tasks.check-all-release]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-wasm-release]
|
||||
clear = true
|
||||
dependencies = [{ name = "check-wasm-release", path = "leptos" }]
|
||||
|
||||
[tasks.check-examples]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter" },
|
||||
{ name = "check", path = "examples/counter_isomorphic" },
|
||||
{ name = "check", path = "examples/counters" },
|
||||
{ name = "check", path = "examples/error_boundary" },
|
||||
{ name = "check", path = "examples/errors_axum" },
|
||||
{ name = "check", path = "examples/fetch" },
|
||||
{ name = "check", path = "examples/hackernews" },
|
||||
{ name = "check", path = "examples/hackernews_axum" },
|
||||
{ name = "check", path = "examples/login_with_token_csr_only" },
|
||||
{ name = "check", path = "examples/parent_child" },
|
||||
{ name = "check", path = "examples/router" },
|
||||
{ name = "check", path = "examples/session_auth_axum" },
|
||||
{ name = "check", path = "examples/slots" },
|
||||
{ name = "check", path = "examples/ssr_modes" },
|
||||
{ name = "check", path = "examples/ssr_modes_axum" },
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
{ name = "check", path = "examples/tailwind_csr_trunk" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_axum" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_viz" },
|
||||
{ name = "check", path = "examples/todomvc" },
|
||||
]
|
||||
[env]
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
|
||||
[tasks.check-stable]
|
||||
workspace = false
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = [
|
||||
"test-all",
|
||||
"test-leptos_macro-example",
|
||||
"doc-leptos_macro-example",
|
||||
]
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.test-leptos_macro-example]
|
||||
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test", "--doc"]
|
||||
cwd = "leptos_macro/example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.doc-leptos_macro-example]
|
||||
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "doc"]
|
||||
cwd = "leptos_macro/example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.test-examples]
|
||||
description = "Run all unit and web tests for examples"
|
||||
[tasks.ci-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "test-unit-and-web"]
|
||||
args = ["make", "ci-clean"]
|
||||
|
||||
[tasks.verify-examples]
|
||||
description = "Run all quality checks and tests for examples"
|
||||
[tasks.check-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "verify-flow"]
|
||||
args = ["make", "check-clean"]
|
||||
|
||||
[tasks.build-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "build-clean"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
description = "Clean all example projects"
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "clean-all"]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS = "-D warnings"
|
||||
args = ["make", "clean"]
|
||||
|
||||
13
README.md
13
README.md
@@ -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
|
||||
@@ -69,7 +68,7 @@ Here are some resources for learning more about Leptos:
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` version of Rust. For this, you can either set your toolchain globally or on per-project basis.
|
||||
Most of the examples assume you’re using `nightly` version of Rust and the `nightly` feature of Leptos. To use `nightly` Rust, you can either set your toolchain globally or on per-project basis.
|
||||
|
||||
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you haven’t already):
|
||||
|
||||
@@ -87,13 +86,7 @@ channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
```
|
||||
|
||||
If you’re on `stable`, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
|
||||
2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
you’ll just call `.get()`, `.set()`, or `.update()` manually. Check out the
|
||||
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
|
||||
for examples of the correct API.
|
||||
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
@@ -112,7 +105,7 @@ Open browser to [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
### What’s up with the name?
|
||||
|
||||
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refined, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
|
||||
leptos = { path = "../leptos", features = ["ssr"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
tokio-test = "0.4"
|
||||
|
||||
7
cargo-make/check.toml
Normal file
7
cargo-make/check.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[tasks.check]
|
||||
alias = "check-all"
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
11
cargo-make/lint.toml
Normal file
11
cargo-make/lint.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[tasks.lint]
|
||||
dependencies = ["check-format-flow", "clippy-each-feature"]
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
|
||||
[tasks.clippy-each-feature]
|
||||
dependencies = ["install-clippy"]
|
||||
command = "cargo"
|
||||
args = ["hack", "clippy", "--all", "--each-feature", "--no-dev-deps"]
|
||||
15
cargo-make/main.toml
Normal file
15
cargo-make/main.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
extend = [
|
||||
{ path = "./check.toml" },
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["lint", "test"]
|
||||
7
cargo-make/test.toml
Normal file
7
cargo-make/test.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[tasks.test]
|
||||
alias = "test-all"
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -9,10 +9,10 @@ This document is intended as a running list of common issues, with example code
|
||||
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, false);
|
||||
let (a, set_a) = create_signal(0);
|
||||
let (b, set_b) = create_signal(false);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
if a() > 5 {
|
||||
set_b(true);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ This creates an inefficient chain of updates, and can easily lead to infinite lo
|
||||
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (a, set_a) = create_signal(0);
|
||||
let b = move || a () > 5;
|
||||
```
|
||||
|
||||
@@ -34,19 +34,19 @@ Sometimes you have nested signals: for example, hash-map that can change over ti
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let resources = create_rw_signal(cx, HashMap::new());
|
||||
pub fn App() -> impl IntoView {
|
||||
let resources = create_rw_signal(HashMap::new());
|
||||
|
||||
let update = move |id: usize| {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(cx, 0))
|
||||
.or_insert_with(|| create_rw_signal(0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
|
||||
<button on:click=move |_| update(1)>"+"</button>
|
||||
@@ -55,17 +55,17 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
Clicking the button twice will cause a panic, because of the nested signal *read*. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
|
||||
Clicking the button twice will cause a panic, because of the nested signal _read_. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
|
||||
|
||||
You can fix this fairly easily by using the [`Scope::batch()`](https://docs.rs/leptos/latest/leptos/struct.Scope.html#method.batch) method:
|
||||
You can fix this fairly easily by using the [`batch()`](https://docs.rs/leptos/latest/leptos/fn.batch.html) method:
|
||||
|
||||
```rust
|
||||
let update = move |id: usize| {
|
||||
cx.batch(move || {
|
||||
batch(move || {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(cx, 0))
|
||||
.or_insert_with(|| create_rw_signal(0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
});
|
||||
@@ -83,11 +83,11 @@ Many DOM attributes can be updated either by setting an attribute on the DOM nod
|
||||
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let (a, set_a) = create_signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
// ❌ reactivity doesn't work as expected: typing only updates the default
|
||||
// of each input, so if you start typing in the second input, it won't
|
||||
// update the first one
|
||||
@@ -97,11 +97,11 @@ view! {
|
||||
```
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let (a, set_a) = create_signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
// ✅ works as intended by setting the value *property*
|
||||
<input prop:value=a on:input=on_input />
|
||||
<input prop:value=a on:input=on_input />
|
||||
|
||||
2
docs/book/book.toml
Normal file
2
docs/book/book.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
@@ -14,24 +14,39 @@ If you don’t already have it installed, you can install Trunk by running
|
||||
cargo install trunk
|
||||
```
|
||||
|
||||
Create a basic Rust binary project
|
||||
Create a basic Rust project
|
||||
|
||||
```bash
|
||||
cargo init leptos-tutorial
|
||||
```
|
||||
|
||||
> We recommend using `nightly` Rust, as it enables [a few nice features](https://github.com/leptos-rs/leptos#nightly-note). To use `nightly` Rust with WebAssembly, you can run
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
cargo add leptos --features=csr,nightly
|
||||
```
|
||||
|
||||
Or you can leave off `nightly` if you're using stable Rust
|
||||
|
||||
```bash
|
||||
cargo add leptos --features=csr
|
||||
```
|
||||
|
||||
> Using `nightly` Rust, and the `nightly` feature in Leptos enables the function-call syntax for signal getters and setters that is used in most of this book.
|
||||
>
|
||||
> To use `nightly` Rust, you can run
|
||||
>
|
||||
> ```bash
|
||||
> rustup toolchain install nightly
|
||||
> rustup default nightly
|
||||
> rustup target add wasm32-unknown-unknown
|
||||
> ```
|
||||
>
|
||||
> If you’d rather use stable Rust with Leptos, you can do that too. In the guide and examples, you’ll just use the [`ReadSignal::get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) and [`WriteSignal::set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) methods instead of calling signal getters and setters as functions.
|
||||
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
Make sure you've added the `wasm32-unknown-unknown` target so that Rust can compile your code to WebAssembly to run in the browser.
|
||||
|
||||
```bash
|
||||
cargo add leptos
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
@@ -50,7 +65,7 @@ And add a simple “Hello, world!” to your `main.rs`
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
mount_to_body(|| view! { <p>"Hello, world!"</p> })
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Responding to Changes with create_effect
|
||||
@@ -29,15 +29,15 @@ all its children and descendants using `provide_context`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
fn App() -> impl IntoView {
|
||||
// here we create a signal in the root that can be consumed
|
||||
// anywhere in the app.
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
// we'll pass the setter to specific components,
|
||||
// but provide the count itself to the whole app via context
|
||||
provide_context(cx, count);
|
||||
provide_context(count);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
// SetterButton is allowed to modify the count
|
||||
<SetterButton set_count/>
|
||||
// These consumers can only read from it
|
||||
@@ -57,14 +57,14 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
```rust
|
||||
/// A component that does some "fancy" math with the global count
|
||||
#[component]
|
||||
fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
fn FancyMath() -> impl IntoView {
|
||||
// here we consume the global count signal with `use_context`
|
||||
let count = use_context::<ReadSignal<u32>>(cx)
|
||||
let count = use_context::<ReadSignal<u32>>()
|
||||
// we know we just provided this in the parent component
|
||||
.expect("there to be a `count` signal provided");
|
||||
let is_even = move || count() & 1 == 0;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="consumer blue">
|
||||
"The number "
|
||||
<strong>{count}</strong>
|
||||
@@ -89,17 +89,17 @@ struct GlobalState {
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
count: create_rw_signal(cx, 0),
|
||||
name: create_rw_signal(cx, "Bob".to_string())
|
||||
count: create_rw_signal(0),
|
||||
name: create_rw_signal("Bob".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
provide_context(cx, GlobalState::new(cx));
|
||||
fn App() -> impl IntoView {
|
||||
provide_context(GlobalState::new());
|
||||
|
||||
// etc.
|
||||
}
|
||||
@@ -117,8 +117,8 @@ struct GlobalState {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
provide_context(cx, create_rw_signal(GlobalState::default()));
|
||||
fn App() -> impl IntoView {
|
||||
provide_context(create_rw_signal(GlobalState::default()));
|
||||
|
||||
// etc.
|
||||
}
|
||||
@@ -127,8 +127,8 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
But there’s a problem: because our whole state is wrapped in one signal, updating the value of one field will cause reactive updates in parts of the UI that only depend on the other.
|
||||
|
||||
```rust
|
||||
let state = expect_context::<RwSignal<GlobalState>>(cx);
|
||||
view! { cx,
|
||||
let state = expect_context::<RwSignal<GlobalState>>();
|
||||
view! {
|
||||
<button on:click=move |_| state.update(|n| *n += 1)>"+1"</button>
|
||||
<p>{move || state.with(|state| state.name.clone())}</p>
|
||||
}
|
||||
@@ -136,19 +136,19 @@ view! { cx,
|
||||
|
||||
In this example, clicking the button will cause the text inside `<p>` to be updated, cloning `state.name` again! Because signals are the atomic unit of reactivity, updating any field of the signal triggers updates to everything that depends on the signal.
|
||||
|
||||
There’s a better way. You can use take fine-grained, reactive slices by using [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html) or [`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html) (which uses `create_memo` but also provides a setter). “Memoizing” a value means creating a new reactive value which will only update when it changes. “Memoizing a slice” means creating a new reactive value which will only update when some field of the state struct updates.
|
||||
There’s a better way. You can take fine-grained, reactive slices by using [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html) or [`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html) (which uses `create_memo` but also provides a setter). “Memoizing” a value means creating a new reactive value which will only update when it changes. “Memoizing a slice” means creating a new reactive value which will only update when some field of the state struct updates.
|
||||
|
||||
Here, instead of reading from the state signal directly, we create “slices” of that state with fine-grained updates via `create_slice`. Each slice signal only updates when the particular piece of the larger struct it accesses updates. This means you can create a single root signal, and then take independent, fine-grained slices of it in different components, each of which can update without notifying the others of changes.
|
||||
|
||||
```rust
|
||||
/// A component that updates the count in the global state.
|
||||
#[component]
|
||||
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|
||||
let state = expect_context::<RwSignal<GlobalState>>(cx);
|
||||
fn GlobalStateCounter() -> impl IntoView {
|
||||
let state = expect_context::<RwSignal<GlobalState>>();
|
||||
|
||||
// `create_slice` lets us create a "lens" into the data
|
||||
let (count, set_count) = create_slice(
|
||||
cx,
|
||||
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
@@ -157,7 +157,7 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|
||||
|state, n| state.count = n,
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="consumer blue">
|
||||
<button
|
||||
on:click=move |_| {
|
||||
@@ -183,3 +183,222 @@ data flow and of fine-grained reactive updates.
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
// So far, we've only been working with local state in components
|
||||
// We've only seen how to communicate between parent and child components
|
||||
// But there are also more general ways to manage global state
|
||||
//
|
||||
// The three best approaches to global state are
|
||||
// 1. Using the router to drive global state via the URL
|
||||
// 2. Passing signals through context
|
||||
// 3. Creating a global state struct and creating lenses into it with `create_slice`
|
||||
//
|
||||
// Option #1: URL as Global State
|
||||
// The next few sections of the tutorial will be about the router.
|
||||
// So for now, we'll just look at options #2 and #3.
|
||||
|
||||
// Option #2: Pass Signals through Context
|
||||
//
|
||||
// In virtual DOM libraries like React, using the Context API to manage global
|
||||
// state is a bad idea: because the entire app exists in a tree, changing
|
||||
// some value provided high up in the tree can cause the whole app to render.
|
||||
//
|
||||
// In fine-grained reactive libraries like Leptos, this is simply not the case.
|
||||
// You can create a signal in the root of your app and pass it down to other
|
||||
// components using provide_context(). Changing it will only cause rerendering
|
||||
// in the specific places it is actually used, not the whole app.
|
||||
#[component]
|
||||
fn Option2() -> impl IntoView {
|
||||
// here we create a signal in the root that can be consumed
|
||||
// anywhere in the app.
|
||||
let (count, set_count) = create_signal(0);
|
||||
// we'll pass the setter to specific components,
|
||||
// but provide the count itself to the whole app via context
|
||||
provide_context(count);
|
||||
|
||||
view! {
|
||||
<h1>"Option 2: Passing Signals"</h1>
|
||||
// SetterButton is allowed to modify the count
|
||||
<SetterButton set_count/>
|
||||
// These consumers can only read from it
|
||||
// But we could give them write access by passing `set_count` if we wanted
|
||||
<div style="display: flex">
|
||||
<FancyMath/>
|
||||
<ListItems/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// A button that increments our global counter.
|
||||
#[component]
|
||||
fn SetterButton(set_count: WriteSignal<u32>) -> impl IntoView {
|
||||
view! {
|
||||
<div class="provider red">
|
||||
<button on:click=move |_| set_count.update(|count| *count += 1)>
|
||||
"Increment Global Count"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// A component that does some "fancy" math with the global count
|
||||
#[component]
|
||||
fn FancyMath() -> impl IntoView {
|
||||
// here we consume the global count signal with `use_context`
|
||||
let count = use_context::<ReadSignal<u32>>()
|
||||
// we know we just provided this in the parent component
|
||||
.expect("there to be a `count` signal provided");
|
||||
let is_even = move || count() & 1 == 0;
|
||||
|
||||
view! {
|
||||
<div class="consumer blue">
|
||||
"The number "
|
||||
<strong>{count}</strong>
|
||||
{move || if is_even() {
|
||||
" is"
|
||||
} else {
|
||||
" is not"
|
||||
}}
|
||||
" even."
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// A component that shows a list of items generated from the global count.
|
||||
#[component]
|
||||
fn ListItems() -> impl IntoView {
|
||||
// again, consume the global count signal with `use_context`
|
||||
let count = use_context::<ReadSignal<u32>>().expect("there to be a `count` signal provided");
|
||||
|
||||
let squares = move || {
|
||||
(0..count())
|
||||
.map(|n| view! { <li>{n}<sup>"2"</sup> " is " {n * n}</li> })
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="consumer green">
|
||||
<ul>{squares}</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// Option #3: Create a Global State Struct
|
||||
//
|
||||
// You can use this approach to build a single global data structure
|
||||
// that holds the state for your whole app, and then access it by
|
||||
// taking fine-grained slices using `create_slice` or `create_memo`,
|
||||
// so that changing one part of the state doesn't cause parts of your
|
||||
// app that depend on other parts of the state to change.
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct GlobalState {
|
||||
count: u32,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Option3() -> impl IntoView {
|
||||
// we'll provide a single signal that holds the whole state
|
||||
// each component will be responsible for creating its own "lens" into it
|
||||
let state = create_rw_signal(GlobalState::default());
|
||||
provide_context(state);
|
||||
|
||||
view! {
|
||||
<h1>"Option 3: Passing Signals"</h1>
|
||||
<div class="red consumer" style="width: 100%">
|
||||
<h2>"Current Global State"</h2>
|
||||
<pre>
|
||||
{move || {
|
||||
format!("{:#?}", state.get())
|
||||
}}
|
||||
</pre>
|
||||
</div>
|
||||
<div style="display: flex">
|
||||
<GlobalStateCounter/>
|
||||
<GlobalStateInput/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// A component that updates the count in the global state.
|
||||
#[component]
|
||||
fn GlobalStateCounter() -> impl IntoView {
|
||||
let state = use_context::<RwSignal<GlobalState>>().expect("state to have been provided");
|
||||
|
||||
// `create_slice` lets us create a "lens" into the data
|
||||
let (count, set_count) = create_slice(
|
||||
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
|state| state.count,
|
||||
// our setter describes how to mutate that slice, given a new value
|
||||
|state, n| state.count = n,
|
||||
);
|
||||
|
||||
view! {
|
||||
<div class="consumer blue">
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count(count() + 1);
|
||||
}
|
||||
>
|
||||
"Increment Global Count"
|
||||
</button>
|
||||
<br/>
|
||||
<span>"Count is: " {count}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// A component that updates the count in the global state.
|
||||
#[component]
|
||||
fn GlobalStateInput() -> impl IntoView {
|
||||
let state = use_context::<RwSignal<GlobalState>>().expect("state to have been provided");
|
||||
|
||||
// this slice is completely independent of the `count` slice
|
||||
// that we created in the other component
|
||||
// neither of them will cause the other to rerun
|
||||
let (name, set_name) = create_slice(
|
||||
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
|state| state.name.clone(),
|
||||
// our setter describes how to mutate that slice, given a new value
|
||||
|state, n| state.name = n,
|
||||
);
|
||||
|
||||
view! {
|
||||
<div class="consumer green">
|
||||
<input
|
||||
type="text"
|
||||
prop:value=name
|
||||
on:input=move |ev| {
|
||||
set_name(event_target_value(&ev));
|
||||
}
|
||||
/>
|
||||
<br/>
|
||||
<span>"Name is: " {name}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
// This `main` function is the entry point into the app
|
||||
// It just mounts our component to the <body>
|
||||
// Because we defined it as `fn App`, we can now use it in a
|
||||
// template as <App/>
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <Option2/><Option3/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
- [Transition](./async/12_transition.md)
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Interlude: Projecting Children](./interlude_projecting_children.md)
|
||||
- [Responding to Changes with `create_effect`](./14_create_effect.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Router](./router/README.md)
|
||||
- [Defining `<Routes/>`](./router/16_routes.md)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A [Resource](https://docs.rs/leptos/latest/leptos/struct.Resource.html) is a reactive data structure that reflects the current state of an asynchronous task, allowing you to integrate asynchronous `Future`s into the synchronous reactive system. Rather than waiting for its data to load with `.await`, you transform the `Future` into a signal that returns `Some(T)` if it has resolved, and `None` if it’s still pending.
|
||||
|
||||
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments (other than the ubiquitous `cx`):
|
||||
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments:
|
||||
|
||||
1. a source signal, which will generate a new `Future` whenever it changes
|
||||
2. a fetcher function, which takes the data from that signal and returns a `Future`
|
||||
@@ -11,10 +11,10 @@ Here’s an example
|
||||
|
||||
```rust
|
||||
// our source signal: some synchronous, local state
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// our resource
|
||||
let async_data = create_resource(cx,
|
||||
let async_data = create_resource(
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
@@ -27,23 +27,20 @@ let async_data = create_resource(cx,
|
||||
To create a resource that simply runs once, you can pass a non-reactive, empty source signal:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
let once = create_resource(|| (), |_| async move { load_data().await });
|
||||
```
|
||||
|
||||
To access the value you can use `.read(cx)` or `.with(cx, |data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but with two differences
|
||||
|
||||
1. For any `Resource<_, T>`, they always return `Option<T>`, not `T`: because it’s always possible that your resource is still loading.
|
||||
2. They take a `Scope` argument. You’ll see why in the next chapter, on `<Suspense/>`.
|
||||
To access the value you can use `.read()` or `.with(|data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but for any `Resource<_, T>`, they always return `Option<T>`, not `T`: because it’s always possible that your resource is still loading.
|
||||
|
||||
So, you can show the current state of a resource in your view:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
view! { cx,
|
||||
let once = create_resource(|| (), |_| async move { load_data().await });
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
{move || match once.read() {
|
||||
None => view! { <p>"Loading..."</p> }.into_view(),
|
||||
Some(data) => view! { <ShowData data/> }.into_view()
|
||||
}}
|
||||
}
|
||||
```
|
||||
@@ -53,3 +50,89 @@ Resources also provide a `refetch()` method that allows you to manually reload t
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::*;
|
||||
|
||||
// Here we define an async function
|
||||
// This could be anything: a network request, database read, etc.
|
||||
// Here, we just multiply a number by 10
|
||||
async fn load_data(value: i32) -> i32 {
|
||||
// fake a one-second delay
|
||||
TimeoutFuture::new(1_000).await;
|
||||
value * 10
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
// this count is our synchronous, local state
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// create_resource takes two arguments after its scope
|
||||
let async_data = create_resource(
|
||||
|
||||
// the first is the "source signal"
|
||||
count,
|
||||
// the second is the loader
|
||||
// it takes the source signal's value as its argument
|
||||
// and does some async work
|
||||
|value| async move { load_data(value).await },
|
||||
);
|
||||
// whenever the source signal changes, the loader reloads
|
||||
|
||||
// you can also create resources that only load once
|
||||
// just return the unit type () from the source signal
|
||||
// that doesn't depend on anything: we just load it once
|
||||
let stable = create_resource(|| (), |_| async move { load_data(1).await });
|
||||
|
||||
// we can access the resource values with .read()
|
||||
// this will reactively return None before the Future has resolved
|
||||
// and update to Some(T) when it has resolved
|
||||
let async_result = move || {
|
||||
async_data
|
||||
.read()
|
||||
.map(|value| format!("Server returned {value:?}"))
|
||||
// This loading state will only show before the first load
|
||||
.unwrap_or_else(|| "Loading...".into())
|
||||
};
|
||||
|
||||
// the resource's loading() method gives us a
|
||||
// signal to indicate whether it's currently loading
|
||||
let loading = async_data.loading();
|
||||
let is_loading = move || if loading() { "Loading..." } else { "Idle." };
|
||||
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
"Click me"
|
||||
</button>
|
||||
<p>
|
||||
<code>"stable"</code>": " {move || stable.read()}
|
||||
</p>
|
||||
<p>
|
||||
<code>"count"</code>": " {count}
|
||||
</p>
|
||||
<p>
|
||||
<code>"async_value"</code>": "
|
||||
{async_result}
|
||||
<br/>
|
||||
{is_loading}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let (count, set_count) = create_signal(0);
|
||||
let a = create_resource(count, |count| async move { load_a(count).await });
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
{move || match once.read() {
|
||||
None => view! { <p>"Loading..."</p> }.into_view(),
|
||||
Some(data) => view! { <ShowData data/> }.into_view()
|
||||
}}
|
||||
}
|
||||
```
|
||||
@@ -18,19 +18,19 @@ view! { cx,
|
||||
But what if we have two resources, and want to wait for both of them?
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
let (count, set_count) = create_signal(0);
|
||||
let (count2, set_count2) = create_signal(0);
|
||||
let a = create_resource(count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(cx), b.read(cx)) {
|
||||
(Some(a), Some(b)) => view! { cx,
|
||||
{move || match (a.read(), b.read()) {
|
||||
(Some(a), Some(b)) => view! {
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
}.into_view(cx),
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx)
|
||||
}.into_view(),
|
||||
_ => view! { <p>"Loading..."</p> }.into_view()
|
||||
}}
|
||||
}
|
||||
```
|
||||
@@ -40,26 +40,26 @@ That’s not _so_ bad, but it’s kind of annoying. What if we could invert the
|
||||
The [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `<Suspense/>` (i.e., in one of its children) registers that resource with the `<Suspense/>`. If it’s still waiting for resources to load, it shows the `fallback`. When they’ve all loaded, it shows the children.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
let (count, set_count) = create_signal(0);
|
||||
let (count2, set_count2) = create_signal(0);
|
||||
let a = create_resource(count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
>
|
||||
<h2>"My Data"</h2>
|
||||
<h3>"A"</h3>
|
||||
{move || {
|
||||
a.read(cx)
|
||||
.map(|a| view! { cx, <ShowA a/> })
|
||||
a.read()
|
||||
.map(|a| view! { <ShowA a/> })
|
||||
}}
|
||||
<h3>"B"</h3>
|
||||
{move || {
|
||||
b.read(cx)
|
||||
.map(|b| view! { cx, <ShowB b/> })
|
||||
b.read()
|
||||
.map(|b| view! { <ShowB b/> })
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
@@ -72,3 +72,58 @@ This inversion of the flow of control makes it easier to add or remove individua
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::*;
|
||||
|
||||
async fn important_api_call(name: String) -> String {
|
||||
TimeoutFuture::new(1_000).await;
|
||||
name.to_ascii_uppercase()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
let (name, set_name) = create_signal("Bill".to_string());
|
||||
|
||||
// this will reload every time `name` changes
|
||||
let async_data = create_resource(
|
||||
|
||||
name,
|
||||
|name| async move { important_api_call(name).await },
|
||||
);
|
||||
|
||||
view! {
|
||||
<input
|
||||
on:input=move |ev| {
|
||||
set_name(event_target_value(&ev));
|
||||
}
|
||||
prop:value=name
|
||||
/>
|
||||
<p><code>"name:"</code> {name}</p>
|
||||
<Suspense
|
||||
// the fallback will show whenever a resource
|
||||
// read "under" the suspense is loading
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
>
|
||||
// the children will be rendered once initially,
|
||||
// and then whenever any resources has been resolved
|
||||
<p>
|
||||
"Your shouting name is "
|
||||
{move || async_data.read()}
|
||||
</p>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -9,3 +9,76 @@ This example shows how you can create a simple tabbed contact list with `<Transi
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::*;
|
||||
|
||||
async fn important_api_call(id: usize) -> String {
|
||||
TimeoutFuture::new(1_000).await;
|
||||
match id {
|
||||
0 => "Alice",
|
||||
1 => "Bob",
|
||||
2 => "Carol",
|
||||
_ => "User not found",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
let (tab, set_tab) = create_signal(0);
|
||||
|
||||
// this will reload every time `tab` changes
|
||||
let user_data = create_resource(tab, |tab| async move { important_api_call(tab).await });
|
||||
|
||||
view! {
|
||||
<div class="buttons">
|
||||
<button
|
||||
on:click=move |_| set_tab(0)
|
||||
class:selected=move || tab() == 0
|
||||
>
|
||||
"Tab A"
|
||||
</button>
|
||||
<button
|
||||
on:click=move |_| set_tab(1)
|
||||
class:selected=move || tab() == 1
|
||||
>
|
||||
"Tab B"
|
||||
</button>
|
||||
<button
|
||||
on:click=move |_| set_tab(2)
|
||||
class:selected=move || tab() == 2
|
||||
>
|
||||
"Tab C"
|
||||
</button>
|
||||
{move || if user_data.loading().get() {
|
||||
"Loading..."
|
||||
} else {
|
||||
""
|
||||
}}
|
||||
</div>
|
||||
<Transition
|
||||
// the fallback will show initially
|
||||
// on subsequent reloads, the current child will
|
||||
// continue showing
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
>
|
||||
<p>
|
||||
{move || user_data.read()}
|
||||
</p>
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -11,27 +11,27 @@ 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 */
|
||||
}
|
||||
```
|
||||
|
||||
`create_action` takes a reactive `Scope` and an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
|
||||
`create_action` takes an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
|
||||
|
||||
> The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple.
|
||||
>
|
||||
> ```rust
|
||||
> // if there's a single argument, just use that
|
||||
> let action1 = create_action(cx, |input: &String| {
|
||||
> let action1 = create_action(|input: &String| {
|
||||
> let input = input.clone();
|
||||
> async move { todo!() }
|
||||
> });
|
||||
>
|
||||
> // if there are no arguments, use the unit type `()`
|
||||
> let action2 = create_action(cx, |input: &()| async { todo!() });
|
||||
> let action2 = create_action(|input: &()| async { todo!() });
|
||||
>
|
||||
> // if there are multiple arguments, use a tuple
|
||||
> let action3 = create_action(cx,
|
||||
> let action3 = create_action(
|
||||
> |input: &(usize, String)| async { 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(|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,22 +58,22 @@ 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.
|
||||
|
||||
```rust
|
||||
let input_ref = create_node_ref::<Input>(cx);
|
||||
let input_ref = create_node_ref::<Input>();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<form
|
||||
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>
|
||||
@@ -94,3 +94,83 @@ Now, there’s a chance this all seems a little over-complicated, or maybe too r
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::{html::Input, *};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Here we define an async function
|
||||
// This could be anything: a network request, database read, etc.
|
||||
// Think of it as a mutation: some imperative async action you run,
|
||||
// whereas a resource would be some async data you load
|
||||
async fn add_todo(text: &str) -> Uuid {
|
||||
_ = text;
|
||||
// fake a one-second delay
|
||||
TimeoutFuture::new(1_000).await;
|
||||
// pretend this is a post ID or something
|
||||
Uuid::new_v4()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
// an action takes an async function with single argument
|
||||
// it can be a simple type, a struct, or ()
|
||||
let add_todo = create_action(|input: &String| {
|
||||
// the input is a reference, but we need the Future to own it
|
||||
// this is important: we need to clone and move into the Future
|
||||
// so it has a 'static lifetime
|
||||
let input = input.to_owned();
|
||||
async move { add_todo(&input).await }
|
||||
});
|
||||
|
||||
// actions provide a bunch of synchronous, reactive variables
|
||||
// that tell us different things about the state of the action
|
||||
let submitted = add_todo.input();
|
||||
let pending = add_todo.pending();
|
||||
let todo_id = add_todo.value();
|
||||
|
||||
let input_ref = create_node_ref::<Input>();
|
||||
|
||||
view! {
|
||||
<form
|
||||
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());
|
||||
}
|
||||
>
|
||||
<label>
|
||||
"What do you need to do?"
|
||||
<input type="text"
|
||||
node_ref=input_ref
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">"Add Todo"</button>
|
||||
</form>
|
||||
<p>{move || pending().then(|| "Loading...")}</p>
|
||||
<p>
|
||||
"Submitted: "
|
||||
<code>{move || format!("{:#?}", submitted())}</code>
|
||||
</p>
|
||||
<p>
|
||||
"Pending: "
|
||||
<code>{move || format!("{:#?}", pending())}</code>
|
||||
</p>
|
||||
<p>
|
||||
"Todo ID: "
|
||||
<code>{move || format!("{:#?}", todo_id())}</code>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -7,12 +7,12 @@ As you build components you may occasionally find yourself wanting to “project
|
||||
Consider the following:
|
||||
|
||||
```rust
|
||||
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
F: Fn() -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
@@ -22,16 +22,16 @@ where
|
||||
when=move || todo!()
|
||||
fallback=fallback
|
||||
>
|
||||
{children(cx)}
|
||||
{children()}
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -50,18 +50,18 @@ If you want to really understand the issue here, it may help to look at the expa
|
||||
|
||||
```rust
|
||||
Suspense(
|
||||
cx,
|
||||
|
||||
::leptos::component_props_builder(&Suspense)
|
||||
.fallback(|| ())
|
||||
.children({
|
||||
// fallback and children are moved into this closure
|
||||
Box::new(move |cx| {
|
||||
Box::new(move || {
|
||||
{
|
||||
// fallback and children captured here
|
||||
leptos::Fragment::lazy(|| {
|
||||
vec![
|
||||
(Show(
|
||||
cx,
|
||||
|
||||
::leptos::component_props_builder(&Show)
|
||||
.when(|| true)
|
||||
// but fallback is moved into Show here
|
||||
@@ -70,7 +70,7 @@ Suspense(
|
||||
.children(children)
|
||||
.build(),
|
||||
)
|
||||
.into_view(cx)),
|
||||
.into_view()),
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -91,22 +91,22 @@ We can solve this problem by using the [`store_value`](https://docs.rs/leptos/la
|
||||
In this case, it’s really simple:
|
||||
|
||||
```rust
|
||||
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
pub fn LoggedIn<F, IV>(F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
F: Fn() -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
let fallback = store_value(cx, fallback);
|
||||
let children = store_value(cx, children);
|
||||
view! { cx,
|
||||
let fallback = store_value(fallback);
|
||||
let children = store_value(children);
|
||||
view! {
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
<Show
|
||||
when=|| todo!()
|
||||
fallback=move |cx| fallback.with_value(|fallback| fallback(cx))
|
||||
fallback=move || fallback.with_value(|fallback| fallback())
|
||||
>
|
||||
{children.with_value(|children| children(cx))}
|
||||
{children.with_value(|children| children())}
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
@@ -125,9 +125,9 @@ Consider this example
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
let name = "Alice".to_string();
|
||||
view! { cx,
|
||||
view! {
|
||||
<Outer>
|
||||
<Inner>
|
||||
<Inmost name=name.clone()/>
|
||||
@@ -137,18 +137,18 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Outer(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
pub fn Outer(ChildrenFn) -> impl IntoView {
|
||||
children()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inner(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
pub fn Inner(ChildrenFn) -> impl IntoView {
|
||||
children()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inmost(cx: Scope, name: String) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Inmost(ng) -> impl IntoView {
|
||||
view! {
|
||||
<p>{name}</p>
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ It’s captured through multiple levels of children that need to run more than o
|
||||
In this case, the `clone:` syntax comes in handy. Calling `clone:name` will clone `name` _before_ moving it into `<Inner/>`’s children, which solves our ownership issue.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<Outer>
|
||||
<Inner clone:name>
|
||||
<Inmost name=name.clone()/>
|
||||
|
||||
@@ -14,10 +14,10 @@ This allows you to write components like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn Home(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn Home() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<main class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
@@ -48,7 +48,7 @@ This allows you to write components like this:
|
||||
use stylers::style;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
let styler_class = style! { "App",
|
||||
#two{
|
||||
color: blue;
|
||||
@@ -74,7 +74,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx, class = styler_class,
|
||||
view! { class = styler_class,
|
||||
<div class="one">
|
||||
<h1 id="two">"Hello"</h1>
|
||||
<h2>"World"</h2>
|
||||
@@ -93,7 +93,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
use styled::style;
|
||||
|
||||
#[component]
|
||||
pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
pub fn MyComponent() -> impl IntoView {
|
||||
let styles = style!(
|
||||
div {
|
||||
background-color: red;
|
||||
@@ -101,7 +101,7 @@ pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
);
|
||||
|
||||
styled::view! { cx, styles,
|
||||
styled::view! { styles,
|
||||
<div>"This text should be red with white text."</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/>`
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
[`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) is a specialized `<Form/>` that takes a server action, and automatically dispatches it on form submission. This allows you to call a server function directly from a `<form>`, even without JS/WASM.
|
||||
|
||||
The process is simple:
|
||||
|
||||
1. Define a server function using the [`#[server]` macro](https://docs.rs/leptos/latest/leptos/attr.server.html) (see [Server Functions](../server/25_server_functions.md).)
|
||||
2. Create an action using [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), specifying the type of the server function you’ve defined.
|
||||
3. Create an `<ActionForm/>`, providing the server action in the `action` prop.
|
||||
4. Pass the named arguments to the server function as form fields with the same names.
|
||||
|
||||
> **Note:** `<ActionForm/>` only works with the default URL-encoded `POST` encoding for server functions, to ensure graceful degradation/correct behavior as an HTML form.
|
||||
> **Note:** `<ActionForm/>` only works with the default URL-encoded `POST` encoding for server functions, to ensure graceful degradation/correct behavior as an HTML form.
|
||||
|
||||
```rust
|
||||
#[server(AddTodo, "/api")]
|
||||
@@ -17,14 +18,14 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn AddTodo(cx: Scope) -> impl IntoView {
|
||||
let add_todo = create_server_action::<AddTodo>(cx);
|
||||
fn AddTodo() -> impl IntoView {
|
||||
let add_todo = create_server_action::<AddTodo>();
|
||||
// holds the latest *returned* value from the server
|
||||
let value = add_todo.value();
|
||||
// check if the server has returned an error
|
||||
let has_error = move || value.with(|val| matches!(val, Some(Err(_))));
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<ActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
@@ -36,6 +37,7 @@ fn AddTodo(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It’s really that easy. With JS/WASM, your form will submit without a page reload, storing its most recent submission in the `.input()` signal of the action, its pending status in `.pending()`, and so on. (See the [`Action`](https://docs.rs/leptos/latest/leptos/struct.Action.html) docs for a refresher, if you need.) Without JS/WASM, your form will submit with a page reload. If you call a `redirect` function (from `leptos_axum` or `leptos_actix`) it will redirect to the correct page. By default, it will redirect back to the page you’re currently on. The power of HTML, HTTP, and isomorphic rendering mean that your `<ActionForm/>` simply works, even with no JS/WASM.
|
||||
|
||||
## Client-Side Validation
|
||||
@@ -53,4 +55,4 @@ let on_submit = move |ev| {
|
||||
ev.prevent_default();
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -9,10 +9,10 @@ Hidden behind the whole reactive DOM renderer that we’ve seen so far is a func
|
||||
[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
let (a, set_a) = create_signal(0);
|
||||
let (b, set_b) = create_signal(0);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
// immediately prints "Value: 0" and subscribes to `a`
|
||||
log::debug!("Value: {}", a());
|
||||
});
|
||||
@@ -42,15 +42,15 @@ While they’re not a “zero-cost abstraction” in the most technical sense—
|
||||
Imagine that I’m creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:
|
||||
|
||||
```rust
|
||||
let (first, set_first) = create_signal(cx, String::new());
|
||||
let (last, set_last) = create_signal(cx, String::new());
|
||||
let (use_last, set_use_last) = create_signal(cx, true);
|
||||
let (first, set_first) = create_signal(String::new());
|
||||
let (last, set_last) = create_signal(String::new());
|
||||
let (use_last, set_use_last) = create_signal(true);
|
||||
|
||||
// this will add the name to the log
|
||||
// any time one of the source signals changes
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
log(
|
||||
cx,
|
||||
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
@@ -77,9 +77,9 @@ If you need to synchronize some reactive value with the non-reactive world outsi
|
||||
We’ve managed to get this far without mentioning effects because they’re built into the Leptos DOM renderer. We’ve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{count}</p>
|
||||
}
|
||||
```
|
||||
@@ -87,13 +87,13 @@ view! { cx,
|
||||
This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// create a DOM element
|
||||
let p = create_element("p");
|
||||
|
||||
// create an effect to reactively update the text
|
||||
create_effect(cx, move |prev_value| {
|
||||
create_effect(move |prev_value| {
|
||||
// first, access the signal’s value and convert it to a string
|
||||
let text = count().to_string();
|
||||
|
||||
@@ -112,3 +112,195 @@ Every time `count` is updated, this effect wil rerun. This is what allows reacti
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::html::Input;
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
// Just making a visible log here
|
||||
// You can ignore this...
|
||||
let log = create_rw_signal::<Vec<String>>(vec![]);
|
||||
let logged = move || log().join("\n");
|
||||
provide_context(log);
|
||||
|
||||
view! {
|
||||
<CreateAnEffect/>
|
||||
<pre>{logged}</pre>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn CreateAnEffect() -> impl IntoView {
|
||||
let (first, set_first) = create_signal(String::new());
|
||||
let (last, set_last) = create_signal(String::new());
|
||||
let (use_last, set_use_last) = create_signal(true);
|
||||
|
||||
// this will add the name to the log
|
||||
// any time one of the source signals changes
|
||||
create_effect(move |_| {
|
||||
log(
|
||||
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
first()
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
view! {
|
||||
<h1><code>"create_effect"</code> " Version"</h1>
|
||||
<form>
|
||||
<label>
|
||||
"First Name"
|
||||
<input type="text" name="first" prop:value=first
|
||||
on:change=move |ev| set_first(event_target_value(&ev))
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
"Last Name"
|
||||
<input type="text" name="last" prop:value=last
|
||||
on:change=move |ev| set_last(event_target_value(&ev))
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
"Show Last Name"
|
||||
<input type="checkbox" name="use_last" prop:checked=use_last
|
||||
on:change=move |ev| set_use_last(event_target_checked(&ev))
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ManualVersion() -> impl IntoView {
|
||||
let first = create_node_ref::<Input>();
|
||||
let last = create_node_ref::<Input>();
|
||||
let use_last = create_node_ref::<Input>();
|
||||
|
||||
let mut prev_name = String::new();
|
||||
let on_change = move |_| {
|
||||
log(" listener");
|
||||
let first = first.get().unwrap();
|
||||
let last = last.get().unwrap();
|
||||
let use_last = use_last.get().unwrap();
|
||||
let this_one = if use_last.checked() {
|
||||
format!("{} {}", first.value(), last.value())
|
||||
} else {
|
||||
first.value()
|
||||
};
|
||||
|
||||
if this_one != prev_name {
|
||||
log(&this_one);
|
||||
prev_name = this_one;
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<h1>"Manual Version"</h1>
|
||||
<form on:change=on_change>
|
||||
<label>
|
||||
"First Name"
|
||||
<input type="text" name="first"
|
||||
node_ref=first
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
"Last Name"
|
||||
<input type="text" name="last"
|
||||
node_ref=last
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
"Show Last Name"
|
||||
<input type="checkbox" name="use_last"
|
||||
checked
|
||||
node_ref=use_last
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn EffectVsDerivedSignal() -> impl IntoView {
|
||||
let (my_value, set_my_value) = create_signal(String::new());
|
||||
// Don't do this.
|
||||
/*let (my_optional_value, set_optional_my_value) = create_signal(Option::<String>::None);
|
||||
|
||||
create_effect(move |_| {
|
||||
if !my_value.get().is_empty() {
|
||||
set_optional_my_value(Some(my_value.get()));
|
||||
} else {
|
||||
set_optional_my_value(None);
|
||||
}
|
||||
});*/
|
||||
|
||||
// Do this
|
||||
let my_optional_value =
|
||||
move || (!my_value.with(String::is_empty)).then(|| Some(my_value.get()));
|
||||
|
||||
view! {
|
||||
<input
|
||||
prop:value=my_value
|
||||
on:input= move |ev| set_my_value(event_target_value(&ev))
|
||||
/>
|
||||
|
||||
<p>
|
||||
<code>"my_optional_value"</code>
|
||||
" is "
|
||||
<code>
|
||||
<Show
|
||||
when=move || my_optional_value().is_some()
|
||||
fallback=|| view! { "None" }
|
||||
>
|
||||
"Some(\"" {my_optional_value().unwrap()} "\")"
|
||||
</Show>
|
||||
</code>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
/*#[component]
|
||||
pub fn Show<F, W, IV>(
|
||||
/// The scope the component is running in
|
||||
|
||||
/// The components Show wraps
|
||||
children: Box<dyn Fn() -> Fragment>,
|
||||
/// A closure that returns a bool that determines whether this thing runs
|
||||
when: W,
|
||||
/// A closure that returns what gets rendered if the when statement is false
|
||||
fallback: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
W: Fn() -> bool + 'static,
|
||||
F: Fn() -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
let memoized_when = create_memo(move |_| when());
|
||||
|
||||
move || match memoized_when.get() {
|
||||
true => children().into_view(),
|
||||
false => fallback().into_view(),
|
||||
}
|
||||
}*/
|
||||
|
||||
fn log(std::fmt::Display) {
|
||||
let log = use_context::<RwSignal<Vec<String>>>().unwrap();
|
||||
log.update(|log| log.push(msg.to_string()));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -6,7 +6,7 @@ application. It sometimes looks a little silly:
|
||||
|
||||
```rust
|
||||
// a signal holds a value, and can be updated
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// a derived signal is a function that accesses other signals
|
||||
let double_count = move || count() * 2;
|
||||
@@ -19,11 +19,11 @@ let text = move || if count_is_odd() {
|
||||
|
||||
// an effect automatically tracks the signals it depends on
|
||||
// and reruns when they change
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
log!("text = {}", text());
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{move || text().to_uppercase()}</p>
|
||||
}
|
||||
```
|
||||
@@ -53,12 +53,12 @@ Take our typical `<SimpleCounter/>` example in its simplest form:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn SimpleCounter(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
pub fn SimpleCounter() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(0);
|
||||
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button on:click=increment>
|
||||
{value}
|
||||
</button>
|
||||
@@ -68,7 +68,7 @@ pub fn SimpleCounter(cx: Scope) -> impl IntoView {
|
||||
|
||||
The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`.
|
||||
|
||||
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in responsive to a change.
|
||||
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in response to a change.
|
||||
|
||||
So remember two things:
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ 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
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count(1);
|
||||
log!(count());
|
||||
```
|
||||
@@ -22,7 +22,7 @@ log!(count());
|
||||
is the same as
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count.set(1);
|
||||
log!(count.get());
|
||||
```
|
||||
@@ -36,7 +36,7 @@ However, there are some very good use cases for `.with()` and `.update()`.
|
||||
For example, consider a signal that holds a `Vec<String>`.
|
||||
|
||||
```rust
|
||||
let (names, set_names) = create_signal(cx, Vec::new());
|
||||
let (names, set_names) = create_signal(Vec::new());
|
||||
if names().is_empty() {
|
||||
set_names(vec!["Alice".to_string()]);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ In terms of logic, this is simple enough, but it’s hiding some significant ine
|
||||
Likewise, `set_names` replaces the value with a whole new `Vec<_>`. This is fine, but we might as well just mutate the original `Vec<_>` in place.
|
||||
|
||||
```rust
|
||||
let (names, set_names) = create_signal(cx, Vec::new());
|
||||
let (names, set_names) = create_signal(Vec::new());
|
||||
if names.with(|names| names.is_empty()) {
|
||||
set_names.update(|names| names.push("Alice".to_string()));
|
||||
}
|
||||
@@ -70,36 +70,42 @@ After all, `.with()` simply takes a function that takes the value by reference.
|
||||
Often people ask about situations in which some signal needs to change based on some other signal’s value. There are three good ways to do this, and one that’s less than ideal but okay under controlled circumstances.
|
||||
|
||||
### Good Options
|
||||
|
||||
**1) B is a function of A.** Create a signal for A and a derived signal or memo for B.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 1);
|
||||
let (count, set_count) = create_signal(1);
|
||||
let derived_signal_double_count = move || count() * 2;
|
||||
let memoized_double_count = create_memo(cx, move |_| count() * 2);
|
||||
let memoized_double_count = create_memo(move |_| count() * 2);
|
||||
```
|
||||
|
||||
> For guidance on whether to use a derived signal or a memo, see the docs for [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html)
|
||||
>
|
||||
**2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C.
|
||||
>
|
||||
> **2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C.
|
||||
|
||||
```rust
|
||||
let (first_name, set_first_name) = create_signal(cx, "Bridget".to_string());
|
||||
let (last_name, set_last_name) = create_signal(cx, "Jones".to_string());
|
||||
let (first_name, set_first_name) = create_signal("Bridget".to_string());
|
||||
let (last_name, set_last_name) = create_signal("Jones".to_string());
|
||||
let full_name = move || format!("{} {}", first_name(), last_name());
|
||||
```
|
||||
|
||||
**3) A and B are independent signals, but sometimes updated at the same time.** When you make the call to update A, make a separate call to update B.
|
||||
|
||||
```rust
|
||||
let (age, set_age) = create_signal(cx, 32);
|
||||
let (favorite_number, set_favorite_number) = create_signal(cx, 42);
|
||||
let (age, set_age) = create_signal(32);
|
||||
let (favorite_number, set_favorite_number) = create_signal(42);
|
||||
// use this to handle a click on a `Clear` button
|
||||
let clear_handler = move |_| {
|
||||
set_age(0);
|
||||
set_favorite_number(0);
|
||||
};
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use leptos_router::*;
|
||||
|
||||
Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, the rest of the app.
|
||||
|
||||
> You shouldn’t try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which ones decides what to do when the URL changes?
|
||||
> You shouldn’t try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which one decides what to do when the URL changes?
|
||||
|
||||
Let’s start with a simple `<App/>` component using the router:
|
||||
|
||||
@@ -33,8 +33,8 @@ use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
@@ -58,8 +58,8 @@ use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
@@ -83,19 +83,21 @@ The `path` can include
|
||||
- dynamic, named parameters beginning with a colon (`/:id`),
|
||||
- and/or a wildcard beginning with an asterisk (`/user/*any`)
|
||||
|
||||
The `view` is a function that takes a `Scope` and returns a view.
|
||||
The `view` is a function that returns a view. Any component with no props works here, as does a closure that returns some view.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home/> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users/> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile/> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound/> }/>
|
||||
<Route path="/" view=Home/>
|
||||
<Route path="/users" view=Users/>
|
||||
<Route path="/users/:id" view=UserProfile/>
|
||||
<Route path="/*any" view=|| view! { <h1>"Not Found"</h1> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The router scores each route to see how good a match it is, so you can define your routes in any order.
|
||||
> `view` takes a `Fn(Scope) -> impl IntoView`. If a component has no props, it is a function that takes `Scope` and returns `impl IntoView`, so it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|cx| view! { cx, <Home/> }`.
|
||||
|
||||
Now if you navigate to `/` or to `/users` you’ll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` you’ll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
|
||||
|
||||
Note that you can define your routes in any order. The router scores each route to see how good a match it is, rather than simply trying to match them top to bottom.
|
||||
|
||||
Simple enough?
|
||||
|
||||
@@ -4,10 +4,10 @@ We just defined the following set of routes:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
|
||||
<Route path="/" view=Home/>
|
||||
<Route path="/users" view=Users/>
|
||||
<Route path="/users/:id" view=UserProfile/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
@@ -17,11 +17,11 @@ Well... you can!
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="/" view=Home/>
|
||||
<Route path="/users" view=Users>
|
||||
<Route path=":id" view=UserProfile/>
|
||||
</Route>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
@@ -39,8 +39,8 @@ Let’s look back at our practical example.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="/users" view=Users/>
|
||||
<Route path="/users/:id" view=UserProfile/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
@@ -53,8 +53,8 @@ Let’s say I use nested routes instead:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="/users" view=Users>
|
||||
<Route path=":id" view=UserProfile/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
@@ -68,9 +68,9 @@ I actually need to add a fallback route
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="" view=|cx| view! { cx, <NoUser /> }/>
|
||||
<Route path="/users" view=Users>
|
||||
<Route path=":id" view=UserProfile/>
|
||||
<Route path="" view=NoUser/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
@@ -94,8 +94,8 @@ You can easily define this with nested routes
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }/>
|
||||
<Route path="/contacts" view=ContactList>
|
||||
<Route path=":id" view=ContactInfo/>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
@@ -107,13 +107,13 @@ You can go even deeper. Say you want to have tabs for each contact’s address,
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }>
|
||||
<Route path="" view=|cx| view! { cx, <EmailAndPhone/> }/>
|
||||
<Route path="address" view=|cx| view! { cx, <Address/> }/>
|
||||
<Route path="messages" view=|cx| view! { cx, <Messages/> }/>
|
||||
<Route path="/contacts" view=ContactList>
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=EmailAndPhone/>
|
||||
<Route path="address" view=Address/>
|
||||
<Route path="messages" view=Messages/>
|
||||
</Route>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Route path="" view=|| view! {
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
@@ -124,7 +124,7 @@ You can go even deeper. Say you want to have tabs for each contact’s address,
|
||||
|
||||
## `<Outlet/>`
|
||||
|
||||
Parent routes do not automatically render their nested routes. After all, they are just components; they don’t know exactly where they should render their children, and “just stick at at the end of the parent component” is not a great answer.
|
||||
Parent routes do not automatically render their nested routes. After all, they are just components; they don’t know exactly where they should render their children, and “just stick it at the end of the parent component” is not a great answer.
|
||||
|
||||
Instead, you tell a parent component where to render any nested components with an `<Outlet/>` component. The `<Outlet/>` simply renders one of two things:
|
||||
|
||||
@@ -135,15 +135,15 @@ That’s all! But it’s important to know and to remember, because it’s a com
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
pub fn ContactList() -> impl IntoView {
|
||||
let contacts = todo!();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div style="display: flex">
|
||||
// the contact list
|
||||
<For each=contacts
|
||||
key=|contact| contact.id
|
||||
view=|cx, contact| todo!()
|
||||
view=|contact| todo!()
|
||||
>
|
||||
// the nested child, if any
|
||||
// don’t forget this!
|
||||
@@ -170,3 +170,119 @@ In fact, in this case, we don’t even need to rerender the `<Contact/>` compone
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<h1>"Contact App"</h1>
|
||||
// this <nav> will show on every routes,
|
||||
// because it's outside the <Routes/>
|
||||
// note: we can just use normal <a> tags
|
||||
// and the router will use client-side navigation
|
||||
<nav>
|
||||
<h2>"Navigation"</h2>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/contacts">"Contacts"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
// / just has an un-nested "Home"
|
||||
<Route path="/" view=|| view! {
|
||||
<h3>"Home"</h3>
|
||||
}/>
|
||||
// /contacts has nested routes
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=ContactList
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<div class="tab">
|
||||
"(Contact Info)"
|
||||
</div>
|
||||
}/>
|
||||
<Route path="conversations" view=|| view! {
|
||||
<div class="tab">
|
||||
"(Conversations)"
|
||||
</div>
|
||||
}/>
|
||||
</Route>
|
||||
// if no id specified, fall back
|
||||
<Route path="" view=|| view! {
|
||||
<div class="select-user">
|
||||
"Select a user to view contact info."
|
||||
</div>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactList() -> impl IntoView {
|
||||
view! {
|
||||
<div class="contact-list">
|
||||
// here's our contact list component itself
|
||||
<div class="contact-list-contacts">
|
||||
<h3>"Contacts"</h3>
|
||||
<A href="alice">"Alice"</A>
|
||||
<A href="bob">"Bob"</A>
|
||||
<A href="steve">"Steve"</A>
|
||||
</div>
|
||||
|
||||
// <Outlet/> will show the nested child route
|
||||
// we can position this outlet wherever we want
|
||||
// within the layout
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactInfo() -> impl IntoView {
|
||||
// we can access the :id param reactively with `use_params_map`
|
||||
let params = use_params_map();
|
||||
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
|
||||
|
||||
// imagine we're loading data from an API here
|
||||
let name = move || match id().as_str() {
|
||||
"alice" => "Alice",
|
||||
"bob" => "Bob",
|
||||
"steve" => "Steve",
|
||||
_ => "User not found.",
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="contact-info">
|
||||
<h4>{name}</h4>
|
||||
<div class="tabs">
|
||||
<A href="" exact=true>"Contact Info"</A>
|
||||
<A href="conversations">"Conversations"</A>
|
||||
</div>
|
||||
|
||||
// <Outlet/> here is the tabs that are nested
|
||||
// underneath the /contacts/:id route
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -36,14 +36,22 @@ struct ContactSearch {
|
||||
```
|
||||
|
||||
> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure you’re importing the right one for the derive macro.
|
||||
>
|
||||
> If you are not using the `nightly` feature, you will get the error
|
||||
>
|
||||
> ```
|
||||
> no function or associated item named `into_param` found for struct `std::string::String` in the current scope
|
||||
> ```
|
||||
>
|
||||
> At the moment, supporting both `T: FromStr` and `Option<T>` for typed params requires a nightly feature. You can fix this by simply changing the struct to use `q: Option<String>` instead of `q: String`.
|
||||
|
||||
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
|
||||
|
||||
The typed versions return `Memo<Result<T, _>>`. It’s a Memo so it reacts to changes in the URL. It’s a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
|
||||
|
||||
```rust
|
||||
let params = use_params::<ContactParams>(cx);
|
||||
let query = use_query::<ContactSearch>(cx);
|
||||
let params = use_params::<ContactParams>();
|
||||
let query = use_query::<ContactSearch>();
|
||||
|
||||
// id: || -> usize
|
||||
let id = move || {
|
||||
@@ -58,8 +66,8 @@ let id = move || {
|
||||
The untyped versions return `Memo<ParamsMap>`. Again, it’s memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
|
||||
|
||||
```rust
|
||||
let params = use_params_map(cx);
|
||||
let query = use_query_map(cx);
|
||||
let params = use_params_map();
|
||||
let query = use_query_map();
|
||||
|
||||
// id: || -> Option<String>
|
||||
let id = move || {
|
||||
@@ -70,10 +78,126 @@ let id = move || {
|
||||
This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But it’s worth doing this for two reasons:
|
||||
|
||||
1. It’s correct, i.e., it forces you to consider the cases, “What if the user doesn’t pass a value for this query field? What if they pass an invalid value?”
|
||||
2. It’s performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replacing or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
|
||||
2. It’s performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replace or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
|
||||
|
||||
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explain them all yet.
|
||||
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explained them all yet.
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<h1>"Contact App"</h1>
|
||||
// this <nav> will show on every routes,
|
||||
// because it's outside the <Routes/>
|
||||
// note: we can just use normal <a> tags
|
||||
// and the router will use client-side navigation
|
||||
<nav>
|
||||
<h2>"Navigation"</h2>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/contacts">"Contacts"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
// / just has an un-nested "Home"
|
||||
<Route path="/" view=|| view! {
|
||||
<h3>"Home"</h3>
|
||||
}/>
|
||||
// /contacts has nested routes
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=ContactList
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<div class="tab">
|
||||
"(Contact Info)"
|
||||
</div>
|
||||
}/>
|
||||
<Route path="conversations" view=|| view! {
|
||||
<div class="tab">
|
||||
"(Conversations)"
|
||||
</div>
|
||||
}/>
|
||||
</Route>
|
||||
// if no id specified, fall back
|
||||
<Route path="" view=|| view! {
|
||||
<div class="select-user">
|
||||
"Select a user to view contact info."
|
||||
</div>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactList() -> impl IntoView {
|
||||
view! {
|
||||
<div class="contact-list">
|
||||
// here's our contact list component itself
|
||||
<div class="contact-list-contacts">
|
||||
<h3>"Contacts"</h3>
|
||||
<A href="alice">"Alice"</A>
|
||||
<A href="bob">"Bob"</A>
|
||||
<A href="steve">"Steve"</A>
|
||||
</div>
|
||||
|
||||
// <Outlet/> will show the nested child route
|
||||
// we can position this outlet wherever we want
|
||||
// within the layout
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactInfo() -> impl IntoView {
|
||||
// we can access the :id param reactively with `use_params_map`
|
||||
let params = use_params_map();
|
||||
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
|
||||
|
||||
// imagine we're loading data from an API here
|
||||
let name = move || match id().as_str() {
|
||||
"alice" => "Alice",
|
||||
"bob" => "Bob",
|
||||
"steve" => "Steve",
|
||||
_ => "User not found.",
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="contact-info">
|
||||
<h4>{name}</h4>
|
||||
<div class="tabs">
|
||||
<A href="" exact=true>"Contact Info"</A>
|
||||
<A href="conversations">"Conversations"</A>
|
||||
</div>
|
||||
|
||||
// <Outlet/> here is the tabs that are nested
|
||||
// underneath the /contacts/:id route
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -11,6 +11,8 @@ The router will bail out of handling an `<a>` click under a number of situations
|
||||
|
||||
In other words, the router will only try to do a client-side navigation when it’s pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior.
|
||||
|
||||
> This also means that if you need to opt out of client-side routing, you can do so easily. For example, if you have a link to another page on the same domain, but which isn’t part of your Leptos app, you can just use `<a rel="external">` to tell the router it isn’t something it can handle.
|
||||
|
||||
The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things:
|
||||
|
||||
1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears.
|
||||
@@ -21,3 +23,119 @@ The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<h1>"Contact App"</h1>
|
||||
// this <nav> will show on every routes,
|
||||
// because it's outside the <Routes/>
|
||||
// note: we can just use normal <a> tags
|
||||
// and the router will use client-side navigation
|
||||
<nav>
|
||||
<h2>"Navigation"</h2>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/contacts">"Contacts"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
// / just has an un-nested "Home"
|
||||
<Route path="/" view=|| view! {
|
||||
<h3>"Home"</h3>
|
||||
}/>
|
||||
// /contacts has nested routes
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=ContactList
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<div class="tab">
|
||||
"(Contact Info)"
|
||||
</div>
|
||||
}/>
|
||||
<Route path="conversations" view=|| view! {
|
||||
<div class="tab">
|
||||
"(Conversations)"
|
||||
</div>
|
||||
}/>
|
||||
</Route>
|
||||
// if no id specified, fall back
|
||||
<Route path="" view=|| view! {
|
||||
<div class="select-user">
|
||||
"Select a user to view contact info."
|
||||
</div>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactList() -> impl IntoView {
|
||||
view! {
|
||||
<div class="contact-list">
|
||||
// here's our contact list component itself
|
||||
<div class="contact-list-contacts">
|
||||
<h3>"Contacts"</h3>
|
||||
<A href="alice">"Alice"</A>
|
||||
<A href="bob">"Bob"</A>
|
||||
<A href="steve">"Steve"</A>
|
||||
</div>
|
||||
|
||||
// <Outlet/> will show the nested child route
|
||||
// we can position this outlet wherever we want
|
||||
// within the layout
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactInfo() -> impl IntoView {
|
||||
// we can access the :id param reactively with `use_params_map`
|
||||
let params = use_params_map();
|
||||
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
|
||||
|
||||
// imagine we're loading data from an API here
|
||||
let name = move || match id().as_str() {
|
||||
"alice" => "Alice",
|
||||
"bob" => "Bob",
|
||||
"steve" => "Steve",
|
||||
_ => "User not found.",
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="contact-info">
|
||||
<h4>{name}</h4>
|
||||
<div class="tabs">
|
||||
<A href="" exact=true>"Contact Info"</A>
|
||||
<A href="conversations">"Conversations"</A>
|
||||
</div>
|
||||
|
||||
// <Outlet/> here is the tabs that are nested
|
||||
// underneath the /contacts/:id route
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# The `<Form/>` Component
|
||||
|
||||
Links and forms sometimes seem completely unrelated. But in fact, they work in very similar ways.
|
||||
Links and forms sometimes seem completely unrelated. But, in fact, they work in very similar ways.
|
||||
|
||||
In plain HTML, there are three ways to navigate to another page:
|
||||
|
||||
1. An `<a>` element that links to another page. Navigates to the URL in its `href` attribute with the `GET` HTTP method.
|
||||
2. A `<form method="GET">`. Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
|
||||
3. A `<form method="POST">`. Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
|
||||
1. An `<a>` element that links to another page: Navigates to the URL in its `href` attribute with the `GET` HTTP method.
|
||||
2. A `<form method="GET">`: Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
|
||||
3. A `<form method="POST">`: Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
|
||||
|
||||
Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way.
|
||||
|
||||
@@ -24,15 +24,15 @@ async fn fetch_results() {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn FormExample(cx: Scope) -> impl IntoView {
|
||||
pub fn FormExample() -> impl IntoView {
|
||||
// reactive access to URL query strings
|
||||
let query = use_query_map(cx);
|
||||
let query = use_query_map();
|
||||
// search stored as ?q=
|
||||
let search = move || query().get("q").cloned().unwrap_or_default();
|
||||
// a resource driven by the search string
|
||||
let search_results = create_resource(cx, search, fetch_results);
|
||||
let search_results = create_resource(search, fetch_results);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search/>
|
||||
<input type="submit"/>
|
||||
@@ -51,7 +51,7 @@ This is a great pattern. The data flow is extremely clear: all data flows from t
|
||||
We can actually take it a step further and do something kind of clever:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search
|
||||
oninput="this.form.requestSubmit()"
|
||||
@@ -65,3 +65,117 @@ You’ll notice that this version drops the `Submit` button. Instead, we add an
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<h1><code>"<Form/>"</code></h1>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=FormExample/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn FormExample() -> impl IntoView {
|
||||
// reactive access to URL query
|
||||
let query = use_query_map();
|
||||
let name = move || query().get("name").cloned().unwrap_or_default();
|
||||
let number = move || query().get("number").cloned().unwrap_or_default();
|
||||
let select = move || query().get("select").cloned().unwrap_or_default();
|
||||
|
||||
view! {
|
||||
// read out the URL query strings
|
||||
<table>
|
||||
<tr>
|
||||
<td><code>"name"</code></td>
|
||||
<td>{name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>"number"</code></td>
|
||||
<td>{number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>"select"</code></td>
|
||||
<td>{select}</td>
|
||||
</tr>
|
||||
</table>
|
||||
// <Form/> will navigate whenever submitted
|
||||
<h2>"Manual Submission"</h2>
|
||||
<Form method="GET" action="">
|
||||
// input names determine query string key
|
||||
<input type="text" name="name" value=name/>
|
||||
<input type="number" name="number" value=number/>
|
||||
<select name="select">
|
||||
// `selected` will set which starts as selected
|
||||
<option selected=move || select() == "A">
|
||||
"A"
|
||||
</option>
|
||||
<option selected=move || select() == "B">
|
||||
"B"
|
||||
</option>
|
||||
<option selected=move || select() == "C">
|
||||
"C"
|
||||
</option>
|
||||
</select>
|
||||
// submitting should cause a client-side
|
||||
// navigation, not a full reload
|
||||
<input type="submit"/>
|
||||
</Form>
|
||||
// This <Form/> uses some JavaScript to submit
|
||||
// on every input
|
||||
<h2>"Automatic Submission"</h2>
|
||||
<Form method="GET" action="">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value=name
|
||||
// this oninput attribute will cause the
|
||||
// form to submit on every input to the field
|
||||
oninput="this.form.requestSubmit()"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
name="number"
|
||||
value=number
|
||||
oninput="this.form.requestSubmit()"
|
||||
/>
|
||||
<select name="select"
|
||||
onchange="this.form.requestSubmit()"
|
||||
>
|
||||
<option selected=move || select() == "A">
|
||||
"A"
|
||||
</option>
|
||||
<option selected=move || select() == "B">
|
||||
"B"
|
||||
</option>
|
||||
<option selected=move || select() == "C">
|
||||
"C"
|
||||
</option>
|
||||
</select>
|
||||
// submitting should cause a client-side
|
||||
// navigation, not a full reload
|
||||
<input type="submit"/>
|
||||
</Form>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -31,9 +31,9 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BusyButton(cx: Scope) -> impl IntoView {
|
||||
pub fn BusyButton() -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
|
||||
<button on:click=move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
@@ -97,6 +97,14 @@ In other words, you have two choices:
|
||||
|
||||
**But remember**: Leptos will handle all the details of this encoding and decoding for you. When you use a server function, it looks just like calling any other asynchronous function!
|
||||
|
||||
> **Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?**
|
||||
>
|
||||
> These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the JSON format.
|
||||
>
|
||||
> The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse, HTML forms don’t support `PUT` or `DELETE`, and they don’t support sending JSON. This means that if you use anything but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded. As we’ll see [in a later chapter](../progressive_enhancement), this isn’t always a great idea.
|
||||
>
|
||||
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
|
||||
|
||||
## An Important Note on Security
|
||||
|
||||
Server functions are a cool technology, but it’s very important to remember. **Server functions are not magic; they’re syntax sugar for defining a public API.** The _body_ of a server function is never made public; it’s just part of your server binary. But the server function is a publicly accessible API endpoint, and it’s return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.
|
||||
@@ -105,7 +113,7 @@ Server functions are a cool technology, but it’s very important to remember. *
|
||||
|
||||
So far, everything I’ve said is actually framework agnostic. (And in fact, the Leptos server function crate has been integrated into Dioxus as well!) Server functions are simply a way of defining a function-like RPC call that leans on Web standards like HTTP requests and URL encoding.
|
||||
|
||||
But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](../async/README.md). So you can easily integrate your server functions with the rest of your applications:
|
||||
But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](https://leptos-rs.github.io/leptos/async/index.html). So you can easily integrate your server functions with the rest of your applications:
|
||||
|
||||
- Create **resources** that call the server function to load data from the server
|
||||
- Read these resources under `<Suspense/>` or `<Transition/>` to enable streaming SSR and fallback states while data loads.
|
||||
|
||||
@@ -6,9 +6,9 @@ The server functions we looked at in the last chapter showed how to run code on
|
||||
|
||||
We call Leptos a “full-stack” framework, but “full-stack” is always a misnomer (after all, it never means everything from the browser to your power company.) For us, “full stack” means that your Leptos app can run in the browser, and can run on the server, and can integrate the two, drawing together the unique features available in each; as we’ve seen in the book so far, a button click on the browser can drive a database read on the server, both written in the same Rust module. But Leptos itself doesn’t provide the server (or the database, or the operating system, or the firmware, or the electrical cables...)
|
||||
|
||||
Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_actix/latest/leptos_axum/)). We’ve built integrations with each server’s router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls.
|
||||
Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/)). We’ve built integrations with each server’s router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls.
|
||||
|
||||
> If haven’t seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, now’s a good time to check them out.
|
||||
> If you haven’t seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, now’s a good time to check them out.
|
||||
|
||||
## Using Extractors
|
||||
|
||||
@@ -23,12 +23,12 @@ The [`extract` function in `leptos_actix`](https://docs.rs/leptos_actix/latest/l
|
||||
```rust
|
||||
|
||||
#[server(ActixExtract, "/api")]
|
||||
pub async fn actix_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
pub async fn actix_extract() -> Result<String, ServerFnError> {
|
||||
use leptos_actix::extract;
|
||||
use actix_web::dev::ConnectionInfo;
|
||||
use actix_web::web::{Data, Query};
|
||||
|
||||
extract(cx,
|
||||
extract(
|
||||
|search: Query<Search>, connection: ConnectionInfo| async move {
|
||||
format!(
|
||||
"search = {}\nconnection = {:?}",
|
||||
@@ -43,15 +43,15 @@ pub async fn actix_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
|
||||
## Axum Extractors
|
||||
|
||||
The syntax for the `leptos_axum::extract` function is very similar. (**Note**: This is available on the git main branch, but has not been released as of writing.) Note that Axum extractors return a `Result`, so you’ll need to add something to handle the error case.
|
||||
The syntax for the [`leptos_axum::extract`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract.html) function is very similar. (**Note**: This is available on the git main branch, but has not been released as of writing.) Note that Axum extractors return a `Result`, so you’ll need to add something to handle the error case.
|
||||
|
||||
```rust
|
||||
#[server(AxumExtract, "/api")]
|
||||
pub async fn axum_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
pub async fn axum_extract() -> Result<String, ServerFnError> {
|
||||
use axum::{extract::Query, http::Method};
|
||||
use leptos_axum::extract;
|
||||
|
||||
extract(cx, |method: Method, res: Query<MyQuery>| async move {
|
||||
extract(|method: Method, res: Query<MyQuery>| async move {
|
||||
format!("{method:?} and {}", res.q)
|
||||
},
|
||||
)
|
||||
@@ -62,6 +62,8 @@ pub async fn axum_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
|
||||
These are relatively simple examples accessing basic data from the server. But you can use extractors to access things like headers, cookies, database connection pools, and more, using the exact same `extract()` pattern.
|
||||
|
||||
> Note: For now, the Axum `extract` function only supports extractors for which the state is `()`, i.e., you can't yet use it to extract `State(_)`. You can access `State(_)` by using a custom handler that extracts the state and then provides it via context. [Click here for an example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/main.rs#L91-L92).
|
||||
|
||||
## A Note about Data-Loading Patterns
|
||||
|
||||
Because Actix and (especially) Axum are built on the idea of a single round-trip HTTP request and response, you typically run extractors near the “top” of your application (i.e., before you start rendering) and use the extracted data to determine how that should be rendered. Before you render a `<button>`, you load all the data your app could need. And any given route handler needs to know all the data that will need to be extracted by that route.
|
||||
|
||||
@@ -1 +1,74 @@
|
||||
# Responses and Redirects
|
||||
|
||||
Extractors provide an easy way to access request data inside server functions. Leptos also provides a way to modify the HTTP response, using the `ResponseOptions` type (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/struct.ResponseOptions.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/struct.ResponseOptions.html)) types and the `redirect` helper function (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/fn.redirect.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/fn.redirect.html)).
|
||||
|
||||
## `ResponseOptions`
|
||||
|
||||
`ResponseOptions` is provided via context during the initial server rendering response and during any subsequent server function call. It allows you to easily set the status code for the HTTP response, or to add headers to the HTTP response, e.g., to set cookies.
|
||||
|
||||
```rust
|
||||
#[server(TeaAndCookies)]
|
||||
pub async fn tea_and_cookies() -> Result<(), ServerFnError> {
|
||||
use actix_web::{cookie::Cookie, http::header, http::header::HeaderValue};
|
||||
use leptos_actix::ResponseOptions;
|
||||
|
||||
// pull ResponseOptions from context
|
||||
let response = expect_context::<ResponseOptions>();
|
||||
|
||||
// set the HTTP status code
|
||||
response.set_status(StatusCode::IM_A_TEAPOT);
|
||||
|
||||
// set a cookie in the HTTP response
|
||||
let mut cookie = Cookie::build("biscuits", "yes").finish();
|
||||
if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) {
|
||||
res.insert_header(header::SET_COOKIE, cookie);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `redirect`
|
||||
|
||||
One common modification to an HTTP response is to redirect to another page. The Actix and Axum integrations provide a `redirect` function to make this easy to do. `redirect` simply sets an HTTP status code of `302 Found` and sets the `Location` header.
|
||||
|
||||
Here’s a simplified example from our [`session_auth_axum` example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/auth.rs#L154-L181).
|
||||
|
||||
```rust
|
||||
#[server(Login, "/api")]
|
||||
pub async fn login(
|
||||
|
||||
username: String,
|
||||
password: String,
|
||||
remember: Option<String>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
// pull the DB pool and auth provider from context
|
||||
let pool = pool()?;
|
||||
let auth = auth()?;
|
||||
|
||||
// check whether the user exists
|
||||
let user: User = User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
ServerFnError::ServerError("User does not exist.".into())
|
||||
})?;
|
||||
|
||||
// check whether the user has provided the correct password
|
||||
match verify(password, &user.password)? {
|
||||
// if the password is correct...
|
||||
true => {
|
||||
// log the user in
|
||||
auth.login_user(user.id);
|
||||
auth.remember_user(remember.is_some());
|
||||
|
||||
// and redirect to the home page
|
||||
leptos_axum::redirect("/");
|
||||
Ok(())
|
||||
}
|
||||
// if not, return an error
|
||||
false => Err(ServerFnError::ServerError(
|
||||
"Password does not match.".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This server function can then be used from your application. This `redirect` works well with the progressively-enhanced `<ActionForm/>` component: without JS/WASM, the server response will redirect because of the status code and header. With JS/WASM, the `<ActionForm/>` will detect the redirect in the server function response, and use client-side navigation to redirect to the new page.
|
||||
|
||||
@@ -8,7 +8,12 @@ If you’ve ever listened to streaming music or watched a video online, I’m su
|
||||
|
||||
Let me say a little more about what I mean.
|
||||
|
||||
Leptos supports all four different of these different ways to render HTML that includes asynchronous data.
|
||||
Leptos supports all four different modes of rendering HTML that includes asynchronous data:
|
||||
|
||||
1. [Synchronous Rendering](#synchronous-rendering)
|
||||
1. [Async Rendering](#async-rendering)
|
||||
1. [In-Order streaming](#in-order-streaming)
|
||||
1. [Out-of-Order Streaming](#out-of-order-streaming)
|
||||
|
||||
## Synchronous Rendering
|
||||
|
||||
@@ -64,7 +69,7 @@ If you’re using server-side rendering, the synchronous mode is almost never wh
|
||||
|
||||
5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. If one of them reads from one or more “blocking resources” (see below), the fallback will not be sent; rather, the server will wait until that `<Suspense/>` has resolved and then replace the fallback with the resolved fragment on the server, which means that it is included in the initial HTML response and appears even if JavaScript is disabled or not supported. Other `<Suspense/>` stream in out of order as usual.
|
||||
|
||||
This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is *not* useful if there’s only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering.
|
||||
This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is _not_ useful if there’s only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering.
|
||||
|
||||
- _Pros_: Works if JavaScript is disabled or not supported on the user’s device.
|
||||
- _Cons_
|
||||
@@ -79,13 +84,13 @@ Because it offers the best blend of performance characteristics, Leptos defaults
|
||||
```rust
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
|
||||
<Route path="" view=HomePage/>
|
||||
|
||||
// We'll load the posts with async rendering, so they can set
|
||||
// the title and metadata *after* loading the data
|
||||
<Route
|
||||
path="/post/:id"
|
||||
view=|cx| view! { cx, <Post/> }
|
||||
view=Post
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
</Routes>
|
||||
@@ -105,14 +110,14 @@ With blocking resources, I can do something like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn BlogPost(cx: Scope) -> impl IntoView {
|
||||
let post_data = create_blocking_resource(cx, /* load blog post */);
|
||||
let comment_data = create_resource(cx, /* load blog post */);
|
||||
view! { cx,
|
||||
pub fn BlogPost() -> impl IntoView {
|
||||
let post_data = create_blocking_resource(/* load blog post */);
|
||||
let comment_data = create_resource(/* load blog post */);
|
||||
view! {
|
||||
<Suspense fallback=|| ()>
|
||||
{move || {
|
||||
post_data.with(cx, |data| {
|
||||
view! { cx,
|
||||
post_data.with(|data| {
|
||||
view! {
|
||||
<Title text=data.title/>
|
||||
<Meta name="description" content=data.excerpt/>
|
||||
<article>
|
||||
|
||||
@@ -8,7 +8,7 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
leptos::log!("where do I run?");
|
||||
// ... whatever
|
||||
}
|
||||
@@ -57,15 +57,15 @@ One way to create a bug is by creating a mismatch between the HTML that’s sent
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
let data = if cfg!(target_arch = "wasm32") {
|
||||
vec![0, 1, 2]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
data.into_iter()
|
||||
.map(|value| view! { cx, <span>{value}</span> })
|
||||
.collect_view(cx)
|
||||
.map(|value| view! { <span>{value}</span> })
|
||||
.collect_view()
|
||||
}
|
||||
```
|
||||
|
||||
@@ -74,11 +74,11 @@ In other words, if this is being compiled to WASM, it has three items; otherwise
|
||||
When I load the page in the browser, I see nothing. If I open the console I see a bunch of warnings:
|
||||
|
||||
```
|
||||
element with id 0-0-1 not found, ignoring it for hydration
|
||||
element with id 0-0-2 not found, ignoring it for hydration
|
||||
element with id 0-0-3 not found, ignoring it for hydration
|
||||
component with id _0-0-4c not found, ignoring it for hydration
|
||||
component with id _0-0-4o not found, ignoring it for hydration
|
||||
element with id 0-3 not found, ignoring it for hydration
|
||||
element with id 0-4 not found, ignoring it for hydration
|
||||
element with id 0-5 not found, ignoring it for hydration
|
||||
component with id _0-6c not found, ignoring it for hydration
|
||||
component with id _0-6o not found, ignoring it for hydration
|
||||
```
|
||||
|
||||
The WASM version of your app, running in the browser, expects to find three items; but the HTML has none.
|
||||
@@ -87,6 +87,56 @@ The WASM version of your app, running in the browser, expects to find three item
|
||||
|
||||
It’s pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If you’re seeing warnings like this and you don’t think it’s your fault, it’s much more likely that it’s a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
|
||||
|
||||
### Mutating the DOM during rendering
|
||||
|
||||
This is a slightly more common way to create a client/server mismatch: updating a signal _during rendering_ in a way that mutates the view.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (loaded, set_loaded) = create_signal(false);
|
||||
|
||||
// create_effect only runs on the client
|
||||
create_effect(move |_| {
|
||||
// do something like reading from localStorage
|
||||
set_loaded(true);
|
||||
});
|
||||
|
||||
move || {
|
||||
if loaded() {
|
||||
view! { <p>"Hello, world!"</p> }.into_any()
|
||||
} else {
|
||||
view! { <div class="loading">"Loading..."</div> }.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This one gives us the scary panic
|
||||
|
||||
```
|
||||
panicked at 'assertion failed: `(left == right)`
|
||||
left: `"DIV"`,
|
||||
right: `"P"`: SSR and CSR elements have the same hydration key but different node kinds.
|
||||
```
|
||||
|
||||
And a handy link to this page!
|
||||
|
||||
The problem here is that `create_effect` runs **immediately** and **synchronously**, but only in the browser. As a result, on the server, `loaded` is false, and a `<div>` is rendered. But on the browser, by the time the view is being rendered, `loaded` has already been set to `true`, and the browser is expecting to find a `<p>`.
|
||||
|
||||
#### Solution
|
||||
|
||||
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.
|
||||
|
||||
```rust
|
||||
create_effect(move |_| {
|
||||
// do something like reading from localStorage
|
||||
request_animation_frame(move || set_loaded(true));
|
||||
});
|
||||
```
|
||||
|
||||
This allows the browser to hydrate with the correct, matching state (`loaded` is `false` when it reaches the view), then immediately update it to `true` once hydration is complete.
|
||||
|
||||
### Not all client code can run on the server
|
||||
|
||||
Imagine you happily import a dependency like `gloo-net` that you’ve been used to using to make requests in the browser, and use it in a `create_resource` in a server-rendered app.
|
||||
@@ -113,7 +163,7 @@ For example, say that I want to store something in the browser’s `localStorage
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
@@ -126,9 +176,9 @@ But if I wrap it in an effect...
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ For example, instead of embedding logic in a component directly like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(vec![Todo { /* ... */ }]);
|
||||
// ⚠️ this is hard to test because it's embedded in the component
|
||||
let num_remaining = move || todos.with(|todos| {
|
||||
todos.iter().filter(|todo| !todo.completed).sum()
|
||||
@@ -43,8 +43,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(Todos(vec![Todo { /* ... */ }]));
|
||||
// ✅ this has a test associated with it
|
||||
let num_remaining = move || todos.with(Todos::num_remaining);
|
||||
}
|
||||
@@ -76,7 +76,7 @@ wasm-pack test --firefox
|
||||
### Writing Your Tests
|
||||
|
||||
Most tests will involve some combination of vanilla DOM manipulation and comparison to a `view`. For example, here’s a test [for the
|
||||
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/mod.rs).
|
||||
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/web.rs).
|
||||
|
||||
First, we set up the testing environment.
|
||||
|
||||
@@ -105,7 +105,7 @@ fn clear() {
|
||||
// note that we start at the initial value of 10
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
|| view! { <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -139,12 +139,12 @@ I like to test the whole view at once. We can do this by testing the element’s
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
run_scope(create_runtime(), || {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (value, set_value) = create_signal(0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
@@ -169,7 +169,7 @@ assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
|| view! { <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
@@ -22,13 +22,13 @@ I’ll give you the whole thing up front, then walk through it line by line.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
set_count(3);
|
||||
}
|
||||
>
|
||||
"Click me: "
|
||||
@@ -49,18 +49,17 @@ used as a component in your Leptos application. We’ll see some of the other fe
|
||||
this macro in a couple chapters.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope) -> impl IntoView
|
||||
fn App() -> impl IntoView
|
||||
```
|
||||
|
||||
Every component is a function with the following characteristics
|
||||
|
||||
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||
as its first argument. This `Scope` is our entrypoint into the reactive system.
|
||||
By convention, it’s usually named `cx`.
|
||||
2. You can include other arguments, which will be available as component “props.”
|
||||
3. Component functions return `impl IntoView`, which is an opaque type that includes
|
||||
1. It takes zero or more arguments of any type.
|
||||
2. It returns `impl IntoView`, which is an opaque type that includes
|
||||
anything you could return from a Leptos `view`.
|
||||
|
||||
> Component function arguments are gathered together into a single props struct which is built by the `view` macro as needed.
|
||||
|
||||
## The Component Body
|
||||
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
@@ -69,7 +68,7 @@ few reactive variables, define any side effects that run in response to those va
|
||||
changing, and describe the user interface.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
```
|
||||
|
||||
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
|
||||
@@ -85,7 +84,7 @@ current value, you’ll call `set_count.set(...)` (or `set_count(...)`).
|
||||
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
// define an event listener with on:
|
||||
on:click=move |_| {
|
||||
@@ -127,7 +126,7 @@ Leptos with `nightly` Rust, signals are already functions, so the closure is unn
|
||||
As a result, you can write a simpler view:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<button /* ... */>
|
||||
"Click me: "
|
||||
// identical to {move || count.get()}
|
||||
@@ -139,10 +138,10 @@ view! { cx,
|
||||
Remember—and this is _very important_—only functions are reactive. This means that
|
||||
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||
in a function, telling the framework to update the view every time `count` changes.
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
`{count()}` accesses the value of `count` once, and passes an `i32` into the view,
|
||||
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
|
||||
|
||||
Let’s make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Let’s 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 |_| {
|
||||
@@ -160,3 +159,67 @@ Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer de
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
// The #[component] macro marks a function as a reusable component
|
||||
// Components are the building blocks of your user interface
|
||||
// They define a reusable unit of behavior
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
// here we create a reactive signal
|
||||
// and get a (getter, setter) pair
|
||||
// signals are the basic unit of change in the framework
|
||||
// we'll talk more about them later
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// the `view` macro is how we define the user interface
|
||||
// it uses an HTML-like format that can accept certain Rust values
|
||||
view! {
|
||||
<button
|
||||
// on:click will run whenever the `click` event fires
|
||||
// every event handler is defined as `on:{eventname}`
|
||||
|
||||
// we're able to move `set_count` into the closure
|
||||
// because signals are Copy and 'static
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
// text nodes in RSX should be wrapped in quotes,
|
||||
// like a normal Rust string
|
||||
"Click me"
|
||||
</button>
|
||||
<p>
|
||||
<strong>"Reactive: "</strong>
|
||||
// you can insert Rust expressions as values in the DOM
|
||||
// by wrapping them in curly braces
|
||||
// if you pass in a function, it will reactively update
|
||||
{move || count.get()}
|
||||
</p>
|
||||
<p>
|
||||
<strong>"Reactive shorthand: "</strong>
|
||||
// signals are functions, so we can remove the wrapping closure
|
||||
{count}
|
||||
</p>
|
||||
<p>
|
||||
<strong>"Not reactive: "</strong>
|
||||
// NOTE: if you write {count()}, this will *not* be reactive
|
||||
// it simply gets the value of count once
|
||||
{count()}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
// This `main` function is the entry point into the app
|
||||
// It just mounts our component to the <body>
|
||||
// Because we defined it as `fn App`, we can now use it in a
|
||||
// template as <App/>
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
@@ -12,10 +12,10 @@ increment a counter.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
@@ -73,9 +73,9 @@ class=("button-20", move || count() % 2 == 1)
|
||||
Individual CSS properties can be directly updated with a similar `style:` syntax.
|
||||
|
||||
```rust
|
||||
let (x, set_x) = create_signal(cx, 0);
|
||||
let (y, set_y) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
let (x, set_x) = create_signal(0);
|
||||
let (y, set_y) = create_signal(0);
|
||||
view! {
|
||||
<div
|
||||
style="position: absolute"
|
||||
style:left=move || format!("{}px", x() + 100)
|
||||
@@ -152,3 +152,67 @@ places in your application with minimal overhead.
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>Code Sandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// a "derived signal" is a function that accesses other signals
|
||||
// we can use this to create reactive values that depend on the
|
||||
// values of one or more other signals
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
// the class: syntax reactively updates a single class
|
||||
// here, we'll set the `red` class when `count` is odd
|
||||
class:red=move || count() % 2 == 1
|
||||
>
|
||||
"Click me"
|
||||
</button>
|
||||
// NOTE: self-closing tags like <br> need an explicit /
|
||||
<br/>
|
||||
|
||||
// We'll update this progress bar every time `count` changes
|
||||
<progress
|
||||
// static attributes work as in HTML
|
||||
max="50"
|
||||
|
||||
// passing a function to an attribute
|
||||
// reactively sets that attribute
|
||||
// signals are functions, so this <=> `move || count.get()`
|
||||
value=count
|
||||
>
|
||||
</progress>
|
||||
<br/>
|
||||
|
||||
// This progress bar will use `double_count`
|
||||
// so it should move twice as fast!
|
||||
<progress
|
||||
max="50"
|
||||
// derived signals are functions, so they can also
|
||||
// reactive update the DOM
|
||||
value=double_count
|
||||
>
|
||||
</progress>
|
||||
<p>"Count: " {count}</p>
|
||||
<p>"Double Count: " {double_count}</p>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -12,10 +12,10 @@ per click.
|
||||
You _could_ do this by just creating two `<progress>` elements:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
value=count
|
||||
@@ -36,9 +36,9 @@ Instead, let’s create a `<ProgressBar/>` component.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope
|
||||
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
// hmm... where will we get this from?
|
||||
@@ -64,10 +64,10 @@ In Leptos, you define props by giving additional arguments to the component func
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
// now this works
|
||||
@@ -81,9 +81,9 @@ Now we can use our component in the main `<App/>` component’s view.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
view! {
|
||||
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||
"Click me"
|
||||
</button>
|
||||
@@ -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
|
||||
@@ -130,14 +118,14 @@ argument to the component function with `#[prop(optional)]`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
max: u16,
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
@@ -161,12 +149,12 @@ with `#[prop(default = ...)`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
@@ -183,11 +171,11 @@ as the `progress` prop on another `<ProgressBar/>`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||
"Click me"
|
||||
</button>
|
||||
@@ -211,7 +199,7 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar<F>(
|
||||
cx: Scope,
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
@@ -219,7 +207,7 @@ fn ProgressBar<F>(
|
||||
where
|
||||
F: Fn() -> i32 + 'static,
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
@@ -231,9 +219,25 @@ where
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
any value that implements this `Fn()` trait.
|
||||
|
||||
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
|
||||
> or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
> a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
This generic can also be specified inline:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar<F: Fn() -> i32 + 'static>(
|
||||
cx: Scope,
|
||||
#[prop(default = 100)] max: u16,
|
||||
progress: F,
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Note that generic component props _can’t_ be specified with an `impl` yet (`progress: impl Fn() -> i32 + 'static,`), in part because they’re actually used to generate a `struct ProgressBarProps`, and struct fields cannot be `impl` types. The `#[component]` macro may be further improved in the future to allow inline `impl` generic props.
|
||||
|
||||
### `into` Props
|
||||
|
||||
@@ -251,14 +255,14 @@ reactive value.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
progress: Signal<i32>
|
||||
) -> impl IntoView
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
@@ -267,22 +271,97 @@ fn ProgressBar(
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||
"Click me"
|
||||
</button>
|
||||
// .into() converts `ReadSignal` to `Signal`
|
||||
<ProgressBar progress=count/>
|
||||
// use `Signal::derive()` to wrap a derived signal
|
||||
<ProgressBar progress=Signal::derive(cx, double_count)/>
|
||||
<ProgressBar progress=Signal::derive(double_count)/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Generic Props
|
||||
|
||||
Note that you can’t specify optional generic props for a component. Let’s see what would happen if you try:
|
||||
|
||||
```rust,compile_fail
|
||||
#[component]
|
||||
fn ProgressBar<F: Fn() -> i32 + 'static>(
|
||||
cx: Scope,
|
||||
#[prop(optional)] progress: Option<F>,
|
||||
) -> impl IntoView {
|
||||
progress.map(|progress| {
|
||||
view! { cx,
|
||||
<progress
|
||||
max=100
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<ProgressBar/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rust helpfully gives the error
|
||||
|
||||
```
|
||||
xx | <ProgressBar/>
|
||||
| ^^^^^^^^^^^ cannot infer type of the type parameter `F` declared on the function `ProgressBar`
|
||||
|
|
||||
help: consider specifying the generic argument
|
||||
|
|
||||
xx | <ProgressBar::<F>/>
|
||||
| +++++
|
||||
```
|
||||
|
||||
There are just two problems:
|
||||
|
||||
1. Leptos’s view macro doesn’t support specifying a generic on a component with this turbofish syntax.
|
||||
2. Even if you could, specifying the correct type here is not possible; closures and functions in general are unnameable types. The compiler can display them with a shorthand, but you can’t specify them.
|
||||
|
||||
However, you can get around this by providing a concrete type using `Box<dyn _>` or `&dyn _`:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
#[prop(optional)] progress: Option<Box<dyn Fn() -> i32>>,
|
||||
) -> impl IntoView {
|
||||
progress.map(|progress| {
|
||||
view! { cx,
|
||||
<progress
|
||||
max=100
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<ProgressBar/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because the Rust compiler now knows the concrete type of the prop, and therefore its size in memory even in the `None` case, this compiles fine.
|
||||
|
||||
> In this particular case, `&dyn Fn() -> i32` will cause lifetime issues, but in other cases, it may be a possibility.
|
||||
|
||||
## Documenting Components
|
||||
|
||||
This is one of the least essential but most important sections of this book.
|
||||
@@ -297,7 +376,7 @@ component function, and each one of the props:
|
||||
/// Shows progress toward a goal.
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
|
||||
/// The maximum value of the progress bar.
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
@@ -321,3 +400,75 @@ and see the power of the `#[component]` macro combined with rust-analyzer here.
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
// Composing different components together is how we build
|
||||
// user interfaces. Here, we'll define a resuable <ProgressBar/>.
|
||||
// You'll see how doc comments can be used to document components
|
||||
// and their properties.
|
||||
|
||||
/// Shows progress toward a goal.
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
// Marks this as an optional prop. It will default to the default
|
||||
// value of its type, i.e., 0.
|
||||
#[prop(default = 100)]
|
||||
/// The maximum value of the progress bar.
|
||||
max: u16,
|
||||
// Will run `.into()` on the value passed into the prop.
|
||||
#[prop(into)]
|
||||
// `Signal<T>` is a wrapper for several reactive types.
|
||||
// It can be helpful in component APIs like this, where we
|
||||
// might want to take any kind of reactive value
|
||||
/// How much progress should be displayed.
|
||||
progress: Signal<i32>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<progress
|
||||
max={max}
|
||||
value=progress
|
||||
/>
|
||||
<br/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
"Click me"
|
||||
</button>
|
||||
<br/>
|
||||
// If you have this open in CodeSandbox or an editor with
|
||||
// rust-analyzer support, try hovering over `ProgressBar`,
|
||||
// `max`, or `progress` to see the docs we defined above
|
||||
<ProgressBar max=50 progress=count/>
|
||||
// Let's use the default max value on this one
|
||||
// the default is 100, so it should move half as fast
|
||||
<ProgressBar progress=count/>
|
||||
// Signal::derive creates a Signal wrapper from our derived signal
|
||||
// using double_count means it should move twice as fast
|
||||
<ProgressBar max=50 progress=Signal::derive(double_count)/>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -19,30 +19,30 @@ any `Vec<IV> where IV: IntoView` into your view. In other words, if you can rend
|
||||
|
||||
```rust
|
||||
let values = vec![0, 1, 2];
|
||||
view! { cx,
|
||||
view! {
|
||||
// this will just render "012"
|
||||
<p>{values.clone()}</p>
|
||||
// or we can wrap them in <li>
|
||||
<ul>
|
||||
{values.into_iter()
|
||||
.map(|n| view! { cx, <li>{n}</li>})
|
||||
.map(|n| view! { <li>{n}</li>})
|
||||
.collect::<Vec<_>>()}
|
||||
</ul>
|
||||
}
|
||||
```
|
||||
|
||||
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
|
||||
Leptos also provides a `.collect_view()` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
|
||||
|
||||
```rust
|
||||
let values = vec![0, 1, 2];
|
||||
view! { cx,
|
||||
view! {
|
||||
// this will just render "012"
|
||||
<p>{values.clone()}</p>
|
||||
// or we can wrap them in <li>
|
||||
<ul>
|
||||
{values.into_iter()
|
||||
.map(|n| view! { cx, <li>{n}</li>})
|
||||
.collect_view(cx)}
|
||||
.map(|n| view! { <li>{n}</li>})
|
||||
.collect_view()}
|
||||
</ul>
|
||||
}
|
||||
```
|
||||
@@ -52,13 +52,13 @@ You can render dynamic items as part of a static list.
|
||||
|
||||
```rust
|
||||
// create a list of N signals
|
||||
let counters = (1..=length).map(|idx| create_signal(cx, idx));
|
||||
let counters = (1..=length).map(|idx| create_signal(idx));
|
||||
|
||||
// each item manages a reactive view
|
||||
// but the list itself will never change
|
||||
let counter_buttons = counters
|
||||
.map(|(count, set_count)| {
|
||||
view! { cx,
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
on:click=move |_| set_count.update(|n| *n += 1)
|
||||
@@ -68,9 +68,9 @@ let counter_buttons = counters
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx);
|
||||
.collect_view();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<ul>{counter_buttons}</ul>
|
||||
}
|
||||
```
|
||||
@@ -106,3 +106,162 @@ Check out the `<DynamicList/>` component below for an example.
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
// Iteration is a very common task in most applications.
|
||||
// So how do you take a list of data and render it in the DOM?
|
||||
// This example will show you the two ways:
|
||||
// 1) for mostly-static lists, using Rust iterators
|
||||
// 2) for lists that grow, shrink, or move items, using <For/>
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Iteration"</h1>
|
||||
<h2>"Static List"</h2>
|
||||
<p>"Use this pattern if the list itself is static."</p>
|
||||
<StaticList length=5/>
|
||||
<h2>"Dynamic List"</h2>
|
||||
<p>"Use this pattern if the rows in your list will change."</p>
|
||||
<DynamicList initial_length=5/>
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of counters, without the ability
|
||||
/// to add or remove any.
|
||||
#[component]
|
||||
fn StaticList(
|
||||
|
||||
/// How many counters to include in this list.
|
||||
length: usize,
|
||||
) -> impl IntoView {
|
||||
// create counter signals that start at incrementing numbers
|
||||
let counters = (1..=length).map(|idx| create_signal(idx));
|
||||
|
||||
// when you have a list that doesn't change, you can
|
||||
// manipulate it using ordinary Rust iterators
|
||||
// and collect it into a Vec<_> to insert it into the DOM
|
||||
let counter_buttons = counters
|
||||
.map(|(count, set_count)| {
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
on:click=move |_| set_count.update(|n| *n += 1)
|
||||
>
|
||||
{count}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Note that if `counter_buttons` were a reactive list
|
||||
// and its value changed, this would be very inefficient:
|
||||
// it would rerender every row every time the list changed.
|
||||
view! {
|
||||
<ul>{counter_buttons}</ul>
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of counters that allows you to add or
|
||||
/// remove counters.
|
||||
#[component]
|
||||
fn DynamicList(
|
||||
|
||||
/// The number of counters to begin with.
|
||||
initial_length: usize,
|
||||
) -> impl IntoView {
|
||||
// This dynamic list will use the <For/> component.
|
||||
// <For/> is a keyed list. This means that each row
|
||||
// has a defined key. If the key does not change, the row
|
||||
// will not be re-rendered. When the list changes, only
|
||||
// the minimum number of changes will be made to the DOM.
|
||||
|
||||
// `next_counter_id` will let us generate unique IDs
|
||||
// we do this by simply incrementing the ID by one
|
||||
// each time we create a counter
|
||||
let mut next_counter_id = initial_length;
|
||||
|
||||
// we generate an initial list as in <StaticList/>
|
||||
// but this time we include the ID along with the signal
|
||||
let initial_counters = (0..initial_length)
|
||||
.map(|id| (id, create_signal(id + 1)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// now we store that initial list in a signal
|
||||
// this way, we'll be able to modify the list over time,
|
||||
// adding and removing counters, and it will change reactively
|
||||
let (counters, set_counters) = create_signal(initial_counters);
|
||||
|
||||
let add_counter = move |_| {
|
||||
// create a signal for the new counter
|
||||
let sig = create_signal(next_counter_id + 1);
|
||||
// add this counter to the list of counters
|
||||
set_counters.update(move |counters| {
|
||||
// since `.update()` gives us `&mut T`
|
||||
// we can just use normal Vec methods like `push`
|
||||
counters.push((next_counter_id, sig))
|
||||
});
|
||||
// increment the ID so it's always unique
|
||||
next_counter_id += 1;
|
||||
};
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<ul>
|
||||
// The <For/> component is central here
|
||||
// This allows for efficient, key list rendering
|
||||
<For
|
||||
// `each` takes any function that returns an iterator
|
||||
// this should usually be a signal or derived signal
|
||||
// if it's not reactive, just render a Vec<_> instead of <For/>
|
||||
each=counters
|
||||
// the key should be unique and stable for each row
|
||||
// using an index is usually a bad idea, unless your list
|
||||
// can only grow, because moving items around inside the list
|
||||
// means their indices will change and they will all rerender
|
||||
key=|counter| counter.0
|
||||
// the view function receives each item from your `each` iterator
|
||||
// and returns a view
|
||||
view=move |(id, (count, set_count))| {
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
on:click=move |_| set_count.update(|n| *n += 1)
|
||||
>
|
||||
{count}
|
||||
</button>
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_counters.update(|counters| {
|
||||
counters.retain(|(counter_id, _)| counter_id != &id)
|
||||
});
|
||||
}
|
||||
>
|
||||
"Remove"
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -22,9 +22,9 @@ There are two important things to remember:
|
||||
`prop:value` for this reason.
|
||||
|
||||
```rust
|
||||
let (name, set_name) = create_signal(cx, "Controlled".to_string());
|
||||
let (name, set_name) = create_signal("Controlled".to_string());
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<input type="text"
|
||||
on:input=move |ev| {
|
||||
// event_target_value is a Leptos helper function
|
||||
@@ -53,9 +53,9 @@ In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
event.
|
||||
|
||||
```rust
|
||||
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||
let (name, set_name) = create_signal("Uncontrolled".to_string());
|
||||
|
||||
let input_element: NodeRef<Input> = create_node_ref(cx);
|
||||
let input_element: NodeRef<Input> = create_node_ref();
|
||||
```
|
||||
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
@@ -89,7 +89,7 @@ We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
gives us access to a correctly-typed HTML element.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<form on:submit=on_submit>
|
||||
<input type="text"
|
||||
value=name
|
||||
@@ -112,3 +112,112 @@ The view should be pretty self-explanatory by now. Note two things:
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::{ev::SubmitEvent, *};
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<h2>"Controlled Component"</h2>
|
||||
<ControlledComponent/>
|
||||
<h2>"Uncontrolled Component"</h2>
|
||||
<UncontrolledComponent/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ControlledComponent() -> impl IntoView {
|
||||
// create a signal to hold the value
|
||||
let (name, set_name) = create_signal("Controlled".to_string());
|
||||
|
||||
view! {
|
||||
<input type="text"
|
||||
// fire an event whenever the input changes
|
||||
on:input=move |ev| {
|
||||
// event_target_value is a Leptos helper function
|
||||
// it functions the same way as event.target.value
|
||||
// in JavaScript, but smooths out some of the typecasting
|
||||
// necessary to make this work in Rust
|
||||
set_name(event_target_value(&ev));
|
||||
}
|
||||
|
||||
// the `prop:` syntax lets you update a DOM property,
|
||||
// rather than an attribute.
|
||||
//
|
||||
// IMPORTANT: the `value` *attribute* only sets the
|
||||
// initial value, until you have made a change.
|
||||
// The `value` *property* sets the current value.
|
||||
// This is a quirk of the DOM; I didn't invent it.
|
||||
// Other frameworks gloss this over; I think it's
|
||||
// more important to give you access to the browser
|
||||
// as it really works.
|
||||
//
|
||||
// tl;dr: use prop:value for form inputs
|
||||
prop:value=name
|
||||
/>
|
||||
<p>"Name is: " {name}</p>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn UncontrolledComponent() -> impl IntoView {
|
||||
// import the type for <input>
|
||||
use leptos::html::Input;
|
||||
|
||||
let (name, set_name) = create_signal("Uncontrolled".to_string());
|
||||
|
||||
// we'll use a NodeRef to store a reference to the input element
|
||||
// this will be filled when the element is created
|
||||
let input_element: NodeRef<Input> = create_node_ref();
|
||||
|
||||
// fires when the form `submit` event happens
|
||||
// this will store the value of the <input> in our signal
|
||||
let on_submit = move |ev: SubmitEvent| {
|
||||
// stop the page from reloading!
|
||||
ev.prevent_default();
|
||||
|
||||
// here, we'll extract the value from the input
|
||||
let value = input_element()
|
||||
// event handlers can only fire after the view
|
||||
// is mounted to the DOM, so the `NodeRef` will be `Some`
|
||||
.expect("<input> to exist")
|
||||
// `NodeRef` implements `Deref` for the DOM element type
|
||||
// this means we can call`HtmlInputElement::value()`
|
||||
// to get the current value of the input
|
||||
.value();
|
||||
set_name(value);
|
||||
};
|
||||
|
||||
view! {
|
||||
<form on:submit=on_submit>
|
||||
<input type="text"
|
||||
// here, we use the `value` *attribute* to set only
|
||||
// the initial value, letting the browser maintain
|
||||
// the state after that
|
||||
value=name
|
||||
|
||||
// store a reference to this input in `input_element`
|
||||
node_ref=input_element
|
||||
/>
|
||||
<input type="submit" value="Submit"/>
|
||||
</form>
|
||||
<p>"Name is: " {name}</p>
|
||||
}
|
||||
}
|
||||
|
||||
// This `main` function is the entry point into the app
|
||||
// It just mounts our component to the <body>
|
||||
// Because we defined it as `fn App`, we can now use it in a
|
||||
// template as <App/>
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -38,7 +38,7 @@ special knowledge.
|
||||
For example, let’s start with a simple signal and derived signal:
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (value, set_value) = create_signal(0);
|
||||
let is_odd = move || value() & 1 == 1;
|
||||
```
|
||||
|
||||
@@ -54,7 +54,7 @@ Let’s say I want to render some text if the number is odd, and some other text
|
||||
if it’s even. Well, how about this?
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>
|
||||
{move || if is_odd() {
|
||||
"Odd"
|
||||
@@ -81,7 +81,7 @@ let message = move || {
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
@@ -90,7 +90,7 @@ This works fine. We can make it a little shorter if we’d like, using `bool::th
|
||||
|
||||
```rust
|
||||
let message = move || is_odd().then(|| "Ding ding ding!");
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
@@ -112,7 +112,7 @@ let message = move || {
|
||||
_ => "Even"
|
||||
}
|
||||
};
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
@@ -131,7 +131,7 @@ above, where the value switches from even to odd on every change, this is fine.
|
||||
But consider the following example:
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (value, set_value) = create_signal(0);
|
||||
|
||||
let message = move || if value() > 5 {
|
||||
"Big"
|
||||
@@ -139,7 +139,7 @@ let message = move || if value() > 5 {
|
||||
"Small"
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
@@ -194,12 +194,12 @@ the answer. You pass it a `when` condition function, a `fallback` to be shown if
|
||||
the `when` function returns `false`, and children to be rendered if `when` is `true`.
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (value, set_value) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<Show
|
||||
when=move || value() > 5
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
when=move || { value() > 5 }
|
||||
fallback=|| view! { <Small/> }
|
||||
>
|
||||
<Big/>
|
||||
</Show>
|
||||
@@ -227,19 +227,19 @@ can be a little annoying if you’re returning different HTML elements from
|
||||
different branches of a conditional:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
view! {
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }
|
||||
view! { <pre>"One"</pre> }
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }
|
||||
view! { <p>"Two"</p> }
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }
|
||||
_ => view! { <textarea>{value()}</textarea> }
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
@@ -259,24 +259,24 @@ to get yourself out of this situation:
|
||||
1. If you have multiple `HtmlElement` types, convert them to `HtmlElement<AnyElement>`
|
||||
with [`.into_any()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.into_any)
|
||||
2. If you have a variety of view types that are not all `HtmlElement`, convert them to
|
||||
`View`s with [`.into_view(cx)`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
|
||||
`View`s with [`.into_view()`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
|
||||
|
||||
Here’s the same example, with the conversion added:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
view! {
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }.into_any()
|
||||
view! { <pre>"One"</pre> }.into_any()
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }.into_any()
|
||||
view! { <p>"Two"</p> }.into_any()
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
|
||||
_ => view! { <textarea>{value()}</textarea> }.into_any()
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
@@ -285,3 +285,100 @@ view! { cx,
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(0);
|
||||
let is_odd = move || value() & 1 == 1;
|
||||
let odd_text = move || if is_odd() { Some("How odd!") } else { None };
|
||||
|
||||
view! {
|
||||
<h1>"Control Flow"</h1>
|
||||
|
||||
// Simple UI to update and show a value
|
||||
<button on:click=move |_| set_value.update(|n| *n += 1)>
|
||||
"+1"
|
||||
</button>
|
||||
<p>"Value is: " {value}</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2><code>"Option<T>"</code></h2>
|
||||
// For any `T` that implements `IntoView`,
|
||||
// so does `Option<T>`
|
||||
|
||||
<p>{odd_text}</p>
|
||||
// This means you can use `Option` methods on it
|
||||
<p>{move || odd_text().map(|text| text.len())}</p>
|
||||
|
||||
<h2>"Conditional Logic"</h2>
|
||||
// You can do dynamic conditional if-then-else
|
||||
// logic in several ways
|
||||
//
|
||||
// a. An "if" expression in a function
|
||||
// This will simply re-render every time the value
|
||||
// changes, which makes it good for lightweight UI
|
||||
<p>
|
||||
{move || if is_odd() {
|
||||
"Odd"
|
||||
} else {
|
||||
"Even"
|
||||
}}
|
||||
</p>
|
||||
|
||||
// b. Toggling some kind of class
|
||||
// This is smart for an element that's going to
|
||||
// toggled often, because it doesn't destroy
|
||||
// it in between states
|
||||
// (you can find the `hidden` class in `index.html`)
|
||||
<p class:hidden=is_odd>"Appears if even."</p>
|
||||
|
||||
// c. The <Show/> component
|
||||
// This only renders the fallback and the child
|
||||
// once, lazily, and toggles between them when
|
||||
// needed. This makes it more efficient in many cases
|
||||
// than a {move || if ...} block
|
||||
<Show when=is_odd
|
||||
fallback=|| view! { <p>"Even steven"</p> }
|
||||
>
|
||||
<p>"Oddment"</p>
|
||||
</Show>
|
||||
|
||||
// d. Because `bool::then()` converts a `bool` to
|
||||
// `Option`, you can use it to create a show/hide toggled
|
||||
{move || is_odd().then(|| view! { <p>"Oddity!"</p> })}
|
||||
|
||||
<h2>"Converting between Types"</h2>
|
||||
// e. Note: if branches return different types,
|
||||
// you can convert between them with
|
||||
// `.into_any()` (for different HTML element types)
|
||||
// or `.into_view()` (for all view types)
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// <pre> returns HtmlElement<Pre>
|
||||
view! { <pre>"One"</pre> }.into_any()
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// <p> returns HtmlElement<P>
|
||||
// so we convert into a more generic type
|
||||
view! { <p>"Two"</p> }.into_any()
|
||||
}
|
||||
_ => view! { <textarea>{value()}</textarea> }.into_any()
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -10,13 +10,13 @@ Let’s start with a simple component to capture a number input.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
fn NumericInput() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(Ok(0));
|
||||
|
||||
// when input changes, try to parse a number from the input
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<label>
|
||||
"Type a number (or not!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
@@ -60,27 +60,27 @@ Let’s add an `<ErrorBoundary/>` to this example.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
fn NumericInput() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(Ok(0));
|
||||
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|cx, errors| view! { cx,
|
||||
fallback=|errors| view! {
|
||||
<div class="error">
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect_view(cx)
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
|
||||
.collect_view()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -113,3 +113,64 @@ an `<ErrorBoundary/>` will appear again.
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(Ok(0));
|
||||
|
||||
// when input changes, try to parse a number from the input
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! {
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
|
||||
// the fallback will be displayed. Otherwise, the children of the
|
||||
// <ErrorBoundary/> will be displayed.
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|errors| view! {
|
||||
<div class="error">
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors
|
||||
// as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
"You entered "
|
||||
// because `value` is `Result<i32, _>`,
|
||||
// it will render the `i32` if it is `Ok`,
|
||||
// and render nothing and trigger the error boundary
|
||||
// if it is `Err`. It's a signal, so this will dynamically
|
||||
// update when `value` changes
|
||||
<strong>{value}</strong>
|
||||
</p>
|
||||
</ErrorBoundary>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -29,17 +29,17 @@ it in the child. This lets you manipulate the state of the parent from the child
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonA setter=set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn ButtonA(setter: WriteSignal<bool>) -> impl IntoView {
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
@@ -62,9 +62,9 @@ Another approach would be to pass a callback to the child: say, `on_click`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
@@ -73,13 +73,13 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn ButtonB<F>(
|
||||
cx: Scope,
|
||||
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<button on:click=on_click>
|
||||
"Toggle"
|
||||
</button>
|
||||
@@ -105,9 +105,9 @@ in your `view` macro in `<App/>`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
// note the on:click instead of on_click
|
||||
// this is the same syntax as an HTML element event listener
|
||||
@@ -117,8 +117,8 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonC<F>(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn ButtonC<F>() -> impl IntoView {
|
||||
view! {
|
||||
<button>"Toggle"</button>
|
||||
}
|
||||
}
|
||||
@@ -141,17 +141,17 @@ tree:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Layout() -> impl IntoView {
|
||||
view! {
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
</header>
|
||||
@@ -162,8 +162,8 @@ pub fn Layout(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Content() -> impl IntoView {
|
||||
view! {
|
||||
<div class="content">
|
||||
<ButtonD/>
|
||||
</div>
|
||||
@@ -171,7 +171,7 @@ pub fn Content(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD<F>(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonD<F>() -> impl IntoView {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
@@ -182,17 +182,17 @@ pass your `WriteSignal` to its props. You could do what’s sometimes called
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Layout(d: WriteSignal<bool>) -> impl IntoView {
|
||||
view! {
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
</header>
|
||||
@@ -203,8 +203,8 @@ pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Content(d: WriteSignal<bool>) -> impl IntoView {
|
||||
view! {
|
||||
<div class="content">
|
||||
<ButtonD set_toggled/>
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@ pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD<F>(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
pub fn ButtonD<F>(d: WriteSignal<bool>) -> impl IntoView {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
@@ -237,13 +237,13 @@ unnecessary prop drilling.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
|
||||
// share `set_toggled` with all children of this component
|
||||
provide_context(cx, set_toggled);
|
||||
provide_context(set_toggled);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
@@ -252,14 +252,14 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// <Layout/> and <Content/> omitted
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonD() -> impl IntoView {
|
||||
// use_context searches up the context tree, hoping to
|
||||
// find a `WriteSignal<bool>`
|
||||
// in this case, I .expect() because I know I provided it
|
||||
let setter = use_context::<WriteSignal<bool>>(cx)
|
||||
let setter = use_context::<WriteSignal<bool>>()
|
||||
.expect("to have found the setter provided");
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
@@ -288,3 +288,150 @@ signals and effects, all the way down.
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::{ev::MouseEvent, *};
|
||||
|
||||
// This highlights four different ways that child components can communicate
|
||||
// with their parent:
|
||||
// 1) <ButtonA/>: passing a WriteSignal as one of the child component props,
|
||||
// for the child component to write into and the parent to read
|
||||
// 2) <ButtonB/>: passing a closure as one of the child component props, for
|
||||
// the child component to call
|
||||
// 3) <ButtonC/>: adding an `on:` event listener to a component
|
||||
// 4) <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct SmallcapsContext(WriteSignal<bool>);
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// just some signals to toggle three classes on our <p>
|
||||
let (red, set_red) = create_signal(false);
|
||||
let (right, set_right) = create_signal(false);
|
||||
let (italics, set_italics) = create_signal(false);
|
||||
let (smallcaps, set_smallcaps) = create_signal(false);
|
||||
|
||||
// the newtype pattern isn't *necessary* here but is a good practice
|
||||
// it avoids confusion with other possible future `WriteSignal<bool>` contexts
|
||||
// and makes it easier to refer to it in ButtonC
|
||||
provide_context(SmallcapsContext(set_smallcaps));
|
||||
|
||||
view! {
|
||||
|
||||
<main>
|
||||
<p
|
||||
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
|
||||
class:red=red
|
||||
class:right=right
|
||||
class:italics=italics
|
||||
class:smallcaps=smallcaps
|
||||
>
|
||||
"Lorem ipsum sit dolor amet."
|
||||
</p>
|
||||
|
||||
// Button A: pass the signal setter
|
||||
<ButtonA setter=set_red/>
|
||||
|
||||
// Button B: pass a closure
|
||||
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
|
||||
|
||||
// Button B: use a regular event listener
|
||||
// setting an event listener on a component like this applies it
|
||||
// to each of the top-level elements the component returns
|
||||
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
|
||||
|
||||
// Button D gets its setter from context rather than props
|
||||
<ButtonD/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
/// Button A receives a signal setter and updates the signal itself
|
||||
#[component]
|
||||
pub fn ButtonA(
|
||||
|
||||
/// Signal that will be toggled when the button is clicked.
|
||||
setter: WriteSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
"Toggle Red"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
/// Button B receives a closure
|
||||
#[component]
|
||||
pub fn ButtonB<F>(
|
||||
|
||||
/// Callback that will be invoked when the button is clicked.
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
{
|
||||
view! {
|
||||
|
||||
<button
|
||||
on:click=on_click
|
||||
>
|
||||
"Toggle Right"
|
||||
</button>
|
||||
}
|
||||
|
||||
// just a note: in an ordinary function ButtonB could take on_click: impl Fn(MouseEvent) + 'static
|
||||
// and save you from typing out the generic
|
||||
// the component macro actually expands to define a
|
||||
//
|
||||
// struct ButtonBProps<F> where F: Fn(MouseEvent) + 'static {
|
||||
// on_click: F
|
||||
// }
|
||||
//
|
||||
// this is what allows us to have named props in our component invocation,
|
||||
// instead of an ordered list of function arguments
|
||||
// if Rust ever had named function arguments we could drop this requirement
|
||||
}
|
||||
|
||||
/// Button C is a dummy: it renders a button but doesn't handle
|
||||
/// its click. Instead, the parent component adds an event listener.
|
||||
#[component]
|
||||
pub fn ButtonC() -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<button>
|
||||
"Toggle Italics"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
/// Button D is very similar to Button A, but instead of passing the setter as a prop
|
||||
/// we get it from the context
|
||||
#[component]
|
||||
pub fn ButtonD() -> impl IntoView {
|
||||
let setter = use_context::<SmallcapsContext>().unwrap().0;
|
||||
|
||||
view! {
|
||||
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
"Toggle Small Caps"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -5,7 +5,7 @@ children into an HTML element. For example, imagine I have a `<FancyForm/>` comp
|
||||
that enhances an HTML `<form>`. I need some way to pass all its inputs.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<Form>
|
||||
<fieldset>
|
||||
<label>
|
||||
@@ -28,12 +28,12 @@ other components:
|
||||
In fact, you’ve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<Show
|
||||
// `when` is a normal prop
|
||||
when=move || value() > 5
|
||||
// `fallback` is a "render prop": a function that returns a view
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
fallback=|| view! { <Small/> }
|
||||
>
|
||||
// `<Big/>` (and anything else here)
|
||||
// will be given to the `children` prop
|
||||
@@ -47,7 +47,7 @@ Let’s define a component that takes some children and a render prop.
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
cx: Scope,
|
||||
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
@@ -58,19 +58,19 @@ where
|
||||
F: Fn() -> IV,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<h2>"Render Prop"</h2>
|
||||
{render_prop()}
|
||||
|
||||
<h2>"Children"</h2>
|
||||
{children(cx)}
|
||||
{children()}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`render_prop` and `children` are both functions, so we can call them to generate
|
||||
the appropriate views. `children`, in particular, is an alias for
|
||||
`Box<dyn FnOnce(Scope) -> Fragment>`. (Aren't you glad we named it `Children` instead?)
|
||||
`Box<dyn FnOnce() -> Fragment>`. (Aren't you glad we named it `Children` instead?)
|
||||
|
||||
> If you need a `Fn` or `FnMut` here because you need to call `children` more than once,
|
||||
> we also provide `ChildrenFn` and `ChildrenMut` aliases.
|
||||
@@ -78,8 +78,8 @@ the appropriate views. `children`, in particular, is an alias for
|
||||
We can use the component like this:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
|
||||
view! {
|
||||
<TakesChildren render_prop=|| view! { <p>"Hi, there!"</p> }>
|
||||
// these get passed to `children`
|
||||
"Some text"
|
||||
<span>"A span"</span>
|
||||
@@ -97,15 +97,15 @@ a component that takes its children and turns them into an unordered list.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
pub fn WrapsChildren(Children) -> impl IntoView {
|
||||
// Fragment has `nodes` field that contains a Vec<View>
|
||||
let children = children(cx)
|
||||
let children = children()
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { cx, <li>{child}</li> })
|
||||
.collect_view(cx);
|
||||
.map(|child| view! { <li>{child}</li> })
|
||||
.collect_view();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<ul>{children}</ul>
|
||||
}
|
||||
}
|
||||
@@ -114,15 +114,119 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
Calling it like this will create a list:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<WrappedChildren>
|
||||
view! {
|
||||
<WrapsChildren>
|
||||
"A"
|
||||
"B"
|
||||
"C"
|
||||
</WrappedChildren>
|
||||
</WrapsChildren>
|
||||
}
|
||||
```
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
// Often, you want to pass some kind of child view to another
|
||||
// component. There are two basic patterns for doing this:
|
||||
// - "render props": creating a component prop that takes a function
|
||||
// that creates a view
|
||||
// - the `children` prop: a special property that contains content
|
||||
// passed as the children of a component in your view, not as a
|
||||
// property
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (items, set_items) = create_signal(vec![0, 1, 2]);
|
||||
let render_prop = move || {
|
||||
// items.with(...) reacts to the value without cloning
|
||||
// by applying a function. Here, we pass the `len` method
|
||||
// on a `Vec<_>` directly
|
||||
let len = move || items.with(Vec::len);
|
||||
view! {
|
||||
<p>"Length: " {len}</p>
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
// This component just displays the two kinds of children,
|
||||
// embedding them in some other markup
|
||||
<TakesChildren
|
||||
// for component props, you can shorthand
|
||||
// `render_prop=render_prop` => `render_prop`
|
||||
// (this doesn't work for HTML element attributes)
|
||||
render_prop
|
||||
>
|
||||
// these look just like the children of an HTML element
|
||||
<p>"Here's a child."</p>
|
||||
<p>"Here's another child."</p>
|
||||
</TakesChildren>
|
||||
<hr/>
|
||||
// This component actually iterates over and wraps the children
|
||||
<WrapsChildren>
|
||||
<p>"Here's a child."</p>
|
||||
<p>"Here's another child."</p>
|
||||
</WrapsChildren>
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays a `render_prop` and some children within markup.
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
/// `children` takes the `Children` type
|
||||
/// this is an alias for `Box<dyn FnOnce() -> Fragment>`
|
||||
/// ... aren't you glad we named it `Children` instead?
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> IV,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! {
|
||||
<h1><code>"<TakesChildren/>"</code></h1>
|
||||
<h2>"Render Prop"</h2>
|
||||
{render_prop()}
|
||||
<hr/>
|
||||
<h2>"Children"</h2>
|
||||
{children()}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps each child in an `<li>` and embeds them in a `<ul>`.
|
||||
#[component]
|
||||
pub fn WrapsChildren(Children) -> impl IntoView {
|
||||
// children() returns a `Fragment`, which has a
|
||||
// `nodes` field that contains a Vec<View>
|
||||
// this means we can iterate over the children
|
||||
// to create something new!
|
||||
let children = children()
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { <li>{child}</li> })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
view! {
|
||||
<h1><code>"<WrapsChildren/>"</code></h1>
|
||||
// wrap our wrapped children in a UL
|
||||
<ul>{children}</ul>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
</preview>
|
||||
|
||||
@@ -5,26 +5,107 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
|
||||
CARGO_MAKE_WORKSPACE_EMULATION = true
|
||||
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"counter",
|
||||
"counter_isomorphic",
|
||||
"counters",
|
||||
"counters_stable",
|
||||
"counter_without_macros",
|
||||
"error_boundary",
|
||||
"errors_axum",
|
||||
"fetch",
|
||||
"hackernews",
|
||||
"hackernews_axum",
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"session_auth_axum",
|
||||
"ssr_modes",
|
||||
"ssr_modes_axum",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
"animated_show",
|
||||
"counter",
|
||||
"counter_isomorphic",
|
||||
"counters",
|
||||
"counters_stable",
|
||||
"counter_url_query",
|
||||
"counter_without_macros",
|
||||
"error_boundary",
|
||||
"errors_axum",
|
||||
"fetch",
|
||||
"hackernews",
|
||||
"hackernews_axum",
|
||||
"js-framework-benchmark",
|
||||
"leptos-tailwind-axum",
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"session_auth_axum",
|
||||
"slots",
|
||||
"ssr_modes",
|
||||
"ssr_modes_axum",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
]
|
||||
|
||||
[tasks.gen-members]
|
||||
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"
|
||||
'''
|
||||
|
||||
[tasks.test-runner-report]
|
||||
workspace = false
|
||||
description = "report ci test runners for each example - OPTION: [all]"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
ITALIC="\e[3m"
|
||||
YELLOW="\e[0;33m"
|
||||
RESET="\e[0m"
|
||||
|
||||
echo
|
||||
echo "${YELLOW}Test Runner Report${RESET}"
|
||||
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
|
||||
echo
|
||||
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
|
||||
sed 's%./%%' |
|
||||
sed 's%/Makefile.toml%%' |
|
||||
grep -v Makefile.toml |
|
||||
sort -u)
|
||||
|
||||
start_path=$(pwd)
|
||||
|
||||
for path in $makefile_paths; do
|
||||
cd $path
|
||||
|
||||
test_runner=
|
||||
|
||||
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
|
||||
if [ $test_count -gt 0 ]; then
|
||||
test_runner="-C"
|
||||
fi
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"wasm-test.toml"*)
|
||||
test_runner=$test_runner"-W"
|
||||
;;
|
||||
*"playwright-test.toml"*)
|
||||
test_runner=$test_runner"-P"
|
||||
;;
|
||||
*"cargo-leptos-test.toml"*)
|
||||
test_runner=$test_runner"-L"
|
||||
;;
|
||||
esac
|
||||
done <"./Makefile.toml"
|
||||
|
||||
if [ ! -z "$1" ]; then
|
||||
# Show all examples
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
elif [ ! -z $test_runner ]; then
|
||||
# Filter out examples that do not run tests in `ci`
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
fi
|
||||
|
||||
cd ${start_path}
|
||||
done
|
||||
echo
|
||||
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
|
||||
echo
|
||||
'''
|
||||
|
||||
14
examples/animated_show/Cargo.toml
Normal file
14
examples/animated_show/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "animated-show"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
1
examples/animated_show/Makefile.toml
Normal file
1
examples/animated_show/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
9
examples/animated_show/README.md
Normal file
9
examples/animated_show/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# `<AnimatedShow>` combined with CSS animations
|
||||
|
||||
This is a very simple example of the `<AnimatedShow>` component.
|
||||
|
||||
This component is an extension for the `<Show>` component and it will not take in a fallback, but it will unmount the
|
||||
component from the DOM after a given duration. This makes it possible to have really easy unmount animations with just
|
||||
CSS.
|
||||
|
||||
Just execute `trunk serve` to start the demo.
|
||||
42
examples/animated_show/index.html
Normal file
42
examples/animated_show/index.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
<style>
|
||||
.hover-me {
|
||||
width: 100px;
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border: 1px solid grey;
|
||||
}
|
||||
.here-i-am {
|
||||
width: 100px;
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
background: black;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
.fade-in-1000 {
|
||||
animation: 1000ms fade-in forwards;
|
||||
}
|
||||
.fade-out-1000 {
|
||||
animation: 1000ms fade-out forwards;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
BIN
examples/animated_show/public/favicon.ico
Normal file
BIN
examples/animated_show/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
34
examples/animated_show/src/lib.rs
Normal file
34
examples/animated_show/src/lib.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use core::time::Duration;
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let show = create_rw_signal(false);
|
||||
|
||||
// the CSS classes in this example are just written directly inside the `index.html`
|
||||
view! {
|
||||
<div
|
||||
class="hover-me"
|
||||
on:mouseenter=move |_| show.set(true)
|
||||
on:mouseleave=move |_| show.set(false)
|
||||
>
|
||||
"Hover Me"
|
||||
</div>
|
||||
|
||||
<AnimatedShow
|
||||
when=show
|
||||
// optional CSS class which will be applied if `when == true`
|
||||
show_class="fade-in-1000"
|
||||
// optional CSS class which will be applied if `when == false` and before the
|
||||
// `hide_delay` starts -> makes CSS unmount animations really easy
|
||||
hide_class="fade-out-1000"
|
||||
// the given unmount delay which should match your unmount animation duration
|
||||
hide_delay=Duration::from_millis(1000)
|
||||
>
|
||||
// provide any `Children` inside here
|
||||
<div class="here-i-am">
|
||||
"Here I Am!"
|
||||
</div>
|
||||
</AnimatedShow>
|
||||
}
|
||||
}
|
||||
8
examples/animated_show/src/main.rs
Normal file
8
examples/animated_show/src/main.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use animated_show::App;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
[tasks.test-e2e]
|
||||
dependencies = ["setup-node", "cargo-leptos-e2e"]
|
||||
[tasks.integration-test]
|
||||
dependencies = ["cargo-leptos-e2e"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
28
examples/cargo-make/clean.toml
Normal file
28
examples/cargo-make/clean.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[tasks.clean]
|
||||
dependencies = [
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-node_modules]
|
||||
script = '''
|
||||
project_dir=${PWD##*/}
|
||||
if [ "$project_dir" != "todomvc" ]; then
|
||||
find . -type d -name node_modules | xargs rm -rf
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.clean-playwright]
|
||||
script = '''
|
||||
find . -name playwright-report -name playwright -name test-results | xargs rm -rf
|
||||
'''
|
||||
@@ -1,97 +0,0 @@
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
|
||||
[tasks.check-style]
|
||||
description = "Check for style violations"
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.check-format]
|
||||
args = ["fmt", "--", "--check", "--config-path", "../../"]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
description = "Runs the cargo clean command."
|
||||
category = "Cleanup"
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
description = "Runs the trunk clean command."
|
||||
category = "Cleanup"
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-node_modules]
|
||||
description = "Delete all node_modules directories"
|
||||
category = "Cleanup"
|
||||
script = '''
|
||||
find . -type d -name node_modules | xargs rm -rf
|
||||
'''
|
||||
|
||||
[tasks.clean-playwright]
|
||||
description = "Delete playwright directories"
|
||||
category = "Cleanup"
|
||||
script = '''
|
||||
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
|
||||
do
|
||||
rm -rf $pw_dir/playwright-report
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.clean-all]
|
||||
description = "Delete all temporary directories"
|
||||
category = "Cleanup"
|
||||
dependencies = ["clean-cargo"]
|
||||
|
||||
[tasks.test-wasm]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
description = "Runs end to end tests with cargo leptos"
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
[tasks.setup-node]
|
||||
description = "Install node dependencies and playwright browsers"
|
||||
env = { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1" }
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
NODE_CMD=pnpm
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
NODE_CMD=npm
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install node dependencies
|
||||
for node_path in $(find . -name package.json -not -path '*/node_modules/*')
|
||||
do
|
||||
node_dir=$(dirname $node_path)
|
||||
echo Install node dependencies for $node_dir
|
||||
cd $node_dir
|
||||
${NODE_CMD} install
|
||||
cd ${project_dir}
|
||||
done
|
||||
|
||||
# Install playwright browsers
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
echo Install playwright browsers for $pw_dir
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright install
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
11
examples/cargo-make/compile.toml
Normal file
11
examples/cargo-make/compile.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[tasks.build]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
9
examples/cargo-make/lint.toml
Normal file
9
examples/cargo-make/lint.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
|
||||
[tasks.check-style]
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
@@ -1,28 +1,41 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/compile.toml" },
|
||||
{ path = "../cargo-make/clean.toml" },
|
||||
{ path = "../cargo-make/lint.toml" },
|
||||
{ path = "../cargo-make/node.toml" },
|
||||
]
|
||||
|
||||
# CI Stages
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["prepare", "lint", "build", "test-flow", "integration-test"]
|
||||
|
||||
[tasks.prepare]
|
||||
dependencies = ["setup-node"]
|
||||
|
||||
[tasks.lint]
|
||||
dependencies = ["check-style"]
|
||||
|
||||
[tasks.integration-test]
|
||||
|
||||
# Support Local Runs
|
||||
|
||||
[tasks.ci-clean]
|
||||
dependencies = ["ci", "clean"]
|
||||
|
||||
[tasks.check-clean]
|
||||
dependencies = ["check", "clean"]
|
||||
|
||||
[tasks.build-clean]
|
||||
dependencies = ["build", "clean"]
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.verify-flow]
|
||||
description = "Provides pre and post hooks for verify"
|
||||
dependencies = ["pre-verify", "verify", "post-verify"]
|
||||
alias = "ci"
|
||||
|
||||
[tasks.verify]
|
||||
description = "Run all quality checks and tests"
|
||||
dependencies = ["check-style", "test-unit-and-e2e"]
|
||||
[tasks.t]
|
||||
dependencies = ["test-flow"]
|
||||
|
||||
[tasks.test-unit-and-e2e]
|
||||
description = "Run all unit and e2e tests"
|
||||
dependencies = ["test-flow", "test-e2e-flow"]
|
||||
|
||||
[tasks.pre-verify]
|
||||
|
||||
[tasks.post-verify]
|
||||
dependencies = ["clean-all"]
|
||||
|
||||
[tasks.test-e2e-flow]
|
||||
description = "Provides pre and post hooks for test-e2e"
|
||||
dependencies = ["pre-test-e2e", "test-e2e", "post-test-e2e"]
|
||||
|
||||
[tasks.pre-test-e2e]
|
||||
|
||||
[tasks.test-e2e]
|
||||
|
||||
[tasks.post-test-e2e]
|
||||
[tasks.it]
|
||||
alias = "integration-test"
|
||||
|
||||
43
examples/cargo-make/node.toml
Normal file
43
examples/cargo-make/node.toml
Normal file
@@ -0,0 +1,43 @@
|
||||
[tasks.setup-node]
|
||||
description = "Install node dependencies and playwright browsers"
|
||||
env = { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1" }
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
NODE_CMD=pnpm
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
NODE_CMD=npm
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install node dependencies
|
||||
for node_path in $(find . -name package.json -not -path '*/node_modules/*')
|
||||
do
|
||||
node_dir=$(dirname $node_path)
|
||||
echo Install node dependencies for $node_dir
|
||||
cd $node_dir
|
||||
${NODE_CMD} install
|
||||
cd ${project_dir}
|
||||
done
|
||||
|
||||
# Install playwright browsers
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
echo Install playwright browsers for $pw_dir
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright install
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
4
examples/cargo-make/playwright-test.toml
Normal file
4
examples/cargo-make/playwright-test.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
extend = [{ path = "../cargo-make/playwright.toml" }]
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = ["test-playwright-autostart"]
|
||||
101
examples/cargo-make/playwright.toml
Normal file
101
examples/cargo-make/playwright.toml
Normal file
@@ -0,0 +1,101 @@
|
||||
[tasks.test-playwright-autostart]
|
||||
command = "npm"
|
||||
args = ["run", "e2e:auto-start"]
|
||||
|
||||
[tasks.test-playwright]
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run playwright command
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright test
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-ui]
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run playwright command
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright test --ui
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-report]
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run playwright command
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright show-report
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.pw]
|
||||
dependencies = ["test-playwright"]
|
||||
|
||||
[tasks.pw-ui]
|
||||
dependencies = ["test-playwright-ui"]
|
||||
|
||||
[tasks.pw-report]
|
||||
dependencies = ["test-playwright-report"]
|
||||
18
examples/cargo-make/trunk_server.toml
Normal file
18
examples/cargo-make/trunk_server.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[tasks.build]
|
||||
command = "trunk"
|
||||
args = ["build"]
|
||||
|
||||
[tasks.start-trunk]
|
||||
command = "trunk"
|
||||
args = ["serve", "${@}"]
|
||||
|
||||
[tasks.stop-trunk]
|
||||
script = '''
|
||||
pkill -f "cargo-make"
|
||||
pkill -f "trunk"
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.dev]
|
||||
dependencies = ["start-trunk"]
|
||||
@@ -1,5 +1,11 @@
|
||||
[tasks.test]
|
||||
env = { RUN_CARGO_TEST = false }
|
||||
condition = { env_true = ["RUN_CARGO_TEST"] }
|
||||
|
||||
[tasks.post-test]
|
||||
dependencies = ["test-wasm"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-trunk"]
|
||||
[tasks.test-wasm]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -2,13 +2,3 @@ extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -5,15 +5,14 @@ use leptos::*;
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleCounter(
|
||||
cx: Scope,
|
||||
/// The starting value for the counter
|
||||
initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
step: i32,
|
||||
) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
|
||||
|
||||
@@ -4,8 +4,8 @@ use leptos::*;
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
mount_to_body(|| {
|
||||
view! {
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
|
||||
@@ -15,7 +15,7 @@ fn clear() {
|
||||
// note that we start at the initial value of 10
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
|| view! { <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
@@ -32,16 +32,17 @@ fn clear() {
|
||||
|
||||
// now let's test the <div> against the expected value
|
||||
// we can do this by testing its `outerHTML`
|
||||
let runtime = create_runtime();
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system, just to render the
|
||||
// test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
{
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, _set_value) = create_signal(cx, 0);
|
||||
let (value, _set_value) = create_signal(0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
@@ -52,7 +53,7 @@ fn clear() {
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
// There's actually an easier way to do this...
|
||||
@@ -61,10 +62,12 @@ fn clear() {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
|| view! { <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
@@ -75,7 +78,7 @@ fn inc() {
|
||||
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> },
|
||||
|| view! { <SimpleCounter initial_value=0 step=1/> },
|
||||
);
|
||||
|
||||
// You can do testing with vanilla DOM operations
|
||||
@@ -118,12 +121,14 @@ fn inc() {
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
|
||||
let runtime = create_runtime();
|
||||
|
||||
// Or you can test against a sample view!
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
run_scope(create_runtime(), |cx| {
|
||||
let (value, _) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
{
|
||||
let (value, _) = create_signal(0);
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
@@ -132,17 +137,17 @@ fn inc() {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
.outer_html())
|
||||
.outer_html()
|
||||
);
|
||||
|
||||
inc.click();
|
||||
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
run_scope(create_runtime(), |cx| {
|
||||
{
|
||||
// because we've clicked, it's as if the signal is starting at 1
|
||||
let (value, _) = create_signal(cx, 1);
|
||||
view! { cx,
|
||||
let (value, _) = create_signal(1);
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
@@ -151,6 +156,8 @@ fn inc() {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
.outer_html())
|
||||
.outer_html()
|
||||
);
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
@@ -19,19 +19,17 @@ console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "=0.2.86"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["nightly"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
@@ -41,10 +39,10 @@ ssr = [
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
stable = ["leptos/stable", "leptos_router/stable"]
|
||||
nightly = ["leptos/nightly", "leptos_router/nightly"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "stable"]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "nightly"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -40,9 +40,9 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
Ok(0)
|
||||
}
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
view! { cx,
|
||||
pub fn Counters() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
view! {
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
@@ -69,20 +69,20 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
<Routes>
|
||||
<Route
|
||||
path=""
|
||||
view=|cx| {
|
||||
view! { cx, <Counter/> }
|
||||
view=|| {
|
||||
view! { <Counter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="form"
|
||||
view=|cx| {
|
||||
view! { cx, <FormCounter/> }
|
||||
view=|| {
|
||||
view! { <FormCounter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="multi"
|
||||
view=|cx| {
|
||||
view! { cx, <MultiuserCounter/> }
|
||||
view=|| {
|
||||
view! { <MultiuserCounter/> }
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
@@ -96,12 +96,11 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
// it's invalidated by one of the user's own actions
|
||||
// This is the typical pattern for a CRUD app
|
||||
#[component]
|
||||
pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let dec = create_action(|_| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || {
|
||||
(
|
||||
dec.version().get(),
|
||||
@@ -112,20 +111,16 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
|_| get_server_count(),
|
||||
);
|
||||
|
||||
let value = move || {
|
||||
counter
|
||||
.read(cx)
|
||||
.map(|count| count.unwrap_or(0))
|
||||
.unwrap_or(0)
|
||||
};
|
||||
let value =
|
||||
move || counter.read().map(|count| count.unwrap_or(0)).unwrap_or(0);
|
||||
let error_msg = move || {
|
||||
counter.read(cx).and_then(|res| match res {
|
||||
counter.read().and_then(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<h2>"Simple Counter"</h2>
|
||||
<p>
|
||||
@@ -140,7 +135,7 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
{move || {
|
||||
error_msg()
|
||||
.map(|msg| {
|
||||
view! { cx, <p>"Error: " {msg.to_string()}</p> }
|
||||
view! { <p>"Error: " {msg.to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
@@ -151,12 +146,11 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
// It uses the same invalidation pattern as the plain counter,
|
||||
// but uses HTML forms to submit the actions
|
||||
#[component]
|
||||
pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
let adjust = create_server_action::<AdjustServerCount>(cx);
|
||||
let clear = create_server_action::<ClearServerCount>(cx);
|
||||
pub fn FormCounter() -> impl IntoView {
|
||||
let adjust = create_server_action::<AdjustServerCount>();
|
||||
let clear = create_server_action::<ClearServerCount>();
|
||||
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || (adjust.version().get(), clear.version().get()),
|
||||
|_| {
|
||||
log::debug!("FormCounter running fetcher");
|
||||
@@ -165,10 +159,10 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let value = move || {
|
||||
log::debug!("FormCounter looking for value");
|
||||
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
|
||||
counter.read().and_then(|n| n.ok()).unwrap_or(0)
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<h2>"Form Counter"</h2>
|
||||
<p>
|
||||
@@ -203,12 +197,11 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
// Whenever another user updates the value, it will update here
|
||||
// This is the primitive pattern for live chat, collaborative editing, etc.
|
||||
#[component]
|
||||
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
pub fn MultiuserCounter() -> impl IntoView {
|
||||
let dec =
|
||||
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc =
|
||||
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
create_action(|_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
@@ -218,7 +211,6 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
source
|
||||
.subscribe("message")
|
||||
.unwrap()
|
||||
@@ -232,14 +224,14 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
}),
|
||||
);
|
||||
|
||||
on_cleanup(cx, move || source.close());
|
||||
on_cleanup(move || source.close());
|
||||
s
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
|
||||
let (multiplayer_value, _) = create_signal(None::<i32>);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<h2>"Multi-User Counter"</h2>
|
||||
<p>
|
||||
|
||||
@@ -13,8 +13,8 @@ cfg_if! {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <Counters/> }
|
||||
mount_to_body(|| {
|
||||
view! { <Counters/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ cfg_if! {
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
|
||||
let routes = generate_route_list(|| view! { <Counters/> });
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
@@ -52,7 +52,7 @@ cfg_if! {
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <Counters/> })
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
|
||||
20
examples/counter_url_query/Cargo.toml
Normal file
20
examples/counter_url_query/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "counter_url_query"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
web-sys = "0.3"
|
||||
1
examples/counter_url_query/Makefile.toml
Normal file
1
examples/counter_url_query/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
7
examples/counter_url_query/README.md
Normal file
7
examples/counter_url_query/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Leptos Query Counter Example
|
||||
|
||||
This example creates a simple counter whose state is persisted and synced in the url with query params.
|
||||
|
||||
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
8
examples/counter_url_query/index.html
Normal file
8
examples/counter_url_query/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
BIN
examples/counter_url_query/public/favicon.ico
Normal file
BIN
examples/counter_url_query/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
39
examples/counter_url_query/src/lib.rs
Normal file
39
examples/counter_url_query/src/lib.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
/// A simple counter component.
|
||||
///
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleQueryCounter() -> impl IntoView {
|
||||
let (count, set_count) = create_query_signal::<i32>("count");
|
||||
let clear = move |_| set_count(None);
|
||||
let decrement = move |_| set_count(Some(count().unwrap_or(0) - 1));
|
||||
let increment = move |_| set_count(Some(count().unwrap_or(0) + 1));
|
||||
|
||||
let (msg, set_msg) = create_query_signal::<String>("message");
|
||||
let update_msg = move |ev| {
|
||||
let new_msg = event_target_value(&ev);
|
||||
if new_msg.is_empty() {
|
||||
set_msg(None);
|
||||
} else {
|
||||
set_msg(Some(new_msg));
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
<span>"Value: " {move || count().unwrap_or(0)} "!"</span>
|
||||
<button on:click=increment>"+1"</button>
|
||||
|
||||
<br />
|
||||
|
||||
<input
|
||||
prop:value=move || msg().unwrap_or_default()
|
||||
on:input=update_msg
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
17
examples/counter_url_query/src/main.rs
Normal file
17
examples/counter_url_query/src/main.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use counter_url_query::SimpleQueryCounter;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="" view=SimpleQueryCounter />
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -4,11 +4,13 @@ extend = [
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["+stable", "build-all-features"]
|
||||
args = ["build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
toolchain = "stable"
|
||||
command = "cargo"
|
||||
args = ["+stable", "check-all-features"]
|
||||
args = ["check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -2,17 +2,17 @@ use leptos::{ev, html::*, *};
|
||||
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, Count::new(initial_value, step));
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(Count::new(initial_value, step));
|
||||
|
||||
// elements are created by calling a function with a Scope argument
|
||||
// the function name is the same as the HTML tag name
|
||||
div(cx)
|
||||
div()
|
||||
// children can be added with .child()
|
||||
// this takes any type that implements IntoView as its argument
|
||||
// for example, a string or an HtmlElement<_>
|
||||
.child(
|
||||
button(cx)
|
||||
button()
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
@@ -20,14 +20,14 @@ pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
|
||||
.child("Clear"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.child("-1"),
|
||||
)
|
||||
.child(
|
||||
span(cx)
|
||||
span()
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
@@ -35,7 +35,7 @@ pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
|
||||
.child("!"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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();
|
||||
mount_to_body(|cx| counter(cx, 0, 1))
|
||||
mount_to_body(|| counter(0, 1))
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ fn should_clear_counter() {
|
||||
|
||||
fn open_counter() {
|
||||
remove_existing_counter();
|
||||
mount_to_body(move |cx| counter(cx, 0, 1));
|
||||
mount_to_body(move || counter(0, 1));
|
||||
}
|
||||
|
||||
fn remove_existing_counter() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user