mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
392 Commits
fix-svgs
...
docs-set-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123b1ef24d | ||
|
|
2d418dae93 | ||
|
|
91e0fcdc1b | ||
|
|
a9ed8461d1 | ||
|
|
5a71ca797a | ||
|
|
70eb07d7d6 | ||
|
|
71ee69af01 | ||
|
|
dd41c0586c | ||
|
|
aaf63dbf5c | ||
|
|
87f6802967 | ||
|
|
2cbf3581c5 | ||
|
|
5a67e208fd | ||
|
|
3391a4a035 | ||
|
|
076aa363a4 | ||
|
|
2cb68c0bd4 | ||
|
|
6eb24b5017 | ||
|
|
b2faa6b86c | ||
|
|
43990b5b67 | ||
|
|
9453164dd2 | ||
|
|
00fcd1c65e | ||
|
|
85ad7b0f38 | ||
|
|
f0a9940364 | ||
|
|
b472aaf6a0 | ||
|
|
059c1bf61c | ||
|
|
add13fd6a4 | ||
|
|
904c2e8a67 | ||
|
|
a5c3be586a | ||
|
|
9f5139d929 | ||
|
|
bae305340e | ||
|
|
40c1556f29 | ||
|
|
0db4f5821f | ||
|
|
12ebc95800 | ||
|
|
d7b919032e | ||
|
|
be8bf8b0d6 | ||
|
|
f84f1422f4 | ||
|
|
b01976e3bb | ||
|
|
50b48fb272 | ||
|
|
1617e31d69 | ||
|
|
51cd082d4c | ||
|
|
72414b7945 | ||
|
|
1afa14ccbd | ||
|
|
477c29cdf1 | ||
|
|
49a424314a | ||
|
|
598523cd9d | ||
|
|
1fdb6f1cdf | ||
|
|
9997487a9c | ||
|
|
b5e94e4054 | ||
|
|
a5f6e0bac4 | ||
|
|
2c9de79576 | ||
|
|
63dd00a050 | ||
|
|
99823a3d4f | ||
|
|
c0bdd464f6 | ||
|
|
7e7377f4f7 | ||
|
|
15448765dd | ||
|
|
f0f1c3144b | ||
|
|
630da4212d | ||
|
|
38bc24bb9e | ||
|
|
012285337b | ||
|
|
3ba4f62cef | ||
|
|
b4996769c1 | ||
|
|
9a6b1f53da | ||
|
|
ef45828ca7 | ||
|
|
ea153e4f26 | ||
|
|
59b8626277 | ||
|
|
d8e03773f0 | ||
|
|
5ab799bbf8 | ||
|
|
6c763a83cb | ||
|
|
9cf337309d | ||
|
|
1af35cdd3b | ||
|
|
fcb98474b8 | ||
|
|
54f7e9366a | ||
|
|
ddf9df2b5e | ||
|
|
7fe9f82d89 | ||
|
|
661adc4027 | ||
|
|
1011c464dc | ||
|
|
4b498a3b42 | ||
|
|
3c90b47e77 | ||
|
|
671b1e4a8f | ||
|
|
52021be806 | ||
|
|
75a7bd610a | ||
|
|
de553cf4fe | ||
|
|
0a65f43789 | ||
|
|
0f277c55ec | ||
|
|
04b01a6ced | ||
|
|
6c3381ce52 | ||
|
|
fa2e2248d3 | ||
|
|
362150a715 | ||
|
|
27b5991ee3 | ||
|
|
0a7dbb0ca4 | ||
|
|
234861a156 | ||
|
|
78d6d312f8 | ||
|
|
a1144a5b6b | ||
|
|
9723cc466e | ||
|
|
79c12c0129 | ||
|
|
a08d6bae10 | ||
|
|
39261a276c | ||
|
|
c471986024 | ||
|
|
d2e3a156e8 | ||
|
|
9badfa997b | ||
|
|
72f8bf4e20 | ||
|
|
c74b15b120 | ||
|
|
9a4f3ab08c | ||
|
|
a0935c169e | ||
|
|
0e2181fb90 | ||
|
|
732ec14302 | ||
|
|
ec95060b6e | ||
|
|
689afec26e | ||
|
|
bbf23ea40a | ||
|
|
34e0a8e47d | ||
|
|
81f330e888 | ||
|
|
e5d657dd55 | ||
|
|
f919127a7e | ||
|
|
2001bd808f | ||
|
|
f51857cedc | ||
|
|
f3b8d27c4f | ||
|
|
d3a577c365 | ||
|
|
b80f9e3871 | ||
|
|
328d42656d | ||
|
|
d3d2cbed7e | ||
|
|
d6f7aedec1 | ||
|
|
7a5a776cb9 | ||
|
|
06f782aa13 | ||
|
|
6b825fec37 | ||
|
|
b452d8af40 | ||
|
|
e96f1d2129 | ||
|
|
72d6af9c84 | ||
|
|
8198cd0b68 | ||
|
|
fe68b47ba2 | ||
|
|
384d39543c | ||
|
|
225e62d12f | ||
|
|
3905a2aa60 | ||
|
|
ff6ce2dac0 | ||
|
|
16675cbff2 | ||
|
|
9524c6e289 | ||
|
|
bc316c648c | ||
|
|
6753ba21c4 | ||
|
|
efbe32e081 | ||
|
|
55fd6d44f9 | ||
|
|
90972f2d94 | ||
|
|
7382c7e51c | ||
|
|
8a6d129575 | ||
|
|
e20c77710d | ||
|
|
93da88eac0 | ||
|
|
5072539917 | ||
|
|
78c59df1d1 | ||
|
|
75e40eafb2 | ||
|
|
274a1ac5f0 | ||
|
|
17040a4af4 | ||
|
|
b09a5f905e | ||
|
|
683511f311 | ||
|
|
151c58733b | ||
|
|
012ff56cd6 | ||
|
|
493c805993 | ||
|
|
764192af36 | ||
|
|
f969fd7eff | ||
|
|
2c7ee0d415 | ||
|
|
5430c78e18 | ||
|
|
6b052557d1 | ||
|
|
70f3edb0f5 | ||
|
|
4e1f963750 | ||
|
|
3c3d3b33f1 | ||
|
|
be7b9eea25 | ||
|
|
016ad6b7a6 | ||
|
|
5dab35447a | ||
|
|
63be819533 | ||
|
|
af8afb1204 | ||
|
|
2170be8e01 | ||
|
|
1187a506dd | ||
|
|
ff5ceddbe2 | ||
|
|
41a5e09caa | ||
|
|
60b96c9118 | ||
|
|
7ccb2d9f44 | ||
|
|
2c2090a194 | ||
|
|
de9b2998ac | ||
|
|
29b81a3d50 | ||
|
|
5bc0d89ce7 | ||
|
|
342b10c232 | ||
|
|
ba9d3c1602 | ||
|
|
d3b3ce6980 | ||
|
|
4b79a91287 | ||
|
|
de06c9b2ca | ||
|
|
84c7d00ea9 | ||
|
|
8f5ae0054d | ||
|
|
374f0c4e27 | ||
|
|
a6170f4da9 | ||
|
|
578dd5ef35 | ||
|
|
934a131deb | ||
|
|
5bc1c36e67 | ||
|
|
b1b9853f92 | ||
|
|
5d6a083d1d | ||
|
|
9478245986 | ||
|
|
4c1c12734a | ||
|
|
5d3a360456 | ||
|
|
b51da35a9a | ||
|
|
164dcd1b97 | ||
|
|
c0964c2b01 | ||
|
|
af5b226e53 | ||
|
|
4e7a0db950 | ||
|
|
cee6ed9a9f | ||
|
|
fa1013f7c3 | ||
|
|
3a1db3a191 | ||
|
|
8b57ba7aa8 | ||
|
|
ea638e37f6 | ||
|
|
4342d45a2f | ||
|
|
fe4d2382b8 | ||
|
|
2a13609eff | ||
|
|
c2ff1cabf1 | ||
|
|
54370e3153 | ||
|
|
e72ed26809 | ||
|
|
64e056ffa9 | ||
|
|
db9b7db53d | ||
|
|
a9e6590b5e | ||
|
|
b67121b755 | ||
|
|
7bce4de682 | ||
|
|
8bdb427133 | ||
|
|
4c23f3c478 | ||
|
|
9502de561b | ||
|
|
210c11a733 | ||
|
|
6917027204 | ||
|
|
e78ce7e6b9 | ||
|
|
a3327f8841 | ||
|
|
f727dd773b | ||
|
|
952646f066 | ||
|
|
1e037ecb60 | ||
|
|
c9f75d82d6 | ||
|
|
de3849c20c | ||
|
|
c391c2e938 | ||
|
|
1cde4b1f8a | ||
|
|
42360d109b | ||
|
|
7aa4d9e6db | ||
|
|
9ed3390b81 | ||
|
|
1ff56f7bfd | ||
|
|
16917997cd | ||
|
|
f42568d262 | ||
|
|
97bbdf561a | ||
|
|
f4043cbd9f | ||
|
|
e9ff26abb4 | ||
|
|
e6b1298915 | ||
|
|
98a9ec8335 | ||
|
|
5329561687 | ||
|
|
89ca047f2f | ||
|
|
a94711fcf0 | ||
|
|
97d88c65ae | ||
|
|
e482e3748d | ||
|
|
8ab9c08448 | ||
|
|
56de70b714 | ||
|
|
38d97babd8 | ||
|
|
4cfecb5d82 | ||
|
|
08b5970b2b | ||
|
|
af20f80b2b | ||
|
|
c2fdd2cd70 | ||
|
|
286f3eebe4 | ||
|
|
509223ab2e | ||
|
|
665b0b8ed2 | ||
|
|
508ad52582 | ||
|
|
cfd5c98f97 | ||
|
|
2e63bb1f50 | ||
|
|
982c8f6b5a | ||
|
|
12c4c115f3 | ||
|
|
d4d20ecdb0 | ||
|
|
b78919c6ed | ||
|
|
abb9320e31 | ||
|
|
875d2d5a3a | ||
|
|
42a58855a0 | ||
|
|
9d142758ec | ||
|
|
2faddd85cb | ||
|
|
ddd463748d | ||
|
|
71ee4cd09d | ||
|
|
08c56f7d6c | ||
|
|
e1ba26b62c | ||
|
|
309f0bf826 | ||
|
|
1698ffa7db | ||
|
|
556955cf1a | ||
|
|
a9f778459a | ||
|
|
f2ac412253 | ||
|
|
3bd52fcc9d | ||
|
|
b9bd1e103c | ||
|
|
55f9081465 | ||
|
|
0bac16dba0 | ||
|
|
a8a9c575b5 | ||
|
|
15ec855db5 | ||
|
|
b8f79a7e56 | ||
|
|
b988ee85f4 | ||
|
|
d6e166f105 | ||
|
|
53ceca8ff8 | ||
|
|
f2f9759138 | ||
|
|
817152ff39 | ||
|
|
38daaf3b72 | ||
|
|
666d53e2ba | ||
|
|
b55e9a9e64 | ||
|
|
8f94c8e6b1 | ||
|
|
604ba3ff90 | ||
|
|
2e671887d9 | ||
|
|
e7d56b76b8 | ||
|
|
87d5bddb21 | ||
|
|
9d46b7bcb0 | ||
|
|
591212a56a | ||
|
|
1a3c1e9e52 | ||
|
|
94998aa95e | ||
|
|
c782017578 | ||
|
|
d291cdb968 | ||
|
|
29fb1842a5 | ||
|
|
b085a6c38e | ||
|
|
17c12823db | ||
|
|
81401a738c | ||
|
|
c9476af063 | ||
|
|
1f95eb1e1d | ||
|
|
1584ab6b72 | ||
|
|
c63c8ac447 | ||
|
|
1a0e2b509e | ||
|
|
c66b673067 | ||
|
|
a13468228a | ||
|
|
bb0324fd48 | ||
|
|
369ea8531f | ||
|
|
9cc28943aa | ||
|
|
10bdff7d4b | ||
|
|
27fb430900 | ||
|
|
207dedab6e | ||
|
|
0052b10df3 | ||
|
|
08d98691a3 | ||
|
|
34aa0e014b | ||
|
|
55ce805b60 | ||
|
|
1e0adcd89a | ||
|
|
4e67b3aef8 | ||
|
|
02e2948e00 | ||
|
|
bd86125629 | ||
|
|
11d9018e4f | ||
|
|
553e38ba15 | ||
|
|
c8e6d18139 | ||
|
|
e29f6a884f | ||
|
|
10a2ada42a | ||
|
|
2dd9116a20 | ||
|
|
2ee323135f | ||
|
|
cebc824fbe | ||
|
|
dd0bcb950a | ||
|
|
bb5ad101a2 | ||
|
|
df90f183fd | ||
|
|
0c261c0fb0 | ||
|
|
f46084723a | ||
|
|
08ad6832af | ||
|
|
700a110350 | ||
|
|
33b5d8c4fb | ||
|
|
ceb7bd398d | ||
|
|
7f72c804f4 | ||
|
|
be0a793179 | ||
|
|
e1625106b8 | ||
|
|
abef12279b | ||
|
|
578853877a | ||
|
|
04eae63e39 | ||
|
|
4b98ece2b4 | ||
|
|
1b2a0fe2ad | ||
|
|
ab6ddc1194 | ||
|
|
b153ab51ee | ||
|
|
34f7b90177 | ||
|
|
3648af0d9d | ||
|
|
3d50ca32cd | ||
|
|
e576d93f83 | ||
|
|
e71779b8a6 | ||
|
|
0301c7f1cf | ||
|
|
46e6e7629c | ||
|
|
a985ae5660 | ||
|
|
efe29fd057 | ||
|
|
05b5b25c0e | ||
|
|
46f6deddbf | ||
|
|
e9c4b490e5 | ||
|
|
f7d0eea04d | ||
|
|
bf246a62e7 | ||
|
|
b8acfd758d | ||
|
|
dc8fd37461 | ||
|
|
cc2b310e4c | ||
|
|
884348d7f8 | ||
|
|
dafd6e51c5 | ||
|
|
322041917d | ||
|
|
a2eaf9b3ee | ||
|
|
4032bfc210 | ||
|
|
4ff08f042b | ||
|
|
ce4b0ecbe1 | ||
|
|
6c31d09eb2 | ||
|
|
59ad6a4725 | ||
|
|
884dacbc6c | ||
|
|
9c572f7617 | ||
|
|
487dba90d8 | ||
|
|
20f24d2f3a | ||
|
|
20cbc240ee | ||
|
|
f2f52b2533 | ||
|
|
46d6e3f78c | ||
|
|
586f524015 | ||
|
|
79781ec20c | ||
|
|
91f6d9a404 | ||
|
|
76a74ecde2 | ||
|
|
0071a48b8a | ||
|
|
8d42e91eb8 |
45
.github/workflows/check-examples.yml
vendored
Normal file
45
.github/workflows/check-examples.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Check examples ${{ 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 examples
|
||||
run: cargo make check-examples
|
||||
45
.github/workflows/check-stable.yml
vendored
Normal file
45
.github/workflows/check-stable.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
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 examples
|
||||
run: cargo make --profile=github-actions check-stable
|
||||
45
.github/workflows/check.yml
vendored
Normal file
45
.github/workflows/check.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
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
|
||||
34
.github/workflows/fmt.yml
vendored
Normal file
34
.github/workflows/fmt.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Test
|
||||
|
||||
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
|
||||
37
.github/workflows/publish-book.yml
vendored
Normal file
37
.github/workflows/publish-book.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Deploy book
|
||||
on:
|
||||
push:
|
||||
paths: ['docs/book/**']
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pull-requests: write # To create a PR from that branch
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install mdbook
|
||||
run: |
|
||||
mkdir mdbook
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.27/mdbook-v0.4.27-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Deploy GitHub Pages
|
||||
run: |
|
||||
cd docs/book
|
||||
mdbook build
|
||||
git worktree add gh-pages
|
||||
git config user.name "Deploy book from CI"
|
||||
git config user.email ""
|
||||
cd gh-pages
|
||||
# Delete the ref to avoid keeping history.
|
||||
git update-ref -d refs/heads/gh-pages
|
||||
rm -rf *
|
||||
mv ../book/* .
|
||||
git add .
|
||||
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
|
||||
git push --force --set-upstream origin gh-pages
|
||||
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@@ -11,7 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
name: Run tests ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -39,10 +39,7 @@ jobs:
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make ci
|
||||
run: cargo make --profile=github-actions test
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.idea
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
29
Cargo.toml
29
Cargo.toml
@@ -4,13 +4,19 @@ members = [
|
||||
"leptos",
|
||||
"leptos_dom",
|
||||
"leptos_config",
|
||||
"leptos_hot_reload",
|
||||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
"server_fn",
|
||||
"server_fn_macro",
|
||||
"server_fn/server_fn_macro_default",
|
||||
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
"integrations/axum",
|
||||
"integrations/viz",
|
||||
"integrations/utils",
|
||||
|
||||
# libraries
|
||||
"meta",
|
||||
@@ -19,17 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.0-alpha"
|
||||
version = "0.3.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.0-alpha" }
|
||||
leptos_router = { path = "./router", version = "0.2.0-alpha" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.0-alpha" }
|
||||
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" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -7,47 +7,114 @@
|
||||
# make tasks run at the workspace root
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["build", "check-examples", "test"]
|
||||
|
||||
[tasks.build]
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = ["build-all", "build-wasm"]
|
||||
dependencies = [
|
||||
"check-all",
|
||||
"check-wasm",
|
||||
"check-all-release",
|
||||
"check-wasm-release",
|
||||
]
|
||||
|
||||
[tasks.build-all]
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.build-wasm]
|
||||
[tasks.check-wasm]
|
||||
clear = true
|
||||
dependencies = [{ name = "build-wasm", path = "leptos" }]
|
||||
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/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[tasks.check-stable]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
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"
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "test-unit-and-web"]
|
||||
|
||||
[tasks.verify-examples]
|
||||
description = "Run all quality checks and tests for examples"
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "verify-flow"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
description = "Clean all example projects"
|
||||
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"
|
||||
|
||||
48
README.md
48
README.md
@@ -6,6 +6,7 @@
|
||||
[](https://crates.io/crates/leptos)
|
||||
[](https://docs.rs/leptos)
|
||||
[](https://discord.gg/YdRAhS7eQB)
|
||||
[](https://matrix.to/#/#leptos:matrix.org)
|
||||
|
||||
# Leptos
|
||||
|
||||
@@ -24,8 +25,7 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// create user interfaces with the declarative `view!` macro
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
@@ -48,27 +48,27 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
|
||||
## What does that mean?
|
||||
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
|
||||
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
|
||||
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)
|
||||
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
|
||||
|
||||
## Learn more
|
||||
|
||||
Here are some resources for learning more about Leptos:
|
||||
|
||||
- [Book](https://leptos-rs.github.io/leptos/) (work in progress)
|
||||
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
|
||||
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
- Leptos Guide (in progress)
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` Rust.
|
||||
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.
|
||||
|
||||
To set up your Rust toolchain using `nightly` (and add the ability to compile Rust to WebAssembly, if you haven’t already)
|
||||
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you haven’t already):
|
||||
|
||||
```
|
||||
rustup toolchain install nightly
|
||||
@@ -76,9 +76,17 @@ rustup default nightly
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
|
||||
|
||||
```toml
|
||||
[toolchain]
|
||||
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.1.0", features = ["stable"] }`
|
||||
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)
|
||||
@@ -86,7 +94,7 @@ If you’re on `stable`, note the following:
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our [starter template](https://github.com/leptos-rs/start).
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
@@ -95,17 +103,21 @@ cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
Open browser to [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
## FAQs
|
||||
|
||||
### 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.
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
People usually mean one of three things by this question.
|
||||
|
||||
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
|
||||
|
||||
With 0.1 the APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
|
||||
The APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
@@ -115,7 +127,7 @@ Yes, I’m sure there are. You can see from the state of our issue tracker over
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
@@ -133,8 +145,8 @@ I've put together a [very simple GTK example](https://github.com/leptos-rs/lepto
|
||||
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
|
||||
|
||||
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply _much_ faster at both creating and updating the UI than Yew is.
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is. (Dioxus has made huge advances in performance with its recent 0.3 release, and is now roughly on par with Leptos.)
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. You can call functions, create timeouts, etc. within the body of your component functions because they won’t be re-run. You don’t need to think about manual dependency tracking for effects; fine-grained reactivity tracks dependencies automatically.
|
||||
|
||||
### How is this different from Sycamore?
|
||||
|
||||
@@ -142,9 +154,9 @@ Conceptually, these two frameworks are very similar: because both are built on f
|
||||
|
||||
There are some practical differences that make a significant difference:
|
||||
|
||||
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
|
||||
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
|
||||
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
|
||||
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
|
||||
|
||||
```rust
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
@@ -16,15 +17,11 @@ lazy_static = "1"
|
||||
log = "0.4"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
serde = { version = "1", features = ["derive", "rc"]}
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
tera = "1"
|
||||
reactive-signals = "0.1.0-alpha.4"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Window",
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement"
|
||||
]
|
||||
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
extern crate test;
|
||||
|
||||
//mod reactive;
|
||||
mod ssr;
|
||||
//åmod reactive;
|
||||
//mod ssr;
|
||||
mod todomvc;
|
||||
|
||||
@@ -1,35 +1,113 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use test::Bencher;
|
||||
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
fn leptos_deep_creation(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
let prev = memos.last().copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_deep_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_down(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
fn leptos_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(|cx| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
@@ -48,17 +126,20 @@ fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_scope, create_signal};
|
||||
fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope({
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
@@ -76,16 +157,252 @@ fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
fn rs_deep_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
|
||||
let sc = ClientRuntime::new_root_scope();
|
||||
b.iter(|| {
|
||||
let signal = signal!(sc, 0);
|
||||
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(signal!(sc, move || prev.get() + 1))
|
||||
} else {
|
||||
memos.push(signal!(sc, move || signal.get() + 1))
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_fanning_out(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
let cx = ClientRuntime::new_root_scope();
|
||||
|
||||
b.iter(|| {
|
||||
let sig = signal!(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| signal!(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_narrowing_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
let cx = ClientRuntime::new_root_scope();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
|
||||
let memo = signal!(cx, {
|
||||
let sigs = sigs.clone();
|
||||
move || {
|
||||
sigs.iter().map(|r| r.get()).sum::<i32>()
|
||||
}
|
||||
});
|
||||
assert_eq!(memo.get(), 499500);
|
||||
signal!(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move || {
|
||||
acc.set(memo.get());
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
sigs[1].update(|n| *n += 1);
|
||||
sigs[10].update(|n| *n += 1);
|
||||
sigs[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo.get(), 499503);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_creation(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_narrowing_down(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
#[bench]
|
||||
fn l021_narrowing_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
writes[1].update(|n| *n += 1);
|
||||
writes[10].update(|n| *n += 1);
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
move |_| {
|
||||
acc.set(r());
|
||||
}
|
||||
});
|
||||
w.update(|n| *n += 1);
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for disposer in disposers {
|
||||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_narrowing_down(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
@@ -97,13 +414,78 @@ fn sycamore_create_1000_signals(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
fn sycamore_fanning_out(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let sig = create_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 1000);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_creation(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(*memos[999].get(), 1001);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
#[bench]
|
||||
fn sycamore_narrowing_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
@@ -129,7 +511,7 @@ fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
fn sycamore_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
|
||||
@@ -4,7 +4,7 @@ use test::Bencher;
|
||||
fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use leptos::*;
|
||||
HydrationCtx::reset_id();
|
||||
leptos_dom::HydrationCtx::reset_id();
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
|
||||
@@ -32,7 +32,8 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>" );
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub use leptos::*;
|
||||
use miniserde::*;
|
||||
use web_sys::HtmlInputElement;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Todos(pub Vec<Todo>);
|
||||
@@ -110,10 +111,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
provide_context(cx, set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
@@ -167,57 +164,79 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" on:keydown=add_todo />
|
||||
</header>
|
||||
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox"
|
||||
prop:checked={move || todos.with(|t| t.remaining() > 0)}
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |todo: Todo| view! { cx, <Todo todo=todo.clone() /> }
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 {
|
||||
" item"
|
||||
} else {
|
||||
" items"
|
||||
}}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
|
||||
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
|
||||
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden={move || todos.with(|t| t.completed() == 0)}
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus=""
|
||||
on:keydown=add_todo
|
||||
/>
|
||||
</header>
|
||||
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
type="checkbox"
|
||||
prop:checked=move || todos.with(|t| t.remaining() > 0)
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |cx, todo: Todo| {
|
||||
view! { cx, <Todo todo=todo.clone()/> }
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a
|
||||
href="#/"
|
||||
class="selected"
|
||||
class:selected=move || mode() == Mode::All
|
||||
>
|
||||
"All"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/active" class:selected=move || mode() == Mode::Active>
|
||||
"Active"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/completed" class:selected=move || mode() == Mode::Completed>
|
||||
"Completed"
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden=move || todos.with(|t| t.completed() == 0)
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by " <a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -237,41 +256,36 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || (todo.completed)()}
|
||||
//_ref=input
|
||||
>
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
|
||||
/>
|
||||
<label on:dblclick=move |_| set_editing(true)>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
|
||||
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
|
||||
<button
|
||||
class="destroy"
|
||||
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
|
||||
></button>
|
||||
</div>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
{move || {
|
||||
editing()
|
||||
.then(|| {
|
||||
view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
prop:value=move || todo.title.get()
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup=move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,15 @@ mod yew;
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new(cx)/>
|
||||
}
|
||||
.into_view(cx)
|
||||
.render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,21 +53,20 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
/*
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new_with_1000(cx)/>
|
||||
}.into_view(cx).render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
}
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,5 +103,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -174,4 +174,4 @@ fn tera_todomvc_1000(b: &mut Bencher) {
|
||||
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,52 @@ let (a, set_a) = create_signal(cx, 0);
|
||||
let b = move || a () > 5;
|
||||
```
|
||||
|
||||
### Nested signal updates/reads triggering panic
|
||||
|
||||
Sometimes you have nested signals: for example, hash-map that can change over time, each of whose values can also change over time:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let resources = create_rw_signal(cx, HashMap::new());
|
||||
|
||||
let update = move |id: usize| {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(cx, 0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
|
||||
<button on:click=move |_| update(1)>"+"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```rust
|
||||
let update = move |id: usize| {
|
||||
cx.batch(move || {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(cx, 0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
This delays running any effects until after both updates are made, preventing the conflict entirely without requiring any other restructuring.
|
||||
|
||||
## Templates and the DOM
|
||||
|
||||
### `<input value=...>` doesn't update or stops updating
|
||||
@@ -61,3 +107,19 @@ view! {
|
||||
<input prop:value=a on:input=on_input />
|
||||
}
|
||||
```
|
||||
|
||||
## Build configuration
|
||||
|
||||
### Cargo feature resolution in workspaces
|
||||
|
||||
A new [version](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) of Cargo's feature resolver was introduced for the 2021 edition of Rust.
|
||||
For single crate projects it will select a resolver version based on the Rust edition in `Cargo.toml`. As there is no Rust edition present for `Cargo.toml` in a workspace, Cargo will default to the pre 2021 edition resolver.
|
||||
This can cause issues resulting in non WASM compatible code being built for a WASM target. Seeing `mio` failing to build is often a sign that none WASM compatible code is being included in the build.
|
||||
|
||||
The resolver version can be set in the workspace `Cargo.toml` to remedy this issue.
|
||||
|
||||
```toml
|
||||
[workspace]
|
||||
members = ["member1", "member2"]
|
||||
resolver = "2"
|
||||
```
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Getting Started
|
||||
|
||||
There are two basic paths to getting started with Leptos:
|
||||
|
||||
1. Client-side rendering with [Trunk](https://trunkrs.dev/)
|
||||
2. Full-stack rendering with [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos)
|
||||
|
||||
For the early examples, it will be easiest to begin with Trunk. We’ll introduce
|
||||
`cargo-leptos` a little later in this series.
|
||||
|
||||
|
||||
If you don’t already have it installed, you can install Trunk by running
|
||||
|
||||
```bash
|
||||
@@ -20,12 +20,22 @@ Create a basic Rust binary project
|
||||
cargo init leptos-tutorial
|
||||
```
|
||||
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
> 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
|
||||
>
|
||||
> ```bash
|
||||
> rustup toolchain install nightly
|
||||
> rustup default nightly
|
||||
> rustup target add wasm32-unknown-unknown
|
||||
> ```
|
||||
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
cargo add leptos
|
||||
```
|
||||
|
||||
Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -35,14 +45,26 @@ Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
```
|
||||
|
||||
And add a simple “Hello, world!” to your `main.rs`
|
||||
```rust
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
}
|
||||
```
|
||||
|
||||
Now run `trunk serve --open`. Trunk should automatically compile your app and
|
||||
open it in your default browser. If you make edits to `main.rs`, Trunk will
|
||||
recompile your source code and live-reload the page.
|
||||
Your directory structure should now look something like this
|
||||
|
||||
```
|
||||
leptos_tutorial
|
||||
├── src
|
||||
│ └── main.rs
|
||||
├── Cargo.toml
|
||||
├── index.html
|
||||
```
|
||||
|
||||
Now run `trunk serve --open` from the root of the `leptos-tutorial` directory.
|
||||
Trunk should automatically compile your app and open it in your default browser.
|
||||
If you make edits to `main.rs`, Trunk will recompile your source code and
|
||||
live-reload the page.
|
||||
|
||||
112
docs/book/src/14_create_effect.md
Normal file
112
docs/book/src/14_create_effect.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Responding to Changes with `create_effect`
|
||||
|
||||
Believe it or not, we’ve made it this far without having mentioned half of the reactive system: effects.
|
||||
|
||||
Leptos is built on a fine-grained reactive system, which means that individual reactive values (“signals,” sometimes known as observables) trigger rerunning the code that reacts to them (“effects,” sometimes known as observers). These two halves of the reactive system are inter-dependent. Without effects, signals can change within the reactive system but never be observed in a way that interacts with the outside world. Without signals, effects run once but never again, as there’s no observable value to subscribe to.
|
||||
|
||||
[`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);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
// immediately prints "Value: 0" and subscribes to `a`
|
||||
log::debug!("Value: {}", a());
|
||||
});
|
||||
```
|
||||
|
||||
The effect function is called with an argument containing whatever value it returned the last time it ran. On the initial run, this is `None`.
|
||||
|
||||
By default, effects **do not run on the server**. This means you can call browser-specific APIs within the effect function without causing issues. If you need an effect to run on the server, use [`create_isomorphic_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_isomorphic_effect.html).
|
||||
|
||||
## Autotracking and Dynamic Dependencies
|
||||
|
||||
If you’re familiar with a framework like React, you might notice one key difference. React and similar frameworks typically require you to pass a “dependency array,” an explicit set of variables that determine when the effect should rerun.
|
||||
|
||||
Because Leptos comes from the tradition of synchronous reactive programming, we don’t need this explicit dependency list. Instead, we automatically track dependencies depending on which signals are accessed within the effect.
|
||||
|
||||
This has two effects (no pun intended). Dependencies are
|
||||
|
||||
1. **Automatic**: You don’t need to maintain a dependency list, or worry about what should or shouldn’t be included. The framework simply tracks which signals might cause the effect to rerun, and handles it for you.
|
||||
2. **Dynamic**: The dependency list is cleared and updated every time the effect runs. If your effect contains a conditional (for example), only signals that are used in the current branch are tracked. This means that effects rerun the absolute minimum number of times.
|
||||
|
||||
> If this sounds like magic, and if you want a deep dive into how automatic dependency tracking works, [check out this video](https://www.youtube.com/watch?v=GWB3vTWeLd4). (Apologies for the low volume!)
|
||||
|
||||
## Effects as Zero-Cost-ish Abstraction
|
||||
|
||||
While they’re not a “zero-cost abstraction” in the most technical sense—they require some additional memory use, exist at runtime, etc.—at a higher level, from the perspective of whatever expensive API calls or other work you’re doing within them, effects are a zero-cost abstraction. They rerun the absolute minimum number of times necessary, given how you’ve described them.
|
||||
|
||||
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);
|
||||
|
||||
// this will add the name to the log
|
||||
// any time one of the source signals changes
|
||||
create_effect(cx, move |_| {
|
||||
log(
|
||||
cx,
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
first()
|
||||
},
|
||||
)
|
||||
});
|
||||
```
|
||||
|
||||
If `use_last` is `true`, effect should rerun whenever `first`, `last`, or `use_last` changes. But if I toggle `use_last` to `false`, a change in `last` will never cause the full name to change. In fact, `last` will be removed from the dependency list until `use_last` toggles again. This saves us from sending multiple unnecessary requests to the API if I change `last` multiple times while `use_last` is still `false`.
|
||||
|
||||
## To `create_effect`, or not to `create_effect`?
|
||||
|
||||
Effects are intended to run _side-effects_ of the system, not to synchronize state _within_ the system. In other words: don’t write to signals within effects.
|
||||
|
||||
If you need to define a signal that depends on the value of other signals, use a derived signal or [`create_memo`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_memo.html).
|
||||
|
||||
If you need to synchronize some reactive value with the non-reactive world outside—like a web API, the console, the filesystem, or the DOM—create an effect.
|
||||
|
||||
> If you’re curious for more information about when you should and shouldn’t use `create_effect`, [check out this video](https://www.youtube.com/watch?v=aQOFJQ2JkvQ) for a more in-depth consideration!
|
||||
|
||||
## Effects and Rendering
|
||||
|
||||
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);
|
||||
|
||||
view! { cx,
|
||||
<p>{count}</p>
|
||||
}
|
||||
```
|
||||
|
||||
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);
|
||||
|
||||
// create a DOM element
|
||||
let p = create_element("p");
|
||||
|
||||
// create an effect to reactively update the text
|
||||
create_effect(cx, move |prev_value| {
|
||||
// first, access the signal’s value and convert it to a string
|
||||
let text = count().to_string();
|
||||
|
||||
// if this is different from the previous value, update the node
|
||||
if prev_value != Some(text) {
|
||||
p.set_text_content(&text);
|
||||
}
|
||||
|
||||
// return this value so we can memoize the next update
|
||||
text
|
||||
});
|
||||
```
|
||||
|
||||
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
|
||||
|
||||
[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>
|
||||
174
docs/book/src/15_global_state.md
Normal file
174
docs/book/src/15_global_state.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Global State Management
|
||||
|
||||
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: Passing 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.
|
||||
|
||||
We start by creating a signal in the root of the app and providing it to
|
||||
all its children and descendants using `provide_context`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> 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);
|
||||
// we'll pass the setter to specific components,
|
||||
// but provide the count itself to the whole app via context
|
||||
provide_context(cx, count);
|
||||
|
||||
view! { cx,
|
||||
// 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
|
||||
<FancyMath/>
|
||||
<ListItems/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`<SetterButton/>` is the kind of counter we’ve written several times now.
|
||||
(See the sandbox below if you don’t understand what I mean.)
|
||||
|
||||
`<FancyMath/>` and `<ListItems/>` both consume the signal we’re providing via
|
||||
`use_context` and do something with it.
|
||||
|
||||
```rust
|
||||
/// A component that does some "fancy" math with the global count
|
||||
#[component]
|
||||
fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
// here we consume the global count signal with `use_context`
|
||||
let count = use_context::<ReadSignal<u32>>(cx)
|
||||
// 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,
|
||||
<div class="consumer blue">
|
||||
"The number "
|
||||
<strong>{count}</strong>
|
||||
{move || if is_even() {
|
||||
" is"
|
||||
} else {
|
||||
" is not"
|
||||
}}
|
||||
" even."
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This kind of “provide a signal in a parent, consume it in a child” should be familiar
|
||||
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
|
||||
pattern you use to communicate between parents and children works for grandparents and
|
||||
grandchildren, or any ancestors and descendants: in other words, between “global” state
|
||||
in the root component of your app and any other components anywhere else in the app.
|
||||
|
||||
Because of the fine-grained nature of updates, this is usually all you need. However,
|
||||
in some cases with more complex state changes, you may want to use a slightly more
|
||||
structured approach to global state.
|
||||
|
||||
## 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`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html)
|
||||
or [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html),
|
||||
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.
|
||||
|
||||
You can begin by defining a simple state struct:
|
||||
|
||||
```rust
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct GlobalState {
|
||||
count: u32,
|
||||
name: String,
|
||||
}
|
||||
```
|
||||
|
||||
Provide it in the root of your app so it’s available everywhere.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> 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(cx, GlobalState::default());
|
||||
provide_context(cx, state);
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Then child components can access “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 = use_context::<RwSignal<GlobalState>>(cx).expect("state to have been provided");
|
||||
|
||||
// `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
|
||||
|state| state.count,
|
||||
// our setter describes how to mutate that slice, given a new value
|
||||
|state, n| state.count = n,
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
<div class="consumer blue">
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count(count() + 1);
|
||||
}
|
||||
>
|
||||
"Increment Global Count"
|
||||
</button>
|
||||
<br/>
|
||||
<span>"Count is: " {count}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clicking this button only updates `state.count`, so if we create another slice
|
||||
somewhere else that only takes `state.name`, clicking the button won’t cause
|
||||
that other slice to update. This allows you to combine the benefits of a top-down
|
||||
data flow and of fine-grained reactive updates.
|
||||
|
||||
[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>
|
||||
@@ -14,28 +14,39 @@
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [Interlude: Reactivity and Functions](./interlude_functions.md)
|
||||
- [Testing](./testing.md)
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Async]()
|
||||
- [Resource]()
|
||||
- [Suspense]()
|
||||
- [Transition]()
|
||||
- [State Management]()
|
||||
- [Interlude: Advanced Reactivity]()
|
||||
- [Router]()
|
||||
- [Fundamentals]()
|
||||
- [defining `<Routes/>`]()
|
||||
- [`<A/>`]()
|
||||
- [`<Form/>`]()
|
||||
- [Async](./async/README.md)
|
||||
- [Loading Data with Resources](./async/10_resources.md)
|
||||
- [Suspense](./async/11_suspense.md)
|
||||
- [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)
|
||||
- [Nested Routing](./router/17_nested_routing.md)
|
||||
- [Params and Queries](./router/18_params_and_queries.md)
|
||||
- [`<A/>`](./router/19_a.md)
|
||||
- [`<Form/>`](./router/20_form.md)
|
||||
- [Interlude: Styling](./interlude_styling.md)
|
||||
- [Metadata]()
|
||||
- [SSR]()
|
||||
- [Models of SSR]()
|
||||
- [`cargo-leptos`]()
|
||||
- [Hydration Footguns]()
|
||||
- [Request/Response]()
|
||||
- [Server Side Rendering](./ssr/README.md)
|
||||
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
|
||||
- [The Life of a Page Load](./ssr/22_life_cycle.md)
|
||||
- [Async Rendering and SSR “Modes”](./ssr/23_ssr_modes.md)
|
||||
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
|
||||
- [Working with the Server](./server/README.md)
|
||||
- [Server Functions](./server/25_server_functions.md)
|
||||
- [Request/Response]()
|
||||
- [Extractors]()
|
||||
- [Axum]()
|
||||
- [Actix]()
|
||||
- [Headers]()
|
||||
- [Cookies]()
|
||||
- [Server Functions]()
|
||||
- [Building Full-Stack Apps]()
|
||||
- [Actions]()
|
||||
- [Forms]()
|
||||
- [`<ActionForm/>`s]()
|
||||
- [Turning off WebAssembly]()
|
||||
- [Turning off WebAssembly: Progressive Enhancement and Graceful Degradation]()
|
||||
- [Advanced Reactivity]()
|
||||
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)
|
||||
|
||||
66
docs/book/src/appendix_binary_size.md
Normal file
66
docs/book/src/appendix_binary_size.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Appendix: Optimizing WASM Binary Size
|
||||
|
||||
One of the primary downsides of deploying a Rust/WebAssembly frontend app is that splitting a WASM file into smaller chunks to be dynamically loaded is significantly more difficult than splitting a JavaScript bundle. There have been experiments like [`wasm-split`](https://emscripten.org/docs/optimizing/Module-Splitting.html) in the Emscripten ecosystem but at present there’s no way to split and dynamically load a Rust/`wasm-bindgen` binary. This means that the whole WASM binary needs to be loaded before your app becomes interactive. Because the WASM format is designed for streaming compilation, WASM files are much faster to compile per kilobyte than JavaScript files. (For a deeper look, you can [read this great article from the Mozilla team](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/) on streaming WASM compilation.)
|
||||
|
||||
Still, it’s important to ship the smallest WASM binary to users that you can, as it will reduce their network usage and make your app interactive as quickly as possible.
|
||||
|
||||
So what are some practical steps?
|
||||
|
||||
## Things to Do
|
||||
|
||||
1. Make sure you’re looking at a release build. (Debug builds are much, much larger.)
|
||||
2. Add a release profile for WASM that optimizes for size, not speed.
|
||||
|
||||
For a `cargo-leptos` project, for example, you can add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
# ....
|
||||
|
||||
[package.metadata.leptos]
|
||||
# ....
|
||||
lib-profile-release = "wasm-release"
|
||||
```
|
||||
|
||||
This will hyper-optimize the WASM for your release build for size, while keeping your server build optimized for speed. (For a pure client-rendered app without server considerations, just use the `[profile.wasm-release]` block as your `[profile.release]`.)
|
||||
|
||||
3. Always serve compressed WASM in production. WASM tends to compress very well, typically shrinking to less than 50% its uncompressed size, and it’s trivial to enable compression for static files being served from Actix or Axum.
|
||||
|
||||
4. If you’re using nightly Rust, you can rebuild the standard library with this same profile rather than the prebuilt standard library that’s distributed with the `wasm32-unknown-unknown` target.
|
||||
|
||||
To do this, create a file in your project at `.cargo/config.toml`
|
||||
|
||||
```toml
|
||||
[unstable]
|
||||
build-std = ["std", "panic_abort", "core", "alloc"]
|
||||
build-std-features = ["panic_immediate_abort"]
|
||||
```
|
||||
|
||||
Note that if you're using this with SSR too, the same Cargo profile will be applied. You'll need to explicitly specify your target:
|
||||
```toml
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-gnu" # or whatever
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Things to Avoid
|
||||
|
||||
There are certain crates that tend to inflate binary sizes. For example, the `regex` crate with its default features adds about 500kb to a WASM binary (largely because it has to pull in Unicode table data!) In a size-conscious setting, you might consider avoiding regexes in general, or even dropping down and calling browser APIs to use the built-in regex engine instead. (This is what `leptos_router` does on the few occasions it needs a regular expression.)
|
||||
|
||||
In general, Rust’s commitment to runtime performance is sometimes at odds with a commitment to a small binary. For example, Rust monomorphizes generic functions, meaning it creates a distinct copy of the function for each generic type it’s called with. This is significantly faster than dynamic dispatch, but increases binary size. Leptos tries to balance runtime performance with binary size considerations pretty carefully; but you might find that writing code that uses many generics tends to increase binary size. For example, if you have a generic component with a lot of code in its body and call it with four different types, remember that the compiler could include four copies of that same code. Refactoring to use a concrete inner function or helper can often maintain performance and ergonomics while reducing binary size.
|
||||
|
||||
## A Final Thought
|
||||
|
||||
Remember that in a server-rendered app, JS bundle size/WASM binary size affects only _one_ thing: time to interactivity on the first load. This is very important to a good user experience—nobody wants to click a button three times and have it do nothing because the interactive code is still loading—but it is not the only important measure.
|
||||
|
||||
It’s especially worth remembering that streaming in a single WASM binary means all subsequent navigations are nearly instantaneous, depending only on any additional data loading. Precisely because your WASM binary is _not_ bundle split, navigating to a new route does not require loading additional JS/WASM, as it does in nearly every JavaScript framework. Is this copium? Maybe. Or maybe it’s just an honest trade-off between the two approaches!
|
||||
|
||||
Always take the opportunity to optimize the low-hanging fruit in your application. And always test your app under real circumstances with real user network speeds and devices before making any heroic efforts.
|
||||
55
docs/book/src/async/10_resources.md
Normal file
55
docs/book/src/async/10_resources.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Loading Data with Resources
|
||||
|
||||
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`):
|
||||
|
||||
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`
|
||||
|
||||
Here’s an example
|
||||
|
||||
```rust
|
||||
// our source signal: some synchronous, local state
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
// our resource
|
||||
let async_data = create_resource(cx,
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
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 });
|
||||
```
|
||||
|
||||
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/>`.
|
||||
|
||||
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,
|
||||
<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)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
|
||||
|
||||
[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>
|
||||
74
docs/book/src/async/11_suspense.md
Normal file
74
docs/book/src/async/11_suspense.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# `<Suspense/>`
|
||||
|
||||
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 });
|
||||
|
||||
view! { cx,
|
||||
<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)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
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 });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(cx), b.read(cx)) {
|
||||
(Some(a), Some(b)) => view! { cx,
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
}.into_view(cx),
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
That’s not _so_ bad, but it’s kind of annoying. What if we could invert the flow of control?
|
||||
|
||||
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 });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
>
|
||||
<h2>"My Data"</h2>
|
||||
<h3>"A"</h3>
|
||||
{move || {
|
||||
a.read(cx)
|
||||
.map(|a| view! { cx, <ShowA a/> })
|
||||
}}
|
||||
<h3>"B"</h3>
|
||||
{move || {
|
||||
b.read(cx)
|
||||
.map(|b| view! { cx, <ShowB b/> })
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
```
|
||||
|
||||
Every time one of the resources is reloading, the `"Loading..."` fallback will show again.
|
||||
|
||||
This inversion of the flow of control makes it easier to add or remove individual resources, as you don’t need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which we’ll talk about during a later chapter.
|
||||
|
||||
[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>
|
||||
11
docs/book/src/async/12_transition.md
Normal file
11
docs/book/src/async/12_transition.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# `<Transition/>`
|
||||
|
||||
You’ll notice in the `<Suspense/>` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, there’s [`<Transition/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html).
|
||||
|
||||
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
|
||||
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
|
||||
|
||||
[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>
|
||||
96
docs/book/src/async/13_actions.md
Normal file
96
docs/book/src/async/13_actions.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Mutating Data with Actions
|
||||
|
||||
We’ve talked about how to load `async` data with resources. Resources immediately load data and work closely with `<Suspense/>` and `<Transition/>` components to show whether data is loading in your app. But what if you just want to call some arbitrary `async` function and keep track of what it’s doing?
|
||||
|
||||
Well, you could always use [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html). This allows you to just spawn an `async` task in a synchronous environment by handing the `Future` off to the browser (or, on the server, Tokio or whatever other runtime you’re using). But how do you know if it’s still pending? Well, you could just set a signal to show whether it’s loading, and another one to show the result...
|
||||
|
||||
All of this is true. Or you could use the final `async` primitive: [`create_action`](https://docs.rs/leptos/latest/leptos/fn.create_action.html).
|
||||
|
||||
Actions and resources seem similar, but they represent fundamentally different things. If you’re trying to load data by running an `async` function, either once or when some other value changes, you probably want to use `create_resource`. If you’re trying to occasionally run an `async` function in response to something like a user clicking a button, you probably want to use `create_action`.
|
||||
|
||||
Say we have some `async` function we want to run.
|
||||
|
||||
```rust
|
||||
async fn add_todo(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.”
|
||||
|
||||
> 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 input = input.clone();
|
||||
> async move { todo!() }
|
||||
> });
|
||||
>
|
||||
> // if there are no arguments, use the unit type `()`
|
||||
> let action2 = create_action(cx, |input: &()| async { todo!() });
|
||||
>
|
||||
> // if there are multiple arguments, use a tuple
|
||||
> let action3 = create_action(cx,
|
||||
> |input: &(usize, String)| async { todo!() }
|
||||
> );
|
||||
> ```
|
||||
>
|
||||
> Because the action function takes a reference but the `Future` needs to have a `'static` lifetime, you’ll usually need to clone the value to pass it into the `Future`. This is admittedly awkward but it unlocks some powerful features like optimistic UI. We’ll see a little more about that in future chapters.
|
||||
|
||||
So in this case, all we need to do to create an action is
|
||||
|
||||
```rust
|
||||
let add_todo = create_action(cx, |input: &String| {
|
||||
let input = input.to_owned();
|
||||
async move { add_todo(&input).await }
|
||||
});
|
||||
```
|
||||
|
||||
Rather than calling `add_todo` directly, we’ll call it with `.dispatch()`, as in
|
||||
|
||||
```rust
|
||||
add_todo.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.
|
||||
|
||||
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>>
|
||||
```
|
||||
|
||||
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);
|
||||
|
||||
view! { cx,
|
||||
<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>
|
||||
// use our loading state
|
||||
<p>{move || pending().then("Loading...")}</p>
|
||||
}
|
||||
```
|
||||
|
||||
Now, there’s a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, you’ll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Don’t worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.)
|
||||
|
||||
[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>
|
||||
9
docs/book/src/async/README.md
Normal file
9
docs/book/src/async/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Working with `async`
|
||||
|
||||
So far we’ve only been working with synchronous users interfaces: You provide some input,
|
||||
the app immediately processes it and updates the interface. This is great, but is a tiny
|
||||
subset of what web applications do. In particular, most web apps have to deal with some kind of asynchronous data loading, usually loading something from an API.
|
||||
|
||||
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code. Leptos provides a cross-platform [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html) function that makes it easy to run a `Future`, but there’s much more to it than that.
|
||||
|
||||
In this chapter, we’ll see how Leptos helps smooth out that process for you.
|
||||
@@ -12,19 +12,19 @@ let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
let count_is_odd = move || count() & 1 == 1;
|
||||
let text = move || if count_is_odd() {
|
||||
"odd"
|
||||
"odd"
|
||||
} else {
|
||||
"even"
|
||||
"even"
|
||||
};
|
||||
|
||||
// an effect automatically tracks the signals it depends on
|
||||
// and re-runs when they change
|
||||
// and reruns when they change
|
||||
create_effect(cx, move |_| {
|
||||
log!("text = {}", text());
|
||||
log!("text = {}", text());
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<p>{move || text().to_uppercase()}</p>
|
||||
<p>{move || text().to_uppercase()}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -45,7 +45,7 @@ The key phrase here is “runs some kind of code.” The natural way to “run s
|
||||
|
||||
1. virtual DOM (VDOM) frameworks like React, Yew, or Dioxus rerun a component or render function over and over, to generate a virtual DOM tree that can be reconciled with the previous result to patch the DOM
|
||||
2. compiled frameworks like Angular and Svelte divide your component templates into “create” and “update” functions, rerunning the update function when they detect a change to the component’s state
|
||||
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that re-run
|
||||
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that rerun
|
||||
|
||||
That’s what all our components are doing.
|
||||
|
||||
@@ -59,16 +59,16 @@ pub fn SimpleCounter(cx: Scope) -> impl IntoView {
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
view! { cx,
|
||||
<button on:click=increment>
|
||||
{value}
|
||||
</button>
|
||||
<button on:click=increment>
|
||||
{value}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 re-run 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 responsive to a change.
|
||||
|
||||
So remember two things:
|
||||
|
||||
|
||||
177
docs/book/src/interlude_projecting_children.md
Normal file
177
docs/book/src/interlude_projecting_children.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Projecting Children
|
||||
|
||||
As you build components you may occasionally find yourself wanting to “project” children through multiple layers of components.
|
||||
|
||||
## The Problem
|
||||
|
||||
Consider the following:
|
||||
|
||||
```rust
|
||||
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
<Show
|
||||
// check whether user is verified
|
||||
// by reading from the resource
|
||||
when=move || todo!()
|
||||
fallback=fallback
|
||||
>
|
||||
{children(cx)}
|
||||
</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.
|
||||
|
||||
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.”
|
||||
|
||||
This won’t compile.
|
||||
|
||||
```
|
||||
error[E0507]: cannot move out of `fallback`, a captured variable in an `Fn` closure
|
||||
error[E0507]: cannot move out of `children`, a captured variable in an `Fn` closure
|
||||
```
|
||||
|
||||
The problem here is that both `<Suspense/>` and `<Show/>` need to be able to construct their `children` multiple times. The first time you construct `<Suspense/>`’s children, it would take ownership of `fallback` and `children` to move them into the invocation of `<Show/>`, but then they're not available for future `<Suspense/>` children construction.
|
||||
|
||||
## The Details
|
||||
|
||||
> Feel free to skip ahead to the solution.
|
||||
|
||||
If you want to really understand the issue here, it may help to look at the expanded `view` macro. Here’s a cleaned-up version:
|
||||
|
||||
```rust
|
||||
Suspense(
|
||||
cx,
|
||||
::leptos::component_props_builder(&Suspense)
|
||||
.fallback(|| ())
|
||||
.children({
|
||||
// fallback and children are moved into this closure
|
||||
Box::new(move |cx| {
|
||||
{
|
||||
// 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
|
||||
.fallback(fallback)
|
||||
// and children is moved into Show here
|
||||
.children(children)
|
||||
.build(),
|
||||
)
|
||||
.into_view(cx)),
|
||||
]
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
```
|
||||
|
||||
All components own their props; so the `<Show/>` in this case can’t be called because it only has captured references to `fallback` and `children`.
|
||||
|
||||
## Solution
|
||||
|
||||
However, both `<Suspense/>` and `<Show/>` take `ChildrenFn`, i.e., their `children` should implement the `Fn` type so they can be called multiple times with only an immutable reference. This means we don’t need to own `children` or `fallback`; we just need to be able to pass `'static` references to them.
|
||||
|
||||
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, which we can access or modify through certain methods.
|
||||
|
||||
In this case, it’s really simple:
|
||||
|
||||
```rust
|
||||
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
let fallback = store_value(cx, fallback);
|
||||
let children = store_value(cx, children);
|
||||
view! { cx,
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
<Show
|
||||
when=|| todo!()
|
||||
fallback=move |cx| fallback.with_value(|fallback| fallback(cx))
|
||||
>
|
||||
{children.with_value(|children| children(cx))}
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At the top level, we store both `fallback` and `children` in the reactive scope owned by `LoggedIn`. Now we can simply move those references down through the other layers into the `<Show/>` component and call them there.
|
||||
|
||||
## A Final Note
|
||||
|
||||
Note that this works because `<Show/>` and `<Suspense/>` only need an immutable reference to their children (which `.with_value` can give it), not ownership.
|
||||
|
||||
In other cases, you may need to project owned props through a function that takes `ChildrenFn` and therefore needs to be called more than once. In this case, you may find the `clone:` helper in the`view` macro helpful.
|
||||
|
||||
Consider this example
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let name = "Alice".to_string();
|
||||
view! { cx,
|
||||
<Outer>
|
||||
<Inner>
|
||||
<Inmost name=name.clone()/>
|
||||
</Inner>
|
||||
</Outer>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Outer(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inner(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inmost(cx: Scope, name: String) -> impl IntoView {
|
||||
view! { cx,
|
||||
<p>{name}</p>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Even with `name=name.clone()`, this gives the error
|
||||
|
||||
```
|
||||
cannot move out of `name`, a captured variable in an `Fn` closure
|
||||
```
|
||||
|
||||
It’s captured through multiple levels of children that need to run more than once, and there’s no obvious way to clone it _into_ the children.
|
||||
|
||||
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,
|
||||
<Outer>
|
||||
<Inner clone:name>
|
||||
<Inmost name=name.clone()/>
|
||||
</Inner>
|
||||
</Outer>
|
||||
}
|
||||
```
|
||||
|
||||
These issues can be a little tricky to understand or debug, because of the opacity of the `view` macro. But in general, they can always be solved.
|
||||
112
docs/book/src/interlude_styling.md
Normal file
112
docs/book/src/interlude_styling.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Interlude: Styling
|
||||
|
||||
Anyone creating a website or application soon runs into the question of styling. For a small app, a single CSS file is probably plenty to style your user interface. But as an application grows, many developers find that plain CSS becomes increasingly hard to manage.
|
||||
|
||||
Some frontend frameworks (like Angular, Vue, and Svelte) provide built-in ways to scope your CSS to particular components, making it easier to manage styles across a whole application without styles meant to modify one small component having a global effect. Other frameworks (like React or Solid) don’t provide built-in CSS scoping, but rely on libraries in the ecosystem to do it for them. Leptos is in this latter camp: the framework itself has no opinions about CSS at all, but provides a few tools and primitives that allow others to build styling libraries.
|
||||
|
||||
Here are a few different approaches to styling your Leptos app, other than plain CSS.
|
||||
|
||||
## TailwindCSS: Utility-first CSS
|
||||
|
||||
[TailwindCSS](https://tailwindcss.com/) is a popular utility-first CSS library. It allows you to style your application by using inline utility classes, with a custom CLI tool that scans your files for Tailwind class names and bundles the necessary CSS.
|
||||
|
||||
This allows you to write components like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn Home(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<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>
|
||||
<button
|
||||
class="bg-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
{move || if count() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count().to_string()
|
||||
}}
|
||||
</button>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It can be a little complicated to set up the Tailwind integration at first, but you can check out our two examples of how to use Tailwind with a [client-side-rendered `trunk` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr_trunk) or with a [server-rendered `cargo-leptos` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind). `cargo-leptos` also has some [built-in Tailwind support](https://github.com/leptos-rs/cargo-leptos#site-parameters) that you can use as an alternative to Tailwind’s CLI.
|
||||
|
||||
## Stylers: Compile-time CSS Extraction
|
||||
|
||||
[Stylers](https://github.com/abishekatp/stylers) is a compile-time scoped CSS library that lets you declare scoped CSS in the body of your component. Stylers will extract this CSS at compile time into CSS files that you can then import into your app, which means that it doesn’t add anything to the WASM binary size of your application.
|
||||
|
||||
This allows you to write components like this:
|
||||
|
||||
```rust
|
||||
use stylers::style;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let styler_class = style! { "App",
|
||||
#two{
|
||||
color: blue;
|
||||
}
|
||||
div.one{
|
||||
color: red;
|
||||
content: raw_str(r#"\hello"#);
|
||||
font: "1.3em/1.2" Arial, Helvetica, sans-serif;
|
||||
}
|
||||
div {
|
||||
border: 1px solid black;
|
||||
margin: 25px 50px 75px 100px;
|
||||
background-color: lightblue;
|
||||
}
|
||||
h2 {
|
||||
color: purple;
|
||||
}
|
||||
@media only screen and (max-width: 1000px) {
|
||||
h3 {
|
||||
background-color: lightblue;
|
||||
color: blue
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx, class = styler_class,
|
||||
<div class="one">
|
||||
<h1 id="two">"Hello"</h1>
|
||||
<h2>"World"</h2>
|
||||
<h2>"and"</h2>
|
||||
<h3>"friends!"</h3>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Styled: Runtime CSS Scoping
|
||||
|
||||
[Styled](https://github.com/eboody/styled) is a runtime scoped CSS library that integrates well with Leptos. It lets you declare scoped CSS in the body of your component function, and then applies those styles at runtime.
|
||||
|
||||
```rust
|
||||
use styled::style;
|
||||
|
||||
#[component]
|
||||
pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
let styles = style!(
|
||||
div {
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
);
|
||||
|
||||
styled::view! { cx, styles,
|
||||
<div>"This text should be red with white text."</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Contributions Welcome
|
||||
|
||||
Leptos has no opinions on how you style your website or app, but we’re very happy to provide support to any tools you’re trying to create to make it easier. If you’re working on a CSS or styling approach that you’d like to add to this list, please let us know!
|
||||
101
docs/book/src/router/16_routes.md
Normal file
101
docs/book/src/router/16_routes.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Defining Routes
|
||||
|
||||
## Getting Started
|
||||
|
||||
It’s easy to get started with the router.
|
||||
|
||||
First things first, make sure you’ve added the `leptos_router` package to your dependencies.
|
||||
|
||||
> It’s important that the router is a separate package from `leptos` itself. This means that everything in the router can be defined in user-land code. If you want to create your own router, or use no router, you’re completely free to do that!
|
||||
|
||||
And import the relevant types from the router, either with something like
|
||||
|
||||
```rust
|
||||
use leptos_router::{Route, RouteProps, Router, RouterProps, Routes, RoutesProps};
|
||||
```
|
||||
|
||||
or simply
|
||||
|
||||
```rust
|
||||
use leptos_router::*;
|
||||
```
|
||||
|
||||
## Providing the `<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?
|
||||
|
||||
Let’s start with a simple `<App/>` component using the router:
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
</nav>
|
||||
<main>
|
||||
/* ... */
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Defining `<Routes/>`
|
||||
|
||||
The [`<Routes/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html) component is where you define all the routes to which a user can navigate in your application. Each possible route is defined by a [`<Route/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Route.html) component.
|
||||
|
||||
You should place the `<Routes/>` component at the location within your app where you want routes to be rendered. Everything outside `<Routes/>` will be present on every page, so you can leave things like a navigation bar or menu outside the `<Routes/>`.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
</nav>
|
||||
<main>
|
||||
// all our routes will appear inside <main>
|
||||
<Routes>
|
||||
/* ... */
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Individual routes are defined by providing children to `<Routes/>` with the `<Route/>` component. `<Route/>` takes a `path` and a `view`. When the current location matches `path`, the `view` will be created and displayed.
|
||||
|
||||
The `path` can include
|
||||
|
||||
- a static path (`/users`),
|
||||
- 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.
|
||||
|
||||
```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/> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The router scores each route to see how good a match it is, so you can define your routes in any order.
|
||||
|
||||
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.
|
||||
|
||||
Simple enough?
|
||||
172
docs/book/src/router/17_nested_routing.md
Normal file
172
docs/book/src/router/17_nested_routing.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Nested Routing
|
||||
|
||||
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 /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
There’s a certain amount of duplication here: `/users` and `/users/:id`. This is fine for a small app, but you can probably already tell it won’t scale well. Wouldn’t it be nice if we could nest these routes?
|
||||
|
||||
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>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
But wait. We’ve just subtly changed what our application does.
|
||||
|
||||
The next section is one of the most important in this entire routing section of the guide. Read it carefully, and feel free to ask questions if there’s anything you don’t understand.
|
||||
|
||||
# Nested Routes as Layout
|
||||
|
||||
Nested routes are a form of layout, not a method of route definition.
|
||||
|
||||
Let me put that another way: The goal of defining nested routes is not primarily to avoid repeating yourself when typing out the paths in your route definitions. It is actually to tell the router to display multiple `<Route/>`s on the page at the same time, side by side.
|
||||
|
||||
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 /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- If I go to `/users`, I get the `<Users/>` component.
|
||||
- If I go to `/users/3`, I get the `<UserProfile/>` component (with the parameter `id` set to `3`; more on that later)
|
||||
|
||||
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>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- If I go to `/users/3`, the path matches two `<Route/>`s: `<Users/>` and `<UserProfile/>`.
|
||||
- If I go to `/users`, the path is not matched.
|
||||
|
||||
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>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
Now:
|
||||
|
||||
- If I go to `/users/3`, the path matches `<Users/>` and `<UserProfile/>`.
|
||||
- If I go to `/users`, the path matches `<Users/>` and `<NoUser/>`.
|
||||
|
||||
When I use nested routes, in other words, each **path** can match multiple **routes**: each URL can render the views provided by multiple `<Route/>` components, at the same time, on the same page.
|
||||
|
||||
This may be counter-intuitive, but it’s very powerful, for reasons you’ll hopefully see in a few minutes.
|
||||
|
||||
## Why Nested Routing?
|
||||
|
||||
Why bother with this?
|
||||
|
||||
Most web applications contain levels of navigation that correspond to different parts of the layout. For example, in an email app you might have a URL like `/contacts/greg`, which shows a list of contacts on the left of the screen, and contact details for Greg on the right of the screen. The contact list and the contact details should always appear on the screen at the same time. If there’s no contact selected, maybe you want to show a little instructional text.
|
||||
|
||||
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="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
You can go even deeper. Say you want to have tabs for each contact’s address, email/phone, and your conversations with them. You can add _another_ set of nested routes inside `:id`:
|
||||
|
||||
```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>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The main page of the [Remix website](https://remix.run/), a React framework from the creators of React Router, has a great visual example if you scroll down, with three levels of nested routing: Sales > Invoices > an invoice.
|
||||
|
||||
## `<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.
|
||||
|
||||
Instead, you tell a parent component where to render any nested components with an `<Outlet/>` component. The `<Outlet/>` simply renders one of two things:
|
||||
|
||||
- if there is no nested route that has been matched, it shows nothing
|
||||
- if there is a nested route that has been matched, it shows its `view`
|
||||
|
||||
That’s all! But it’s important to know and to remember, because it’s a common source of “Why isn’t this working?” frustration. If you don’t provide an `<Outlet/>`, the nested route won’t be displayed.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
let contacts = todo!();
|
||||
|
||||
view! { cx,
|
||||
<div style="display: flex">
|
||||
// the contact list
|
||||
<For each=contacts
|
||||
key=|contact| contact.id
|
||||
view=|cx, contact| todo!()
|
||||
>
|
||||
// the nested child, if any
|
||||
// don’t forget this!
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Nested Routing and Performance
|
||||
|
||||
All of this is nice, conceptually, but again—what’s the big deal?
|
||||
|
||||
Performance.
|
||||
|
||||
In a fine-grained reactive library like Leptos, it’s always important to do the least amount of rendering work you can. Because we’re working with real DOM nodes and not diffing a virtual DOM, we want to “rerender” components as infrequently as possible. Nested routing makes this extremely easy.
|
||||
|
||||
Imagine my contact list example. If I navigate from Greg to Alice to Bob and back to Greg, the contact information needs to change on each navigation. But the `<ContactList/>` should never be rerendered. Not only does this save on rendering performance, it also maintains state in the UI. For example, if I have a search bar at the top of `<ContactList/>`, navigating from Greg to Alice to Bob won’t clear the search.
|
||||
|
||||
In fact, in this case, we don’t even need to rerender the `<Contact/>` component when moving between contacts. The router will just reactively update the `:id` parameter as we navigate, allowing us to make fine-grained updates. As we navigate between contacts, we’ll update single text nodes to change the contact’s name, address, and so on, without doing _any_ additional rerendering.
|
||||
|
||||
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple we’ll cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so don’t be surprised if there’s anything you don’t understand.
|
||||
|
||||
[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>
|
||||
79
docs/book/src/router/18_params_and_queries.md
Normal file
79
docs/book/src/router/18_params_and_queries.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Params and Queries
|
||||
|
||||
Static paths are useful for distinguishing between different pages, but almost every application wants to pass data through the URL at some point.
|
||||
|
||||
There are two ways you can do this:
|
||||
|
||||
1. named route **params** like `id` in `/users/:id`
|
||||
2. named route **queries** like `q` in `/search?q=Foo`
|
||||
|
||||
Because of the way URLs are built, you can access the query from _any_ `<Route/>` view. You can access route params from the `<Route/>` that defines them or any of its nested children.
|
||||
|
||||
Accessing params and queries is pretty simple with a couple of hooks:
|
||||
|
||||
- [`use_query`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query.html) or [`use_query_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
|
||||
- [`use_params`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_params.html) or [`use_params_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
|
||||
|
||||
Each of these comes with a typed option (`use_query` and `use_params`) and an untyped option (`use_query_map` and `use_params_map`).
|
||||
|
||||
The untyped versions hold a simple key-value map. To use the typed versions, derive the [`Params`](https://docs.rs/leptos_router/0.2.3/leptos_router/trait.Params.html) trait on a struct.
|
||||
|
||||
> `Params` is a very lightweight trait to convert a flat key-value map of strings into a struct by applying `FromStr` to each field. Because of the flat structure of route params and URL queries, it’s significantly less flexible than something like `serde`; it also adds much less weight to your binary.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[derive(Params)]
|
||||
struct ContactParams {
|
||||
id: usize
|
||||
}
|
||||
|
||||
#[derive(Params)]
|
||||
struct ContactSearch {
|
||||
q: String
|
||||
}
|
||||
```
|
||||
|
||||
> 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.
|
||||
|
||||
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);
|
||||
|
||||
// id: || -> usize
|
||||
let id = move || {
|
||||
params.with(|params| {
|
||||
params
|
||||
.map(|params| params.id)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
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);
|
||||
|
||||
// id: || -> Option<String>
|
||||
let id = move || {
|
||||
params.with(|params| params.get("id").cloned())
|
||||
};
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
> 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.
|
||||
|
||||
[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>
|
||||
23
docs/book/src/router/19_a.md
Normal file
23
docs/book/src/router/19_a.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# The `<A/>` Component
|
||||
|
||||
Client-side navigation works perfectly fine with ordinary HTML `<a>` elements. The router adds a listener that handles every click on a `<a>` element and tries to handle it on the client side, i.e., without doing another round trip to the server to request HTML. This is what enables the snappy “single-page app” navigations you’re probably familiar with from most modern web apps.
|
||||
|
||||
The router will bail out of handling an `<a>` click under a number of situations
|
||||
|
||||
- the click event has had `prevent_default()` called on it
|
||||
- the <kbd>Meta</kbd>, <kbd>Alt</kbd>, <kbd>Ctrl</kbd>, or <kbd>Shift</kbd> keys were held during click
|
||||
- the `<a>` has a `target` or `download` attribute, or `rel="external"`
|
||||
- the link has a different origin from the current location
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., it’s a link to the page you’re on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if it’s a link to the page you’re currently on, you can match this attribute with a CSS selector.
|
||||
|
||||
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
|
||||
|
||||
[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>
|
||||
67
docs/book/src/router/20_form.md
Normal file
67
docs/book/src/router/20_form.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# The `<Form/>` Component
|
||||
|
||||
Links and forms sometimes seem completely unrelated. But in fact, they work in very similar ways.
|
||||
|
||||
In plain HTML, there are three ways to navigate to another page:
|
||||
|
||||
1. An `<a>` element that links to another page. Navigates to the URL in its `href` attribute with the `GET` HTTP method.
|
||||
2. A `<form method="GET">`. Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
|
||||
3. A `<form method="POST">`. Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
|
||||
|
||||
Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way.
|
||||
|
||||
The router provides a [`<Form>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Form.html) component, which works like the HTML `<form>` element, but uses client-side navigations instead of full page reloads. `<Form/>` works with both `GET` and `POST` requests. With `method="GET"`, it will navigate to the URL encoded in the form data. With `method="POST"` it will make a `POST` request and handle the server’s response.
|
||||
|
||||
`<Form/>` provides the basis for some components like `<ActionForm/>` and `<MultiActionForm/>` that we’ll see in later chapters. But it also enables some powerful patterns of its own.
|
||||
|
||||
For example, imagine that you want to create a search field that updates search results in real time as the user searches, without a page reload, but that also stores the search in the URL so a user can copy and paste it to share results with someone else.
|
||||
|
||||
It turns out that the patterns we’ve learned so far make this easy to implement.
|
||||
|
||||
```rust
|
||||
async fn fetch_results() {
|
||||
// some async function to fetch our search results
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn FormExample(cx: Scope) -> impl IntoView {
|
||||
// reactive access to URL query strings
|
||||
let query = use_query_map(cx);
|
||||
// search stored as ?q=
|
||||
let search = move || query().get("q").cloned().unwrap_or_default();
|
||||
// a resource driven by the search string
|
||||
let search_results = create_resource(cx, search, fetch_results);
|
||||
|
||||
view! { cx,
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search/>
|
||||
<input type="submit"/>
|
||||
</Form>
|
||||
<Transition fallback=move || ()>
|
||||
/* render search results */
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Whenever you click `Submit`, the `<Form/>` will “navigate” to `?q={search}`. But because this navigation is done on the client side, there’s no page flicker or reload. The URL query string changes, which triggers `search` to update. Because `search` is the source signal for the `search_results` resource, this triggers `search_results` to reload its resource. The `<Transition/>` continues displaying the current search results until the new ones have loaded. When they are complete, it switches to displaying the new result.
|
||||
|
||||
This is a great pattern. The data flow is extremely clear: all data flows from the URL to the resource into the UI. The current state of the application is stored in the URL, which means you can refresh the page or text the link to a friend and it will show exactly what you’re expecting. And once we introduce server rendering, this pattern will prove to be really fault-tolerant, too: because it uses a `<form>` element and URLs under the hood, it actually works really well without even loading your WASM on the client.
|
||||
|
||||
We can actually take it a step further and do something kind of clever:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search
|
||||
oninput="this.form.requestSubmit()"
|
||||
/>
|
||||
</Form>
|
||||
}
|
||||
```
|
||||
|
||||
You’ll notice that this version drops the `Submit` button. Instead, we add an `oninput` attribute to the input. Note that this is _not_ `on:input`, which would listen for the `input` event and run some Rust code. Without the colon, `oninput` is the plain HTML attribute. So the string is actually a JavaScript string. `this.form` gives us the form the input is attached to. `requestSubmit()` fires the `submit` event on the `<form>`, which is caught by `<Form/>` just as if we had clicked a `Submit` button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the user’s input as they type.
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
23
docs/book/src/router/README.md
Normal file
23
docs/book/src/router/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Routing
|
||||
|
||||
## The Basics
|
||||
|
||||
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 _scheme_: `https`
|
||||
- a _domain_: `leptos.dev`
|
||||
- a **path**: `/blog/search`
|
||||
- a **query** (or **search**): `?q=Search`
|
||||
- a _hash_: `#results`
|
||||
|
||||
The Leptos Router works with the path and query (`/blog/search?q=Search`). Given this piece of the URL, what should the app render on the page?
|
||||
|
||||
## The Philosophy
|
||||
|
||||
In most cases, the path should drive what is displayed on the page. From the user’s perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
|
||||
|
||||
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
|
||||
|
||||
The router handles most of this work for you by mapping the current location to particular components.
|
||||
125
docs/book/src/server/25_server_functions.md
Normal file
125
docs/book/src/server/25_server_functions.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Server Functions
|
||||
|
||||
If you’re creating anything beyond a toy app, you’ll need to run code on the server all the time: reading from or writing to a database that only runs on the server, running expensive computations using libraries you don’t want to ship down to the client, accessing APIs that need to be called from the server rather than the client for CORS reasons or because you need a secret API key that’s stored on the server and definitely shouldn’t be shipped down to a user’s browser.
|
||||
|
||||
Traditionally, this is done by separating your server and client code, and by setting up something like a REST API or GraphQL API to allow your client to fetch and mutate data on the server. This is fine, but it requires you to write and maintain your code in multiple separate places (client-side code for fetching, server-side functions to run), as well as creating a third thing to manage, which is the API contract between the two.
|
||||
|
||||
Leptos is one of a number of modern frameworks that introduce the concept of **server functions**. Server functions have two key characteristics:
|
||||
|
||||
1. Server functions are **co-located** with your component code, so that you can organize your work by feature, not by technology. For example, you might have a “dark mode” feature that should persist a user’s dark/light mode preference across sessions, and be applied during server rendering so there’s no flicker. This requires a component that needs to be interactive on the client, and some work to be done on the server (setting a cookie, maybe even storing a user in a database.) Traditionally, this feature might end up being split between two different locations in your code, one in your “frontend” and one in your “backend.” With server functions, you’ll probably just write them both in one `dark_mode.rs` and forget about it.
|
||||
2. Server functions are **isomorphic**, i.e., they can be called either from the server or the browser. This is done by generating code differently for the two platforms. On the server, a server function simply runs. In the browser, the server function’s body is replaced with a stub that actually makes a fetch request to the server, serializing the arguments into the request and deserializing the return value from the response. But on either end, the function can simply be called: you can create an `add_todo` function that writes to your database, and simply call it from a click handler on a button in the browser!
|
||||
|
||||
## Using Server Functions
|
||||
|
||||
Actually, I kind of like that example. What would it look like? It’s pretty simple, actually.
|
||||
|
||||
```rust
|
||||
// todo.rs
|
||||
|
||||
#[server(AddTodo, "/api")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BusyButton(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<button on:click=move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
});
|
||||
}>
|
||||
"Add Todo"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
// somewhere in main.rs
|
||||
fn main() {
|
||||
// ...
|
||||
|
||||
AddTodo::register();
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You’ll notice a couple things here right away:
|
||||
|
||||
- Server functions can use server-only dependencies, like `sqlx`, and can access server-only resources, like our database.
|
||||
- Server functions are `async`. Even if they only did synchronous work on the server, the function signature would still need to be `async`, because calling them from the browser _must_ be asynchronous.
|
||||
- Server functions return `Result<T, ServerFnError>`. Again, even if they only do infallible work on the server, this is true, because `ServerFnError`’s variants include the various things that can be wrong during the process of making a network request.
|
||||
- Server functions can be called from the client. Take a look at our click handler. This is code that will _only ever_ run on the client. But it can call the function `add_todo` (using `spawn_local` to run the `Future`) as if it were an ordinary async function:
|
||||
|
||||
```rust
|
||||
move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- Server functions are top-level functions defined with `fn`. Unlike event listeners, derived signals, and most everything else in Leptos, they are not closures! As `fn` calls, they have no access to the reactive state of your app or anything else that is not passed in as an argument. And again, this makes perfect sense: When you make a request to the server, the server doesn’t have access to client state unless you send it explicitly. (Otherwise we’d have to serialize the whole reactive system and send it across the wire with every request, which—while it served classic ASP for a while—is a really bad idea.)
|
||||
- Server function arguments and return values both need to be serializable with `serde`. Again, hopefully this makes sense: while function arguments in general don’t need to be serialized, calling a server function from the browser means serializing the arguments and sending them over HTTP.
|
||||
|
||||
There are a few things to note about the way you define a server function, too.
|
||||
|
||||
- Server functions are created by using the [`#[server]` macro](https://docs.rs/leptos_server/latest/leptos_server/index.html#server) to annotate a top-level function, which can be defined anywhere.
|
||||
- We provide the macro a type name. The type name is used to register the server function (in `main.rs`), and it’s used internally as a container to hold, serialize, and deserialize the arguments.
|
||||
- We provide the macro a path. This is a prefix for the path at which we’ll mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
|
||||
- You’ll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
|
||||
|
||||
## Server Function Encodings
|
||||
|
||||
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which we’ll see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
|
||||
|
||||
```rust
|
||||
#[server(AddTodo, "/api", "Url")]
|
||||
#[server(AddTodo, "/api", "GetJson")]
|
||||
#[server(AddTodo, "/api", "Cbor")]
|
||||
#[server(AddTodo, "/api", "GetCbor")]
|
||||
```
|
||||
|
||||
The four options use different combinations of HTTP verbs and encoding methods:
|
||||
|
||||
| Name | Method | Request | Response |
|
||||
| ----------------- | ------ | ----------- | -------- |
|
||||
| **Url** (default) | POST | URL encoded | JSON |
|
||||
| **GetJson** | GET | URL encoded | JSON |
|
||||
| **Cbor** | POST | CBOR | CBOR |
|
||||
| **GetCbor** | GET | URL encoded | CBOR |
|
||||
|
||||
In other words, you have two choices:
|
||||
|
||||
- `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached, `GET` requests can be.
|
||||
- Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64 string)?
|
||||
|
||||
**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!
|
||||
|
||||
## 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.
|
||||
|
||||
## Integrating Server Functions with Leptos
|
||||
|
||||
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:
|
||||
|
||||
- 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.
|
||||
- Create **actions** that call the server function to mutate data on the server
|
||||
|
||||
The final section of this book will make this a little more concrete by introducing patterns that use progressively-enhanced HTML forms to run these server actions.
|
||||
|
||||
But in the next few chapters, we’ll actually take a look at some of the details of what you might want to do with your server functions, including the best ways to integrate with the powerful extractors provided by the Actix and Axum server frameworks.
|
||||
11
docs/book/src/server/README.md
Normal file
11
docs/book/src/server/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Working with the Server
|
||||
|
||||
The previous section described the process of server-side rendering, using the server to generate an HTML version of the page that will become interactive in the browser. So far, everything has been “isomorphic” or “universal”; in other words, your app has had the “same (_iso_) shape (_morphe_)” on the client and the server.
|
||||
|
||||
But a server can do a lot more than just render HTML! In fact, a server can do a whole bunch of things your browser _can’t,_ like reading from and writing to a SQL database.
|
||||
|
||||
If you’re used to building JavaScript frontend apps, you’re probably used to calling out to some kind of REST API to do this sort of server work. If you’re used to building sites with PHP or Python or Ruby (or Java or C# or...), this server-side work is your bread and butter, and it’s the client-side interactivity that tends to be an afterthought.
|
||||
|
||||
With Leptos, you can do both: not only in the same language, not only sharing the same types, but even in the same files!
|
||||
|
||||
This section will talk about how to build the uniquely-server-side parts of your application.
|
||||
37
docs/book/src/ssr/21_cargo_leptos.md
Normal file
37
docs/book/src/ssr/21_cargo_leptos.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Introducing `cargo-leptos`
|
||||
|
||||
So far, we’ve just been running code in the browser and using Trunk to coordinate the build process and run a local development process. If we’re going to add server-side rendering, we’ll need to run our application code on the server as well. This means we’ll need to build two separate binaries, one compiled to native code and running the server, the other compiled to WebAssembly (WASM) and running in the user’s browser. Additionally, the server needs to know how to serve this WASM version (and the JavaScript required to initialize it) to the browser.
|
||||
|
||||
This is not an insurmountable task but it adds some complication. For convenience and an easier developer experience, we built the [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool. `cargo-leptos` basically exists to coordinate the build process for your app, handling recompiling the server and client halves when you make changes, and adding some built-in support for things like Tailwind, SASS, and testing.
|
||||
|
||||
Getting started is pretty easy. Just run
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
```
|
||||
|
||||
And then to create a new project, you can run either
|
||||
|
||||
```bash
|
||||
# for an Actix template
|
||||
cargo leptos new --git leptos-rs/start
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
# for an Axum template
|
||||
cargo leptos new --git leptos-rs/start-axum
|
||||
```
|
||||
|
||||
Now `cd` into the directory you’ve created and run
|
||||
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Once your app has compiled you can open up your browser to [`http://localhost:3000`](http://localhost:3000) to see it.
|
||||
|
||||
`cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/cargo-leptos/blob/main/README.md).
|
||||
|
||||
But what exactly is happening when you open our browser to `localhost:3000`? Well, read on to find out.
|
||||
43
docs/book/src/ssr/22_life_cycle.md
Normal file
43
docs/book/src/ssr/22_life_cycle.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# The Life of a Page Load
|
||||
|
||||
Before we get into the weeds it might be helpful to have a higher-level overview. What exactly happens between the moment you type in the URL of a server-rendered Leptos app, and the moment you click a button and a counter increases?
|
||||
|
||||
I’m assuming some basic knowledge of how the Internet works here, and won’t get into the weeds about HTTP or whatever. Instead, I’ll try to show how different parts of the Leptos APIs map onto each part of the process.
|
||||
|
||||
This description also starts from the premise that your app is being compiled for two separate targets:
|
||||
|
||||
1. A server version, often running on Actix or Axum, compiled with the Leptos `ssr` feature
|
||||
2. A browser version, compiled to WebAssembly (WASM) with the Leptos `hydrate` feature
|
||||
|
||||
The [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool exists to coordinate the process of compiling your app for these two different targets.
|
||||
|
||||
## On the Server
|
||||
|
||||
- Your browser makes a `GET` request for that URL to your server. At this point, the browser knows almost nothing about the page that’s going to be rendered. (The question “How does the browser know where to ask for the page?” is an interesting one, but out of the scope of this tutorial!)
|
||||
- The server receives that request, and checks whether it has a way to handle a `GET` request at that path. This is what the `.leptos_routes()` methods in [`leptos_axum`](https://docs.rs/leptos_axum/0.2.5/leptos_axum/trait.LeptosRoutes.html) and [`leptos_actix`](https://docs.rs/leptos_actix/0.2.5/leptos_actix/trait.LeptosRoutes.html) are for. When the server starts up, these methods walk over the routing structure you provide in `<Routes/>`, generating a list of all possible routes your app can handle and telling the server’s router “for each of these routes, if you get a request... hand it off to Leptos.”
|
||||
- The server sees that this route can be handled by Leptos. So it renders your root component (often called something like `<App/>`), providing it with the URL that’s being requested and some other data like the HTTP headers and request metadata.
|
||||
- Your application runs once on the server, building up an HTML version of the component tree that will be rendered at that route. (There’s more to be said here about resources and `<Suspense/>` in the next chapter.)
|
||||
- The server returns this HTML page, also injecting information on how to load the version of your app that has been compiled to WASM so that it can run in the browser.
|
||||
|
||||
> The HTML page that’s returned is essentially your app, “dehydrated” or “freeze-dried”: it is HTML without any of the reactivity or event listeners you’ve added. The browser will “rehydrate” this HTML page by adding the reactive system and attaching event listeners to that server-rendered HTML. Hence the two feature flags that apply to the two halves of this process: `ssr` on the server for “server-side rendering”, and `hydrate` in the browser for that process of rehydration.
|
||||
|
||||
## In the Browser
|
||||
|
||||
- The browser receives this HTML page from the server. It immediately goes back to the server to begin loading the JS and WASM necessary to run the interactive, client side version of the app.
|
||||
- In the meantime, it renders the HTML version.
|
||||
- When the WASM version has reloaded, it does the same route-matching process that the server did. Because the `<Routes/>` component is identical on the server and in the client, the browser version will read the URL and render the same page that was already returned by the server.
|
||||
- During this initial “hydration” phase, the WASM version of your app doesn’t re-create the DOM nodes that make up your application. Instead, it walks over the existing HTML tree, “picking up” existing elements and adding the necessary interactivity.
|
||||
|
||||
> Note that there are some trade-offs here. Before this hydration process is complete, the page will _appear_ interactive but won’t actually respond to interactions. For example, if you have a counter button and click it before WASM has loaded, the count will not increment, because the necessary event listeners and reactivity have not been added yet. We’ll look at some ways to build in “graceful degradation” in future chapters.
|
||||
|
||||
## Client-Side Navigation
|
||||
|
||||
The next step is very important. Imagine that the user now clicks a link to navigate to another page in your application.
|
||||
|
||||
The browser will _not_ make another round trip to the server, reloading the full page as it would for navigating between plain HTML pages or an application that uses server rendering (for example with PHP) but without a client-side half.
|
||||
|
||||
Instead, the WASM version of your app will load the new page, right there in the browser, without requesting another page from the server. Essentially, your app upgrades itself from a server-loaded “multi-page app” into a browser-rendered “single-page app.” This yields the best of both worlds: a fast initial load time due to the server-rendered HTML, and fast secondary navigations because of the client-side routing.
|
||||
|
||||
Some of what will be described in the following chapters—like the interactions between server functions, resources, and `<Suspense/>`—may seem overly complicated. You might find yourself asking, “If my page is being rendered to HTML on the server, why can’t I just `.await` this on the server? If I can just call library X in a server function, why can’t I call it in my component?” The reason is pretty simple: to enable the upgrade from server rendering to client rendering, everything in your application must be able to run either on the server or in the browser.
|
||||
|
||||
This is not the only way to create a website or web framework, of course. But it’s the most common way, and we happen to think it’s quite a good way, to create the smoothest possible experience for your users.
|
||||
122
docs/book/src/ssr/23_ssr_modes.md
Normal file
122
docs/book/src/ssr/23_ssr_modes.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Async Rendering and SSR “Modes”
|
||||
|
||||
Server-rendering a page that uses only synchronous data is pretty simple: You just walk down the component tree, rendering each element to an HTML string. But this is a pretty big caveat: it doesn’t answer the question of what we should do with pages that includes asynchronous data, i.e., the sort of stuff that would be rendered under a `<Suspense/>` node on the client.
|
||||
|
||||
When a page loads async data that it needs to render, what should we do? Should we wait for all the async data to load, and then render everything at once? (Let’s call this “async” rendering) Should we go all the way in the opposite direction, just sending the HTML we have immediately down to the client and letting the client load the resources and fill them in? (Let’s call this “synchronous” rendering) Or is there some middle-ground solution that somehow beats them both? (Hint: There is.)
|
||||
|
||||
If you’ve ever listened to streaming music or watched a video online, I’m sure you realize that HTTP supports streaming, allowing a single connection to send chunks of data one after another without waiting for the full content to load. You may not realize that browsers are also really good at rendering partial HTML pages. Taken together, this means that you can actually enhance your users’ experience by **streaming HTML**: and this is something that Leptos supports out of the box, with no configuration at all. And there’s actually more than one way to stream HTML: you can stream the chunks of HTML that make up your page in order, like frames of a video, or you can stream them... well, out of order.
|
||||
|
||||
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.
|
||||
|
||||
## Synchronous Rendering
|
||||
|
||||
1. **Synchronous**: Serve an HTML shell that includes `fallback` for any `<Suspense/>`. Load data on the client using `create_local_resource`, replacing `fallback` once resources are loaded.
|
||||
|
||||
- _Pros_: App shell appears very quickly: great TTFB (time to first byte).
|
||||
- _Cons_
|
||||
- Resources load relatively slowly; you need to wait for JS + WASM to load before even making a request.
|
||||
- No ability to include data from async resources in the `<title>` or other `<meta>` tags, hurting SEO and things like social media link previews.
|
||||
|
||||
If you’re using server-side rendering, the synchronous mode is almost never what you actually want, from a performance perspective. This is because it misses out on an important optimization. If you’re loading async resources during server rendering, you can actually begin loading the data on the server. Rather than waiting for the client to receive the HTML response, then loading its JS + WASM, _then_ realize it needs the resources and begin loading them, server rendering can actually begin loading the resources when the client first makes the response. In this sense, during server rendering an async resource is like a `Future` that begins loading on the server and resolves on the client. As long as the resources are actually serializable, this will always lead to a faster total load time.
|
||||
|
||||
> This is why [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) requires resources data to be serializable by default, and why you need to explicitly use [`create_local_resource`](https://docs.rs/leptos/latest/leptos/fn.create_local_resource.html) for any async data that is not serializable and should therefore only be loaded in the browser itself. Creating a local resource when you could create a serializable resource is always a deoptimization.
|
||||
|
||||
## Async Rendering
|
||||
|
||||
<video controls>
|
||||
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/async.mov?raw=true" type="video/mp4">
|
||||
</video>
|
||||
|
||||
2. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
|
||||
|
||||
- _Pros_: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
|
||||
- _Cons_: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client. The page is totally blank until everything is loaded.
|
||||
|
||||
## In-Order Streaming
|
||||
|
||||
<video controls>
|
||||
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/in-order.mov?raw=true" type="video/mp4">
|
||||
</video>
|
||||
|
||||
3. **In-order streaming**: Walk through the component tree, rendering HTML until you hit a `<Suspense/>`. Send down all the HTML you’ve got so far as a chunk in the stream, wait for all the resources accessed under the `<Suspense/>` to load, then render it to HTML and keep walking until you hit another `<Suspense/>` or the end of the page.
|
||||
|
||||
- _Pros_: Rather than a blank screen, shows at least _something_ before the data are ready.
|
||||
- _Cons_
|
||||
- Loads the shell more slowly than synchronous rendering (or out-of-order streaming) because it needs to pause at every `<Suspense/>`.
|
||||
- Unable to show fallback states for `<Suspense/>`.
|
||||
- Can’t begin hydration until the entire page has loaded, so earlier pieces of the page will not be interactive until the suspended chunks have loaded.
|
||||
|
||||
## Out-of-Order Streaming
|
||||
|
||||
<video controls>
|
||||
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/out-of-order.mov?raw=true" type="video/mp4">
|
||||
</video>
|
||||
|
||||
4. **Out-of-order streaming**: Like synchronous rendering, serve an HTML shell that includes `fallback` for any `<Suspense/>`. But load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `<Suspense/>` nodes, which is swapped in to replace the fallback.
|
||||
|
||||
- _Pros_: Combines the best of **synchronous** and **`async`**.
|
||||
- Fast initial response/TTFB because it immediately sends the whole synchronous shell
|
||||
- Fast total time because resources begin loading on the server.
|
||||
- Able to show the fallback loading state and dynamically replace it, instead of showing blank sections for un-loaded data.
|
||||
- _Cons_: Requires JavaScript to be enabled for suspended fragments to appear in correct order. (This small chunk of JS streamed down in a `<script>` tag alongside the `<template>` tag that contains the rendered `<Suspense/>` fragment, so it does not need to load any additional JS files.)
|
||||
|
||||
## Using SSR Modes
|
||||
|
||||
Because it offers the best blend of performance characteristics, Leptos defaults to out-of-order streaming. But it’s really simple to opt into these different modes. You do it by adding an `ssr` property onto one or more of your `<Route/>` components, like in the [`ssr_modes` example](https://github.com/leptos-rs/leptos/blob/main/examples/ssr_modes/src/app.rs).
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=|cx| view! { cx, <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/> }
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
For a path that includes multiple nested routes, the most restrictive mode will be used: i.e., if even a single nested route asks for `async` rendering, the whole initial request will be rendered `async`. `async` is the most restricted requirement, followed by in-order, and then out-of-order. (This probably makes sense if you think about it for a few minutes.)
|
||||
|
||||
## Blocking Resources
|
||||
|
||||
Any Leptos versions later than `0.2.5` (i.e., git main and `0.3.x` or later) introduce a new resource primitive with `create_blocking_resource`. A blocking resource still loads asynchronously like any other `async`/`.await` in Rust; it doesn’t block a server thread or anything. Instead, reading from a blocking resource under a `<Suspense/>` blocks the HTML _stream_ from returning anything, including its initial synchronous shell, until that `<Suspense/>` has resolved.
|
||||
|
||||
Now from a performance perspective, this is not ideal. None of the synchronous shell for your page will load until that resource is ready. However, rendering nothing means that you can do things like set the `<title>` or `<meta>` tags in your `<head>` in actual HTML. This sounds a lot like `async` rendering, but there’s one big difference: if you have multiple `<Suspense/>` sections, you can block on _one_ of them but still render a placeholder and then stream in the other.
|
||||
|
||||
For example, think about a blog post. For SEO and for social sharing, I definitely want my blog post’s title and metadata in the initial HTML `<head>`. But I really don’t care whether comments have loaded yet or not; I’d like to load those as lazily as possible.
|
||||
|
||||
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,
|
||||
<Suspense fallback=|| ()>
|
||||
{move || {
|
||||
post_data.with(cx, |data| {
|
||||
view! { cx,
|
||||
<Title text=data.title/>
|
||||
<Meta name="description" content=data.excerpt/>
|
||||
<article>
|
||||
/* render the post content */
|
||||
</article>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
<Suspense fallback=|| "Loading comments...">
|
||||
/* render comment data here */
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. The second `<Suspense/>`, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience.
|
||||
148
docs/book/src/ssr/24_hydration_bugs.md
Normal file
148
docs/book/src/ssr/24_hydration_bugs.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Hydration Bugs _(and how to avoid them)_
|
||||
|
||||
## A Thought Experiment
|
||||
|
||||
Let’s try an experiment to test your intuitions. Open up an app you’re server-rendering with `cargo-leptos`. (If you’ve just been using `trunk` so far to play with examples, go [clone a `cargo-leptos` template](./21_cargo_leptos.md) just for the sake of this exercise.)
|
||||
|
||||
Put a log somewhere in your root component. (I usually call mine `<App/>`, but anything will do.)
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
leptos::log!("where do I run?");
|
||||
// ... whatever
|
||||
}
|
||||
```
|
||||
|
||||
And let’s fire it up
|
||||
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Where do you expect `where do I run?` to log?
|
||||
|
||||
- In the command line where you’re running the server?
|
||||
- In the browser console when you load the page?
|
||||
- Neither?
|
||||
- Both?
|
||||
|
||||
Try it out.
|
||||
|
||||
...
|
||||
|
||||
...
|
||||
|
||||
...
|
||||
|
||||
Okay, consider the spoiler alerted.
|
||||
|
||||
You’ll notice of course that it logs in both places, assuming everything goes according to plan. In fact on the server it logs twice—first during the initial server startup, when Leptos renders your app once to extract the route tree, then a second time when you make a request. Each time you reload the page, `where do I run?` should log once on the server and once on the client.
|
||||
|
||||
If you think about the description in the last couple sections, hopefully this makes sense. Your application runs once on the server, where it builds up a tree of HTML which is sent to the client. During this initial render, `where do I run?` logs on the server.
|
||||
|
||||
Once the WASM binary has loaded in the browser, your application runs a second time, walking over the same user interface tree and adding interactivity.
|
||||
|
||||
> Does that sound like a waste? It is, in a sense. But reducing that waste is a genuinely hard problem. It’s what some JS frameworks like Qwik are intended to solve, although it’s probably too early to tell whether it’s a net performance gain as opposed to other approaches.
|
||||
|
||||
## The Potential for Bugs
|
||||
|
||||
Okay, hopefully all of that made sense. But what does it have to do with the title of this chapter, which is “Hydration bugs (and how to avoid them)”?
|
||||
|
||||
Remember that the application needs to run on both the server and the client. This generates a few different sets of potential issues you need to know how to avoid.
|
||||
|
||||
### Mismatches between server and client code
|
||||
|
||||
One way to create a bug is by creating a mismatch between the HTML that’s sent down by the server and what’s rendered on the client. It’s actually fairly hard to do this unintentionally, I think (at least judging by the bug reports I get from people.) But imagine I do something like this
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> 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)
|
||||
}
|
||||
```
|
||||
|
||||
In other words, if this is being compiled to WASM, it has three items; otherwise it’s empty.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
The WASM version of your app, running in the browser, expects to find three items; but the HTML has none.
|
||||
|
||||
#### Solution
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
You’ll probably instantly see the dreaded message
|
||||
|
||||
```
|
||||
panicked at 'cannot call wasm-bindgen imported functions on non-wasm targets'
|
||||
```
|
||||
|
||||
Uh-oh.
|
||||
|
||||
But of course this makes sense. We’ve just said that your app needs to run on the client and the server.
|
||||
|
||||
#### Solution
|
||||
|
||||
There are a few ways to avoid this:
|
||||
|
||||
1. Only use libraries that can run on both the server and the client. `reqwest`, for example, works for making HTTP requests in both settings.
|
||||
2. Use different libraries on the server and the client, and gate them using the `#[cfg]` macro. ([Click here for an example](https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/api.rs).)
|
||||
3. Wrap client-only code in `create_effect`. Because `create_effect` only runs on the client, this can be an effective way to access browser APIs that are not needed for initial rendering.
|
||||
|
||||
For example, say that I want to store something in the browser’s `localStorage` whenever a signal changes.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
}
|
||||
```
|
||||
|
||||
This panics because I can’t access `LocalStorage` during server rendering.
|
||||
|
||||
But if I wrap it in an effect...
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
create_effect(cx, move |_| {
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
It’s fine! This will render appropriately on the server, ignoring the client-only code, and then access the storage and log a message on the browser.
|
||||
|
||||
### Not all server code can run on the client
|
||||
|
||||
WebAssembly running in the browser is a pretty limited environment. You don’t have access to a file-system or to many of the other things the standard library may be used to having. Not every crate can even be compiled to WASM, let alone run in a WASM environment.
|
||||
|
||||
In particular, you’ll sometimes see errors about the crate `mio` or missing things from `core`. This is generally a sign that you are trying to compile something to WASM that can’t be compiled to WASM. If you’re adding server-only dependencies, you’ll want to mark them `optional = true` in your `Cargo.toml` and then enable them in the `ssr` feature definition. (Check out one of the template `Cargo.toml` files to see more details.)
|
||||
|
||||
You can use `create_effect` to specify that something should only run on the client, and not in the server. Is there a way to specify that something should run only on the server, and not the client?
|
||||
|
||||
In fact, there is. The next chapter will cover the topic of server functions in some detail. (In the meantime, you can check out their docs [here](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html).)
|
||||
21
docs/book/src/ssr/README.md
Normal file
21
docs/book/src/ssr/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Server Side Rendering
|
||||
|
||||
So far, everything we’ve written has been rendered almost entirely in the browser. When we create an app using Trunk, it’s served using a local development server. If you build it for production and deploy it, it’s served by whatever server or CDN you’re using. In either case, what’s served is an HTML page with
|
||||
|
||||
1. the URL of your Leptos app, which has been compiled to WebAssembly (WASM)
|
||||
2. the URL of the JavaScript used to initialized this WASM blob
|
||||
3. an empty `<body>` element
|
||||
|
||||
When the JS and WASM have loaded, Leptos will render your app into the `<body>`. This means that nothing appears on the screen until JS/WASM have loaded and run. This has some drawbacks:
|
||||
|
||||
1. It increases load time, as your user’s screen is blank until additional resources have been downloaded.
|
||||
2. It’s bad for SEO, as load times are longer and the HTML you serve has no meaningful content.
|
||||
3. It’s broken for users for whom JS/WASM don’t load for some reason (e.g., they’re on a train and just went into a tunnel before WASM finished loading; they’re using an older device that doesn’t support WASM; they have JavaScript or WASM turned off for some reason; etc.)
|
||||
|
||||
These downsides apply across the web ecosystem, but especially to WASM apps.
|
||||
|
||||
So what do you do if you want to return more than just an empty `<body>` tag? Use “server-side rendering.”
|
||||
|
||||
Whole books could be (and probably have been) written about this topic, but at its core, it’s really simple: rather than returning an empty `<body>` tag, return an initial HTML page that reflects the actual starting state of your app or site, so that while JS/WASM are loading, and until they load, the user can access the plain HTML version.
|
||||
|
||||
The rest of this section will cover this topic in some detail!
|
||||
@@ -15,11 +15,11 @@ 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 { /* ... */ }]);
|
||||
// ⚠️ this is hard to test because it's embedded in the component
|
||||
let maximum = move || todos.with(|todos| {
|
||||
todos.iter().filter(|todo| todo.completed).sum()
|
||||
});
|
||||
let (todos, set_todos) = create_signal(cx, 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()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -29,24 +29,24 @@ You could pull that logic out into a separate data structure and test it:
|
||||
pub struct Todos(Vec<Todo>);
|
||||
|
||||
impl Todos {
|
||||
pub fn remaining(&self) -> usize {
|
||||
todos.iter().filter(|todo| todo.completed).sum()
|
||||
}
|
||||
pub fn num_remaining(&self) -> usize {
|
||||
todos.iter().filter(|todo| !todo.completed).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
// ...
|
||||
}
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
|
||||
// ✅ this has a test associated with it
|
||||
let maximum = move || todos.with(Todos::remaining);
|
||||
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
|
||||
// ✅ this has a test associated with it
|
||||
let num_remaining = move || todos.with(Todos::num_remaining);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -107,27 +107,28 @@ fn clear() {
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
We’ll use some manual DOM operations to grab the `<div>` that wraps
|
||||
the whole component, as well as the `clear` button.
|
||||
|
||||
```rust
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
// this would be easier if they had IDs
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = test_wrapper
|
||||
.query_selector("button")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlElement>();
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
// this would be easier if they had IDs
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = test_wrapper
|
||||
.query_selector("button")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlElement>();
|
||||
```
|
||||
|
||||
Now we can use ordinary DOM APIs to simulate user interaction.
|
||||
|
||||
```rust
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
```
|
||||
|
||||
You can test individual DOM element attributes or text node values. Sometimes
|
||||
@@ -135,27 +136,27 @@ I like to test the whole view at once. We can do this by testing the element’s
|
||||
`outerHTML` against our expectations.
|
||||
|
||||
```rust
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
That test involved us manually replicating the `view` that’s inside the component.
|
||||
@@ -164,15 +165,14 @@ with the initial value `0`. This is where our wrapping element comes in: I’ll
|
||||
the wrapper’s `innerHTML` against another comparison case.
|
||||
|
||||
```rust
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
}
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
```
|
||||
|
||||
This is only a very limited introduction to testing. But I hope it’s useful as you begin to build applications.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# A Basic Component
|
||||
|
||||
That “Hello, world!” was a *very* simple example. Let’s move on to something a
|
||||
That “Hello, world!” was a _very_ simple example. Let’s move on to something a
|
||||
little more like an ordinary app.
|
||||
|
||||
First, let’s edit the `main` function so that, instead of rendering the whole
|
||||
app, it just renders an `<App/>` component. Components are the basic unit of
|
||||
composition and design in most web frameworks, and Leptos is no exception.
|
||||
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
`PascalCase`, so most Leptos applications will start with something like an
|
||||
composition and design in most web frameworks, and Leptos is no exception.
|
||||
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
`PascalCase`, so most Leptos applications will start with something like an
|
||||
`<App/>` component.
|
||||
|
||||
```rust
|
||||
@@ -31,7 +31,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
"Click me"
|
||||
"Click me: "
|
||||
{move || count.get()}
|
||||
</button>
|
||||
}
|
||||
@@ -39,11 +39,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
```
|
||||
|
||||
## The Component Signature
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
```
|
||||
|
||||
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||
used as a component in your Leptos application. We’ll see some of the other features of
|
||||
this macro in a couple chapters.
|
||||
|
||||
@@ -52,6 +53,7 @@ fn App(cx: Scope) -> impl IntoView
|
||||
```
|
||||
|
||||
Every component is a function with the following characteristics
|
||||
|
||||
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||
as its first argument. This `Scope` is our entrypoint into the reactive system.
|
||||
By convention, it’s usually named `cx`.
|
||||
@@ -60,24 +62,26 @@ Every component is a function with the following characteristics
|
||||
anything you could return from a Leptos `view`.
|
||||
|
||||
## The Component Body
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
render function that re-runs multiple times. You’ll typically use it to create a
|
||||
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
render function that reruns multiple times. You’ll typically use it to create a
|
||||
few reactive variables, define any side effects that run in response to those values
|
||||
changing, and describe the user interface.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
```
|
||||
|
||||
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
|
||||
creates a signal, the basic unit of reactive change and state management in Leptos.
|
||||
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||
current value, you’ll call `set_count.set(...)` (or `set_count(...)`).
|
||||
|
||||
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||
efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||
> efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||
|
||||
## The View
|
||||
## The View
|
||||
|
||||
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
|
||||
|
||||
@@ -86,7 +90,8 @@ view! { cx,
|
||||
<button
|
||||
// define an event listener with on:
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
// on stable, this is set_count.set(3);
|
||||
set_count(3);
|
||||
}
|
||||
>
|
||||
// text nodes are wrapped in quotation marks
|
||||
@@ -100,25 +105,28 @@ view! { cx,
|
||||
This should mostly be easy to understand: it looks like HTML, with a special
|
||||
`on:click` to define a `click` event listener, a text node that’s formatted like
|
||||
a Rust string, and then...
|
||||
|
||||
```rust
|
||||
{move || count.get()}
|
||||
```
|
||||
|
||||
whatever that is.
|
||||
|
||||
People sometimes joke that they use more closures in their first Leptos application
|
||||
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||
People sometimes joke that they use more closures in their first Leptos application
|
||||
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||
into the view tells the framework: “Hey, this is something that might change.”
|
||||
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, re-runs,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
nothing else in your application. This is what allows for extremely efficient updates
|
||||
to the DOM.
|
||||
|
||||
Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice
|
||||
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||
Leptos with `nightly` Rust, signals are already functions, so the closure is unnecessary.
|
||||
As a result, you can write a simpler view:
|
||||
As a result, you can write a simpler view:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<button /* ... */>
|
||||
@@ -129,15 +137,27 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
Remember—and this is *very important*—only functions are reactive. This means that
|
||||
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||
Remember—and this is _very important_—only functions are reactive. This means that
|
||||
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||
in a function, telling the framework to update the view every time `count` changes.
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
|
||||
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||
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”:
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
||||
```rust
|
||||
move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
```
|
||||
|
||||
You can see here that while `set_count` just sets the value, `set_count.update()` gives us a mutable reference and mutates the value in place. Either one will trigger a reactive update in our UI.
|
||||
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
> show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
> and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# `view`: Dynamic Attributes and Classes
|
||||
# `view`: Dynamic Classes, Styles and Attributes
|
||||
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
create dynamic text by passing a function (such as a signal) into the view.
|
||||
|
||||
But of course there are other things you might want to update in your user interface.
|
||||
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||
In this section, we’ll look at how to update classes, styles and attributes dynamically,
|
||||
and we’ll introduce the concept of a **derived signal**.
|
||||
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
increment a counter.
|
||||
|
||||
```rust
|
||||
@@ -20,6 +20,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
"Click me: "
|
||||
{move || count()}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So far, this is just the example from the last chapter.
|
||||
@@ -28,27 +34,67 @@ So far, this is just the example from the last chapter.
|
||||
|
||||
Now let’s say I’d like to update the list of CSS classes on this element dynamically.
|
||||
For example, let’s say I want to add the class `red` when the count is odd. I can
|
||||
do this using the `class:` syntax.
|
||||
do this using the `class:` syntax.
|
||||
|
||||
```rust
|
||||
class:red=move || count() & 1 == 1
|
||||
class:red=move || count() % 2 == 1
|
||||
```
|
||||
|
||||
`class:` attributes take
|
||||
|
||||
1. the class name, following the colon (`red`)
|
||||
2. a value, which can be a `bool` or a function that returns a `bool`
|
||||
|
||||
When the value is `true`, the class is added. When the value is `false`, the class
|
||||
is removed. And if the value is a function that accesses a signal, the class will
|
||||
is removed. And if the value is a function that accesses a signal, the class will
|
||||
reactively update when the signal changes.
|
||||
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
the number switches between even and odd.
|
||||
|
||||
Some CSS class names can’t be directly parsed by the `view` macro, especially if they include a mix of dashes and numbers or other characters. In that case, you can use a tuple syntax: `class=("name", value)` still directly updates a single class.
|
||||
|
||||
```rust
|
||||
class=("button-20", move || count() % 2 == 1)
|
||||
```
|
||||
|
||||
> If you’re following along, make sure you go into your `index.html` and add something like this:
|
||||
>
|
||||
> ```html
|
||||
> <style>
|
||||
> .red {
|
||||
> color: red;
|
||||
> }
|
||||
> </style>
|
||||
> ```
|
||||
|
||||
## Dynamic Styles
|
||||
|
||||
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,
|
||||
<div
|
||||
style="position: absolute"
|
||||
style:left=move || format!("{}px", x() + 100)
|
||||
style:top=move || format!("{}px", y() + 100)
|
||||
style:background-color=move || format!("rgb({}, {}, 100)", x(), y())
|
||||
style=("--columns", x)
|
||||
>
|
||||
"Moves when coordinates change"
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Attributes
|
||||
|
||||
The same applies to plain attributes. Passing a plain string or primitive value to
|
||||
an attribute gives it a static value. Passing a function (including a signal) to
|
||||
an attribute causes it to update its value reactively. Let’s add another element
|
||||
an attribute causes it to update its value reactively. Let’s add another element
|
||||
to our view:
|
||||
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
@@ -57,17 +103,18 @@ to our view:
|
||||
/>
|
||||
```
|
||||
|
||||
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||
our progress bar will move forward.
|
||||
|
||||
## Derived Signals
|
||||
## Derived Signals
|
||||
|
||||
Let’s go one layer deeper, just for fun.
|
||||
|
||||
You already know that we create reactive interfaces just by passing functions into
|
||||
You already know that we create reactive interfaces just by passing functions into
|
||||
the `view`. This means that we can easily change our progress bar. For example,
|
||||
suppose we want it to move twice as fast:
|
||||
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
@@ -77,28 +124,31 @@ suppose we want it to move twice as fast:
|
||||
|
||||
But imagine we want to reuse that calculation in more than one place. You can do this
|
||||
using a **derived signal**: a closure that accesses a signal.
|
||||
|
||||
```rust
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
/* insert the rest of the view */
|
||||
/* insert the rest of the view */
|
||||
<progress
|
||||
max="50"
|
||||
// we use it once here
|
||||
value=double_count
|
||||
/>
|
||||
<p>
|
||||
"Double Count: "
|
||||
"Double Count: "
|
||||
// and again here
|
||||
{double_count}
|
||||
</p>
|
||||
```
|
||||
|
||||
Derived signals let you create reactive computed values that can be used in multiple
|
||||
Derived signals let you create reactive computed values that can be used in multiple
|
||||
places in your application with minimal overhead.
|
||||
|
||||
> Note: Using a derived signal like this means that the calculation runs once per
|
||||
signal change per place we access `double_count`; in other words, twice. This is a
|
||||
very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
are designed to solve this problem for expensive calculations.
|
||||
> Note: Using a derived signal like this means that the calculation runs once per
|
||||
> signal change per place we access `double_count`; in other words, twice. This is a
|
||||
> very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
> are designed to solve this problem for expensive calculations.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Components and Props
|
||||
|
||||
So far, we’ve been building our whole application in a single component. This
|
||||
is fine for really tiny examples, but in any real application you’ll need to
|
||||
break the user interface out into multiple components, so you can break your
|
||||
So far, we’ve been building our whole application in a single component. This
|
||||
is fine for really tiny examples, but in any real application you’ll need to
|
||||
break the user interface out into multiple components, so you can break your
|
||||
interface down into smaller, reusable, composable chunks.
|
||||
|
||||
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||
instead of one: one that advances one tick per click, one that advances two ticks
|
||||
per click.
|
||||
|
||||
@@ -15,19 +15,20 @@ You _could_ do this by just creating two `<progress>` elements:
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! {
|
||||
view! { cx,
|
||||
<progress
|
||||
max="50"
|
||||
value=progress
|
||||
value=count
|
||||
/>
|
||||
<progress
|
||||
max="50"
|
||||
value=double_count
|
||||
/>
|
||||
}
|
||||
```
|
||||
|
||||
But of course, this doesn’t scale very well. If you want to add a third progress
|
||||
bar, you need to add this code another time. And if you want to edit anything
|
||||
bar, you need to add this code another time. And if you want to edit anything
|
||||
about it, you need to edit it in triplicate.
|
||||
|
||||
Instead, let’s create a `<ProgressBar/>` component.
|
||||
@@ -47,15 +48,15 @@ fn ProgressBar(
|
||||
}
|
||||
```
|
||||
|
||||
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||
When we were defining everything manually, we just used the local variable names.
|
||||
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||
When we were defining everything manually, we just used the local variable names.
|
||||
Now we need some way to pass an argument into the component.
|
||||
|
||||
## Component Props
|
||||
## Component Props
|
||||
|
||||
We do this using component properties, or “props.” If you’ve used another frontend
|
||||
framework, this is probably a familiar idea. Basically, properties are to components
|
||||
as attributes are to HTML elements: they let you pass additional information into
|
||||
framework, this is probably a familiar idea. Basically, properties are to components
|
||||
as attributes are to HTML elements: they let you pass additional information into
|
||||
the component.
|
||||
|
||||
In Leptos, you define props by giving additional arguments to the component function.
|
||||
@@ -69,7 +70,7 @@ fn ProgressBar(
|
||||
view! { cx,
|
||||
<progress
|
||||
max="50"
|
||||
// now this works
|
||||
// now this works
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
@@ -92,41 +93,45 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||
notice that you can easily tell the difference between an element and a component
|
||||
because components always have `PascalCase` names. You pass the `progress` prop
|
||||
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||
notice that you can easily tell the difference between an element and a component
|
||||
because components always have `PascalCase` names. You pass the `progress` prop
|
||||
in as if it were an HTML element attribute. Simple.
|
||||
|
||||
> ### Important Note
|
||||
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||
is what allows us to have named props, when Rust does not have named function parameters.
|
||||
If you’re defining a component in one module and importing it into another, make
|
||||
sure you include this `ComponentProps` type:
|
||||
> ### Important Note
|
||||
>
|
||||
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||
> is what allows us to have named props, when Rust does not have named function parameters.
|
||||
> If you’re defining a component in one module and importing it into another, make
|
||||
> sure you include this `ComponentProps` type:
|
||||
>
|
||||
> `use progress_bar::{ProgressBar, ProgressBarProps};`
|
||||
>
|
||||
> **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
|
||||
You’ll notice that throughout this example, `progress` takes a reactive
|
||||
`ReadSignal<i32>`, and not a plain `i32`. This is **very important**.
|
||||
|
||||
Component props have no special meaning attached to them. A component is simply
|
||||
a function that runs once to set up the user interface. The only way to tell the
|
||||
interface to respond to changing is to pass it a signal type. So if you have a
|
||||
component property that will change over time, like our `progress`, it should
|
||||
Component props have no special meaning attached to them. A component is simply
|
||||
a function that runs once to set up the user interface. The only way to tell the
|
||||
interface to respond to changing is to pass it a signal type. So if you have a
|
||||
component property that will change over time, like our `progress`, it should
|
||||
be a signal.
|
||||
|
||||
### `optional` Props
|
||||
### `optional` Props
|
||||
|
||||
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||
argument to the component function with `#[prop(optional)]`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
// mark this prop optional
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
max: u16,
|
||||
@@ -142,7 +147,7 @@ fn ProgressBar(
|
||||
```
|
||||
|
||||
Now, we can use `<ProgressBar max=50 value=count/>`, or we can omit `max`
|
||||
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||
on an `optional` is its `Default::default()` value, which for a `u16` is going to
|
||||
be `0`. In the case of a progress bar, a max value of `0` is not very useful.
|
||||
|
||||
@@ -187,20 +192,20 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
"Click me"
|
||||
</button>
|
||||
<ProgressBar progress=count/>
|
||||
// add a second progress bar
|
||||
// add a second progress bar
|
||||
<ProgressBar progress=double_count/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hm... this won’t compile. It should be pretty easy to understand why: we’ve declared
|
||||
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||
it’s a closure that returns an `i32`.
|
||||
|
||||
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||
could just take any function?” If you’re savvy, you may know that both these
|
||||
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||
could just take any function?” If you’re savvy, you may know that both these
|
||||
implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
|
||||
```rust
|
||||
@@ -210,9 +215,9 @@ fn ProgressBar<F>(
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> i32
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> i32 + 'static,
|
||||
{
|
||||
view! { cx,
|
||||
<progress
|
||||
@@ -223,27 +228,26 @@ where
|
||||
}
|
||||
```
|
||||
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
any value that implements this `Fn()` trait.
|
||||
|
||||
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
|
||||
or as `progress: impl Fn() -> i32`, in part because they’re actually used to generate
|
||||
a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
> or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
> a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
|
||||
### `into` Props
|
||||
### `into` Props
|
||||
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
This attribute automatically calls `.into()` on the values you pass as proprs,
|
||||
which allows you to pass props of different values easily.
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
This attribute automatically calls `.into()` on the values you pass as props,
|
||||
which allows you to easily pass props with different values.
|
||||
|
||||
In this case, it’s helpful to know about the
|
||||
In this case, it’s helpful to know about the
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
|
||||
is a enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
is an enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
reactive value.
|
||||
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
@@ -252,7 +256,7 @@ fn ProgressBar(
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
progress: Signal<i32>
|
||||
) -> impl IntoView
|
||||
) -> impl IntoView
|
||||
{
|
||||
view! { cx,
|
||||
<progress
|
||||
@@ -281,12 +285,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
## Documenting Components
|
||||
|
||||
This is one of the least essential but most important sections of this book.
|
||||
It’s not strictly necessary to document your components and their props. It may
|
||||
be very important, depending on the size of your team and your app. But it’s very
|
||||
This is one of the least essential but most important sections of this book.
|
||||
It’s not strictly necessary to document your components and their props. It may
|
||||
be very important, depending on the size of your team and your app. But it’s very
|
||||
easy, and bears immediate fruit.
|
||||
|
||||
To document a component and its props, you can simply add doc comments on the
|
||||
To document a component and its props, you can simply add doc comments on the
|
||||
component function, and each one of the props:
|
||||
|
||||
```rust
|
||||
@@ -309,9 +313,11 @@ That’s all you need to do. These behave like ordinary Rust doc comments, excep
|
||||
that you can document individual component props, which can’t be done with Rust
|
||||
function arguments.
|
||||
|
||||
This will automatically generate documentation for your component, its `Props`
|
||||
type, and each of the fields used to add props. It can be a little hard to
|
||||
understand how powerful this is until you hover over the component name or props
|
||||
This will automatically generate documentation for your component, its `Props`
|
||||
type, and each of the fields used to add props. It can be a little hard to
|
||||
understand how powerful this is until you hover over the component name or props
|
||||
and see the power of the `#[component]` macro combined with rust-analyzer here.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
# Iteration
|
||||
|
||||
Whether you’re listing todos, displaying a table, or showing product images,
|
||||
Whether you’re listing todos, displaying a table, or showing product images,
|
||||
iterating over a list of items is a common task in web applications. Reconciling
|
||||
the differences between changing sets of items can also be one of the trickiest
|
||||
tasks for a framework to handle well.
|
||||
|
||||
Leptos supports to two different patterns for iterating over items:
|
||||
|
||||
1. For static views: `Vec<_>`
|
||||
2. For dynamic lists: `<For/>`
|
||||
|
||||
## Static Views with `Vec<_>`
|
||||
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
any `Vec<IV> where IV: IntoView` into your view. In other views, if you can render
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
any `Vec<IV> where IV: IntoView` into your view. In other words, if you can render
|
||||
`T`, you can render `Vec<T>`.
|
||||
|
||||
```rust
|
||||
@@ -30,6 +31,22 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
|
||||
|
||||
```rust
|
||||
let values = vec![0, 1, 2];
|
||||
view! { cx,
|
||||
// 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)}
|
||||
</ul>
|
||||
}
|
||||
```
|
||||
|
||||
The fact that the _list_ is static doesn’t mean the interface needs to be static.
|
||||
You can render dynamic items as part of a static list.
|
||||
|
||||
@@ -51,38 +68,41 @@ let counter_buttons = counters
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect_view(cx);
|
||||
|
||||
view! { cx,
|
||||
<ul>{counter_buttons}</ul>
|
||||
}
|
||||
```
|
||||
|
||||
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||
it changes, this will rerender every item in the list. This is quite inefficient!
|
||||
Fortunately, there’s a better way.
|
||||
|
||||
## Dynamic Rendering with the `<For/>` Component
|
||||
|
||||
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||
keyed dynamic list. It takes three props:
|
||||
|
||||
- `each`: a function (such as a signal) that returns the items `T` to be iterated over
|
||||
- `key`: a key function that takes `&T` and returns a stable, unique key or ID
|
||||
- `view`: renders each `T` into a view
|
||||
- `view`: renders each `T` into a view
|
||||
|
||||
`key` is, well, the key. You can add, remove, and move items within the list. As
|
||||
long as each item’s key is stable over time, the framework does not need to rerender
|
||||
any of the items, unless they are new additions, and it can very efficiently add,
|
||||
remove, and move items as they change. This allows for extremely efficient updates
|
||||
remove, and move items as they change. This allows for extremely efficient updates
|
||||
to the list as it changes, with minimal additional work.
|
||||
|
||||
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||
indices change.
|
||||
|
||||
But it’s a great idea to do something like generating a unique ID for each row as
|
||||
But it’s a great idea to do something like generating a unique ID for each row as
|
||||
it is generated, and using that as an ID for the key function.
|
||||
|
||||
Check out the `<DynamicList/>` component below for an example.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="100px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
# Forms and Inputs
|
||||
|
||||
Forms and form inputs are an important part of interactive apps. There are two
|
||||
Forms and form inputs are an important part of interactive apps. There are two
|
||||
basic patterns for interacting with inputs in Leptos, which you may recognize
|
||||
if you’re familiar with React, SolidJS, or a similar framework: using **controlled**
|
||||
or **uncontrolled** inputs.
|
||||
|
||||
## Controlled Inputs
|
||||
|
||||
In a "controlled input," the framework controls the state of the input
|
||||
element. On every `input` event, it updates a local signal that holds the current
|
||||
In a "controlled input," the framework controls the state of the input
|
||||
element. On every `input` event, it updates a local signal that holds the current
|
||||
state, which in turn updates the `value` prop of the input.
|
||||
|
||||
There are two important things to remember:
|
||||
1. The `input` event fires on (almost) every change to the element, while the
|
||||
`change` event fires (more or less) when you unfocus the input. You probably
|
||||
|
||||
1. The `input` event fires on (almost) every change to the element, while the
|
||||
`change` event fires (more or less) when you unfocus the input. You probably
|
||||
want `on:input`, but we give you the freedom to choose.
|
||||
2. The `value` *attribute* only sets the initial value of the input, i.e., it
|
||||
only updates the input up to the point that you begin typing. The `value`
|
||||
*property* continues updating the input after that. You usually want to set
|
||||
2. The `value` _attribute_ only sets the initial value of the input, i.e., it
|
||||
only updates the input up to the point that you begin typing. The `value`
|
||||
_property_ continues updating the input after that. You usually want to set
|
||||
`prop:value` for this reason.
|
||||
|
||||
```rust
|
||||
@@ -41,22 +42,23 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
## Uncontrolled Inputs
|
||||
## Uncontrolled Inputs
|
||||
|
||||
In an "uncontrolled input," the browser controls the state of the input element.
|
||||
Rather than continuously updating a signal to hold its value, we use a
|
||||
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||
In an "uncontrolled input," the browser controls the state of the input element.
|
||||
Rather than continuously updating a signal to hold its value, we use a
|
||||
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||
the input once when we want to get its value.
|
||||
|
||||
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
event.
|
||||
|
||||
```rust
|
||||
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||
|
||||
let input_element: NodeRef<HtmlElement<Input>> = NodeRef::new(cx);
|
||||
let input_element: NodeRef<Input> = create_node_ref(cx);
|
||||
```
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
underlying DOM node. Its value will be set when the element is rendered.
|
||||
|
||||
```rust
|
||||
@@ -76,13 +78,14 @@ let on_submit = move |ev: SubmitEvent| {
|
||||
set_name(value);
|
||||
};
|
||||
```
|
||||
|
||||
Our `on_submit` handler will access the input’s value and use it to call `set_name`.
|
||||
To access the DOM node stored in the `NodeRef`, we can simply call it as a function
|
||||
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||
know it will already have been filled when we rendered the view, so it’s safe to
|
||||
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||
know it will already have been filled when we rendered the view, so it’s safe to
|
||||
unwrap here.
|
||||
|
||||
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
gives us access to a correctly-typed HTML element.
|
||||
|
||||
```rust
|
||||
@@ -97,11 +100,15 @@ view! { cx,
|
||||
<p>"Name is: " {name}</p>
|
||||
}
|
||||
```
|
||||
|
||||
The view should be pretty self-explanatory by now. Note two things:
|
||||
|
||||
1. Unlike in the controlled input example, we use `value` (not `prop:value`).
|
||||
This is because we’re just setting the initial value of the input, and letting
|
||||
This is because we’re just setting the initial value of the input, and letting
|
||||
the browser control its state. (We could use `prop:value` instead.)
|
||||
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
|
||||
They are the same thing, but `node_ref` has better rust-analyzer support.)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -26,7 +26,7 @@ things:
|
||||
all of which can be rendered. Spending time in the `Option` and `Result` docs in particular
|
||||
is one of the best ways to level up your Rust game.
|
||||
4. And always remember: to be reactive, values must be functions. You’ll see me constantly
|
||||
wrap things in a `move ||` closure, below. This is to ensure that they actually re-run
|
||||
wrap things in a `move ||` closure, below. This is to ensure that they actually rerun
|
||||
when the signal they depend on changes, keeping the UI reactive.
|
||||
|
||||
## So What?
|
||||
@@ -55,13 +55,13 @@ if it’s even. Well, how about this?
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<p>
|
||||
{move || if is_odd() {
|
||||
"Odd"
|
||||
} else {
|
||||
"Even"
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
{move || if is_odd() {
|
||||
"Odd"
|
||||
} else {
|
||||
"Even"
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -74,15 +74,15 @@ Let’s say we want to render some text if it’s odd, and nothing if it’s eve
|
||||
|
||||
```rust
|
||||
let message = move || {
|
||||
if is_odd() {
|
||||
Some("Ding ding ding!")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if is_odd() {
|
||||
Some("Ding ding ding!")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -91,7 +91,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,
|
||||
<p>{message}</p>
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -105,15 +105,15 @@ pattern matching at your disposal.
|
||||
|
||||
```rust
|
||||
let message = move || {
|
||||
match value() {
|
||||
0 => "Zero",
|
||||
1 => "One",
|
||||
n if is_odd() => "Odd",
|
||||
_ => "Even"
|
||||
}
|
||||
match value() {
|
||||
0 => "Zero",
|
||||
1 => "One",
|
||||
n if is_odd() => "Odd",
|
||||
_ => "Even"
|
||||
}
|
||||
};
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -134,13 +134,13 @@ But consider the following example:
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
let message = move || if value() > 5 {
|
||||
"Big"
|
||||
"Big"
|
||||
} else {
|
||||
"Small"
|
||||
"Small"
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -148,11 +148,11 @@ This _works_, for sure. But if you added a log, you might be surprised
|
||||
|
||||
```rust
|
||||
let message = move || if value() > 5 {
|
||||
log!("{}: rendering Big", value());
|
||||
"Big"
|
||||
log!("{}: rendering Big", value());
|
||||
"Big"
|
||||
} else {
|
||||
log!("{}: rendering Small", value());
|
||||
"Small"
|
||||
log!("{}: rendering Small", value());
|
||||
"Small"
|
||||
};
|
||||
```
|
||||
|
||||
@@ -177,9 +177,9 @@ like this:
|
||||
|
||||
```rust
|
||||
let message = move || if value() > 5 {
|
||||
<Big/>
|
||||
<Big/>
|
||||
} else {
|
||||
<Small/>
|
||||
<Small/>
|
||||
};
|
||||
```
|
||||
|
||||
@@ -228,20 +228,20 @@ different branches of a conditional:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }
|
||||
}}
|
||||
</main>
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -265,21 +265,23 @@ Here’s the same example, with the conversion added:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }.into_any()
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }.into_any()
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
|
||||
}}
|
||||
</main>
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }.into_any()
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }.into_any()
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -63,7 +63,7 @@ Let’s add an `<ErrorBoundary/>` to this example.
|
||||
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
<h1>"Error Handling"</h1>
|
||||
@@ -77,12 +77,10 @@ let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.unwrap()
|
||||
.get()
|
||||
.0
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -112,4 +110,6 @@ Not a number! Errors:
|
||||
If you fix the error, the error message will disappear and the content you’re wrapping in
|
||||
an `<ErrorBoundary/>` will appear again.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -15,7 +15,7 @@ covered some of this in the material on [components and props](./03_components.m
|
||||
Basically if you want the parent to communicate to the child, you can pass a
|
||||
[`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html), a
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html), or even a
|
||||
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/struct.MaybeSignal.html) as a prop.
|
||||
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) as a prop.
|
||||
|
||||
But what about the other direction? How can a child send notifications about events
|
||||
or state changes back up to the parent?
|
||||
@@ -30,11 +30,11 @@ 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,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonA setter=set_toggled/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonA setter=set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -63,11 +63,11 @@ 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,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,13 +106,13 @@ 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,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
// note the on:click instead of on_click
|
||||
// this is the same syntax as an HTML element event listener
|
||||
<ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
// note the on:click instead of on_click
|
||||
// this is the same syntax as an HTML element event listener
|
||||
<ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,31 +142,32 @@ tree:
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
<main>
|
||||
<Content/>
|
||||
</main>
|
||||
}
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Content/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD/>
|
||||
</div>
|
||||
}
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -182,31 +183,32 @@ 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,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout set_toggled/>
|
||||
}
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
<main>
|
||||
<Content set_toggled/>
|
||||
</main>
|
||||
}
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Content set_toggled/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD set_toggled/>
|
||||
</div>
|
||||
}
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD set_toggled/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -236,26 +238,26 @@ unnecessary prop drilling.
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
|
||||
// share `set_toggled` with all children of this component
|
||||
provide_context(cx, set_toggled);
|
||||
// share `set_toggled` with all children of this component
|
||||
provide_context(cx, set_toggled);
|
||||
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
}
|
||||
|
||||
// <Layout/> and <Content/> omitted
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD(cx: Scope) -> 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)
|
||||
.expect("to have found the setter provided");
|
||||
// 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)
|
||||
.expect("to have found the setter provided");
|
||||
|
||||
view! { cx,
|
||||
<button
|
||||
@@ -283,4 +285,6 @@ in `<ButtonD/>` and a single text node in `<App/>`. It’s as if the components
|
||||
themselves don’t exist at all. And, well... at runtime, they don’t. It’s just
|
||||
signals and effects, all the way down.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -6,15 +6,15 @@ that enhances an HTML `<form>`. I need some way to pass all its inputs.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Form>
|
||||
<fieldset>
|
||||
<label>
|
||||
"Some Input"
|
||||
<input type="text" name="something"/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<button>"Submit"</button>
|
||||
</Form>
|
||||
<Form>
|
||||
<fieldset>
|
||||
<label>
|
||||
"Some Input"
|
||||
<input type="text" name="something"/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<button>"Submit"</button>
|
||||
</Form>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,13 +30,13 @@ In fact, you’ve already seen these both in action in the [`<Show/>`](/view/06_
|
||||
```rust
|
||||
view! { cx,
|
||||
<Show
|
||||
// `when` is a normal prop
|
||||
// `when` is a normal prop
|
||||
when=move || value() > 5
|
||||
// `fallback` is a "render prop": a function that returns a view
|
||||
// `fallback` is a "render prop": a function that returns a view
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
>
|
||||
// `<Big/>` (and anything else here)
|
||||
// will be given to the `children` prop
|
||||
// `<Big/>` (and anything else here)
|
||||
// will be given to the `children` prop
|
||||
<Big/>
|
||||
</Show>
|
||||
}
|
||||
@@ -62,7 +62,7 @@ where
|
||||
<h2>"Render Prop"</h2>
|
||||
{render_prop()}
|
||||
|
||||
<h2>"Children"</h2>
|
||||
<h2>"Children"</h2>
|
||||
{children(cx)}
|
||||
}
|
||||
}
|
||||
@@ -79,11 +79,11 @@ We can use the component like this:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
|
||||
// these get passed to `children`
|
||||
"Some text"
|
||||
<span>"A span"</span>
|
||||
</TakesChildren>
|
||||
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
|
||||
// these get passed to `children`
|
||||
"Some text"
|
||||
<span>"A span"</span>
|
||||
</TakesChildren>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { cx, <li>{child}</li> })
|
||||
.collect::<Vec<_>>();
|
||||
.collect_view(cx);
|
||||
|
||||
view! { cx,
|
||||
<ul>{children}</ul>
|
||||
@@ -115,10 +115,14 @@ Calling it like this will create a list:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<WrappedChildren>
|
||||
"A"
|
||||
"B"
|
||||
"C"
|
||||
</WrappedChildren>
|
||||
<WrappedChildren>
|
||||
"A"
|
||||
"B"
|
||||
"C"
|
||||
</WrappedChildren>
|
||||
}
|
||||
```
|
||||
|
||||
[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>
|
||||
|
||||
BIN
docs/video/async.mov
Normal file
BIN
docs/video/async.mov
Normal file
Binary file not shown.
BIN
docs/video/in-order.mov
Normal file
BIN
docs/video/in-order.mov
Normal file
Binary file not shown.
BIN
docs/video/out-of-order.mov
Normal file
BIN
docs/video/out-of-order.mov
Normal file
Binary file not shown.
57
examples/Makefile.toml
Normal file
57
examples/Makefile.toml
Normal file
@@ -0,0 +1,57 @@
|
||||
extend = [{ path = "./cargo-make/common.toml" }]
|
||||
|
||||
[env]
|
||||
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",
|
||||
]
|
||||
|
||||
[tasks.verify-flow]
|
||||
description = "Provides pre and post hooks for verify"
|
||||
dependencies = ["pre-verify", "verify", "post-verify"]
|
||||
|
||||
[tasks.verify]
|
||||
description = "Run all quality checks and tests"
|
||||
dependencies = ["check-style", "test-unit-and-web"]
|
||||
|
||||
[tasks.test-unit-and-web]
|
||||
description = "Run all unit and web tests"
|
||||
dependencies = ["test-flow", "web-test-flow"]
|
||||
|
||||
[tasks.pre-verify]
|
||||
|
||||
[tasks.post-verify]
|
||||
dependencies = ["clean-all"]
|
||||
|
||||
[tasks.web-test-flow]
|
||||
description = "Provides pre and post hooks for web-test"
|
||||
dependencies = ["pre-web-test", "web-test", "post-web-test"]
|
||||
|
||||
[tasks.pre-web-test]
|
||||
|
||||
[tasks.web-test]
|
||||
|
||||
[tasks.post-web-test]
|
||||
5
examples/cargo-make/cargo-leptos-web-test.toml
Normal file
5
examples/cargo-make/cargo-leptos-web-test.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[tasks.web-test]
|
||||
dependencies = ["auto-setup", "cargo-leptos-e2e"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]
|
||||
87
examples/cargo-make/common.toml
Normal file
87
examples/cargo-make/common.toml
Normal file
@@ -0,0 +1,87 @@
|
||||
[env]
|
||||
END2END_DIR = "end2end"
|
||||
|
||||
[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.verify-local]
|
||||
description = "Run all quality checks and tests from an example directory"
|
||||
dependencies = ["check-style", "test-local"]
|
||||
|
||||
[tasks.test-local]
|
||||
description = "Run all tests from an example directory"
|
||||
dependencies = ["test", "web-test"]
|
||||
|
||||
[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"
|
||||
cwd = "${END2END_DIR}"
|
||||
command = "rm"
|
||||
args = ["-rf", "playwright", "playwright/.cache", "test-results"]
|
||||
|
||||
[tasks.clean-all]
|
||||
description = "Delete all temporary directories"
|
||||
category = "Cleanup"
|
||||
dependencies = ["clean-cargo"]
|
||||
|
||||
[tasks.wasm-web-test]
|
||||
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]
|
||||
description = "Setup e2e dependencies"
|
||||
cwd = "${END2END_DIR}"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
if command -v pnpm; then
|
||||
pnpm install
|
||||
elif command -v npm; then
|
||||
npm install
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.auto-setup]
|
||||
script = '''
|
||||
if [ ! -d "${END2END_DIR}/node_modules" ]; then
|
||||
cargo make setup
|
||||
fi
|
||||
'''
|
||||
5
examples/cargo-make/wasm-web-test.toml
Normal file
5
examples/cargo-make/wasm-web-test.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[tasks.web-test]
|
||||
dependencies = ["wasm-web-test"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-trunk"]
|
||||
@@ -3,12 +3,17 @@ name = "counter"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
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,3 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use leptos::*;
|
||||
|
||||
/// A simple counter component.
|
||||
///
|
||||
///
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleCounter(
|
||||
@@ -9,7 +9,7 @@ pub fn SimpleCounter(
|
||||
/// The starting value for the counter
|
||||
initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
step: i32
|
||||
step: i32,
|
||||
) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use counter::*;
|
||||
use counter::SimpleCounter;
|
||||
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,
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
/>
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
/>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn clear() {
|
||||
let document = leptos::document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
let _ = document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
// start by rendering our counter and mounting it to the DOM
|
||||
// note that we start at the initial value of 10
|
||||
@@ -38,7 +38,7 @@ fn clear() {
|
||||
// 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(cx, 0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
@@ -71,7 +71,7 @@ fn clear() {
|
||||
fn inc() {
|
||||
let document = leptos::document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
let _ = document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
@@ -79,27 +79,27 @@ fn inc() {
|
||||
);
|
||||
|
||||
// You can do testing with vanilla DOM operations
|
||||
let document = leptos::document();
|
||||
let _document = leptos::document();
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.unwrap();
|
||||
let dec = clear
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.unwrap();
|
||||
let text = dec
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.unwrap();
|
||||
let inc = text
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
inc.click();
|
||||
@@ -6,13 +6,16 @@ edition = "2021"
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
@@ -23,9 +26,9 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0.0"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use broadcaster::BroadcastChannel;
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use broadcaster::BroadcastChannel;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn register_server_functions() {
|
||||
_ = GetServerCount::register();
|
||||
_ = AdjustServerCount::register();
|
||||
_ = ClearServerCount::register();
|
||||
pub fn register_server_functions() {
|
||||
_ = GetServerCount::register();
|
||||
_ = AdjustServerCount::register();
|
||||
_ = ClearServerCount::register();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
|
||||
#[server(GetServerCount, "/api")]
|
||||
pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
@@ -29,7 +29,10 @@ pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
}
|
||||
|
||||
#[server(AdjustServerCount, "/api")]
|
||||
pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerFnError> {
|
||||
pub async fn adjust_server_count(
|
||||
delta: i32,
|
||||
msg: String,
|
||||
) -> Result<i32, ServerFnError> {
|
||||
let new = COUNT.load(Ordering::Relaxed) + delta;
|
||||
COUNT.store(new, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&new).await;
|
||||
@@ -46,36 +49,49 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
<p>"Each of these counters stores its data in the same variable on the server."</p>
|
||||
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
|
||||
<p>
|
||||
"The value is shared across connections. Try opening this is another browser tab to see what I mean."
|
||||
</p>
|
||||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><A href="">"Simple"</A></li>
|
||||
<li><A href="form">"Form-Based"</A></li>
|
||||
<li><A href="multi">"Multi-User"</A></li>
|
||||
<li>
|
||||
<A href="">"Simple"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="form">"Form-Based"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="multi">"Multi-User"</A>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Counter/>
|
||||
}/>
|
||||
<Route path="form" view=|cx| view! {
|
||||
cx,
|
||||
<FormCounter/>
|
||||
}/>
|
||||
<Route path="multi" view=|cx| view! {
|
||||
cx,
|
||||
<MultiuserCounter/>
|
||||
}/>
|
||||
<Route
|
||||
path=""
|
||||
view=|cx| {
|
||||
view! { cx, <Counter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="form"
|
||||
view=|cx| {
|
||||
view! { cx, <FormCounter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="multi"
|
||||
view=|cx| {
|
||||
view! { cx, <MultiuserCounter/> }
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -93,33 +109,47 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || (dec.version().get(), inc.version().get(), clear.version().get()),
|
||||
move || {
|
||||
(
|
||||
dec.version().get(),
|
||||
inc.version().get(),
|
||||
clear.version().get(),
|
||||
)
|
||||
},
|
||||
|_| get_server_count(),
|
||||
);
|
||||
|
||||
let value = move || counter.read().map(|count| count.unwrap_or(0)).unwrap_or(0);
|
||||
let error_msg = move || {
|
||||
let value = move || {
|
||||
counter
|
||||
.read()
|
||||
.map(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
.flatten()
|
||||
.read(cx)
|
||||
.map(|count| count.unwrap_or(0))
|
||||
.unwrap_or(0)
|
||||
};
|
||||
let error_msg = move || {
|
||||
counter.read(cx).and_then(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<h2>"Simple Counter"</h2>
|
||||
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
|
||||
<p>
|
||||
"This counter sets the value on the server and automatically reloads the new value."
|
||||
</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
{move || error_msg().map(|msg| view! { cx, <p>"Error: " {msg.to_string()}</p>})}
|
||||
{move || {
|
||||
error_msg()
|
||||
.map(|msg| {
|
||||
view! { cx, <p>"Error: " {msg.to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -142,19 +172,15 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let value = move || {
|
||||
log::debug!("FormCounter looking for value");
|
||||
counter
|
||||
.read()
|
||||
.map(|n| n.ok())
|
||||
.flatten()
|
||||
.map(|n| n)
|
||||
.unwrap_or(0)
|
||||
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<h2>"Form Counter"</h2>
|
||||
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
|
||||
<p>
|
||||
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
|
||||
</p>
|
||||
<div>
|
||||
// calling a server function is the same as POSTing to its API URL
|
||||
// so we can just do that with a form and button
|
||||
@@ -185,26 +211,32 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
// This is the primitive pattern for live chat, collaborative editing, etc.
|
||||
#[component]
|
||||
pub fn MultiuserCounter(cx: Scope) -> 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 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());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let mut source =
|
||||
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().map(|value| {
|
||||
value
|
||||
.expect("no message event")
|
||||
.1
|
||||
.data()
|
||||
.as_string()
|
||||
.expect("expected string value")
|
||||
}),
|
||||
source
|
||||
.subscribe("message")
|
||||
.unwrap()
|
||||
.map(|value| match value {
|
||||
Ok(value) => value
|
||||
.1
|
||||
.data()
|
||||
.as_string()
|
||||
.expect("expected string value"),
|
||||
Err(_) => "0".to_string(),
|
||||
}),
|
||||
);
|
||||
|
||||
on_cleanup(cx, move || source.close());
|
||||
@@ -212,18 +244,20 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let (multiplayer_value, _) =
|
||||
create_signal(cx, None::<i32>);
|
||||
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<h2>"Multi-User Counter"</h2>
|
||||
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
|
||||
<p>
|
||||
"This one uses server-sent events (SSE) to live-update when other users make changes."
|
||||
</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default().to_string()}</span>
|
||||
<span>
|
||||
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
|
||||
</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod counters;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::counters::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod counters;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::*;
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
@@ -37,7 +37,7 @@ cfg_if! {
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
|
||||
|
||||
HttpServer::new(move || {
|
||||
@@ -48,7 +48,7 @@ cfg_if! {
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
|
||||
.service(Files::new("/", &site_root))
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -3,11 +3,22 @@ name = "counter_without_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.34"
|
||||
pretty_assertions = "1.3.0"
|
||||
rstest = "0.17.0"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
features = ["HtmlElement", "XPathResult"]
|
||||
version = "0.3.61"
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
args = ["+stable", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
args = ["+stable", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
|
||||
|
||||
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.
|
||||
|
||||
Issue the `cargo make test-flow` command to run unit and wasm tests.
|
||||
|
||||
@@ -1,48 +1,75 @@
|
||||
use leptos::{ev, html::*, *};
|
||||
|
||||
pub struct Props {
|
||||
/// The starting value for the counter
|
||||
pub initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
pub step: i32,
|
||||
}
|
||||
|
||||
/// A simple counter view.
|
||||
pub fn view(cx: Scope, props: Props) -> impl IntoView {
|
||||
let Props {
|
||||
initial_value,
|
||||
step,
|
||||
} = props;
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
// 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));
|
||||
|
||||
// elements are created by calling a function with a Scope argument
|
||||
// the function name is the same as the HTML tag name
|
||||
div(cx)
|
||||
.child((
|
||||
cx,
|
||||
// children can be added with .child()
|
||||
// this takes any type that implements IntoView as its argument
|
||||
// for example, a string or an HtmlElement<_>
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value = 0))
|
||||
.child((cx, "Clear")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value -= step)
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.child((cx, "-1")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("-1"),
|
||||
)
|
||||
.child(
|
||||
span(cx)
|
||||
.child((cx, "Value: "))
|
||||
.child((cx, move || value.get()))
|
||||
.child((cx, "!")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child(move || count.get().value())
|
||||
.child("!"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value += step)
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.child((cx, "+1")),
|
||||
))
|
||||
.child("+1"),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Count {
|
||||
value: i32,
|
||||
step: i32,
|
||||
}
|
||||
|
||||
impl Count {
|
||||
pub fn new(value: i32, step: u32) -> Self {
|
||||
Count {
|
||||
value,
|
||||
step: step as i32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self) -> i32 {
|
||||
self.value
|
||||
}
|
||||
|
||||
pub fn increase(&mut self) {
|
||||
self.value += self.step;
|
||||
}
|
||||
|
||||
pub fn decrease(&mut self) {
|
||||
self.value += -self.step;
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
use counter_without_macros as counter;
|
||||
use counter_without_macros::counter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
})
|
||||
mount_to_body(|cx| counter(cx, 0, 1))
|
||||
}
|
||||
|
||||
49
examples/counter_without_macros/tests/business.rs
Normal file
49
examples/counter_without_macros/tests/business.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
mod count {
|
||||
use counter_without_macros::Count;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(-2, 1)]
|
||||
#[case(-1, 1)]
|
||||
#[case(0, 1)]
|
||||
#[case(1, 1)]
|
||||
#[case(2, 1)]
|
||||
#[case(3, 2)]
|
||||
#[case(4, 3)]
|
||||
fn should_increase_count(#[case] initial_value: i32, #[case] step: u32) {
|
||||
let mut count = Count::new(initial_value, step);
|
||||
count.increase();
|
||||
assert_eq!(count.value(), initial_value + step as i32);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(-2, 1)]
|
||||
#[case(-1, 1)]
|
||||
#[case(0, 1)]
|
||||
#[case(1, 1)]
|
||||
#[case(2, 1)]
|
||||
#[case(3, 2)]
|
||||
#[case(4, 3)]
|
||||
#[trace]
|
||||
fn should_decrease_count(#[case] initial_value: i32, #[case] step: u32) {
|
||||
let mut count = Count::new(initial_value, step);
|
||||
count.decrease();
|
||||
assert_eq!(count.value(), initial_value - step as i32);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(-2, 1)]
|
||||
#[case(-1, 1)]
|
||||
#[case(0, 1)]
|
||||
#[case(1, 1)]
|
||||
#[case(2, 1)]
|
||||
#[case(3, 2)]
|
||||
#[case(4, 3)]
|
||||
#[trace]
|
||||
fn should_clear_count(#[case] initial_value: i32, #[case] step: u32) {
|
||||
let mut count = Count::new(initial_value, step);
|
||||
count.clear();
|
||||
assert_eq!(count.value(), 0);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counter_without_macros as counter;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let dec = clear
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let text = dec
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let inc = text
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
inc.click();
|
||||
inc.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
|
||||
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
|
||||
|
||||
clear.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
}
|
||||
86
examples/counter_without_macros/tests/web.rs
Normal file
86
examples/counter_without_macros/tests/web.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use counter_without_macros::counter;
|
||||
use leptos::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increment_counter() {
|
||||
open_counter();
|
||||
|
||||
click_increment();
|
||||
click_increment();
|
||||
|
||||
assert_eq!(see_text(), Some("Value: 2!".to_string()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrement_counter() {
|
||||
open_counter();
|
||||
|
||||
click_decrement();
|
||||
click_decrement();
|
||||
|
||||
assert_eq!(see_text(), Some("Value: -2!".to_string()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_clear_counter() {
|
||||
open_counter();
|
||||
|
||||
click_increment();
|
||||
click_increment();
|
||||
|
||||
click_clear();
|
||||
|
||||
assert_eq!(see_text(), Some("Value: 0!".to_string()));
|
||||
}
|
||||
|
||||
fn open_counter() {
|
||||
remove_existing_counter();
|
||||
mount_to_body(move |cx| counter(cx, 0, 1));
|
||||
}
|
||||
|
||||
fn remove_existing_counter() {
|
||||
if let Some(counter) =
|
||||
leptos::document().query_selector("body div").unwrap()
|
||||
{
|
||||
counter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
fn click_clear() {
|
||||
click_text("Clear");
|
||||
}
|
||||
|
||||
fn click_decrement() {
|
||||
click_text("-1");
|
||||
}
|
||||
|
||||
fn click_increment() {
|
||||
click_text("+1");
|
||||
}
|
||||
|
||||
fn click_text(text: &str) {
|
||||
find_by_text(text).click();
|
||||
}
|
||||
|
||||
fn see_text() -> Option<String> {
|
||||
find_by_text("Value: ").text_content()
|
||||
}
|
||||
|
||||
fn find_by_text(text: &str) -> HtmlElement {
|
||||
let xpath = format!("//*[text()='{}']", text);
|
||||
let document = leptos::document();
|
||||
document
|
||||
.evaluate(&xpath, &document)
|
||||
.unwrap()
|
||||
.iterate_next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
@@ -6,9 +6,10 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = "0.3"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{For, ForProps, *};
|
||||
use leptos::{For, *};
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
@@ -72,7 +72,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use counters::{Counters, CountersProps};
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> })
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
use counters::{Counters, CountersProps};
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| view! { cx, <Counters/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let add_counter = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
// add 3 counters
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
|
||||
// check HTML
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>0</span> from <span>3</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li></ul>");
|
||||
|
||||
let counters = div
|
||||
.query_selector("ul")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.children();
|
||||
|
||||
// click first counter once, second counter twice, etc.
|
||||
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let inc_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
for _ in 0..=idx {
|
||||
inc_button.click();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>6</span> from <span>3</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>1</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>2</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>3</span><button>+1</button><button>x</button></li></ul>");
|
||||
|
||||
// remove the first counter
|
||||
counters
|
||||
.item(0)
|
||||
.unwrap()
|
||||
.last_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.click();
|
||||
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>5</span> from <span>2</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>2</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>3</span><button>+1</button><button>x</button></li></ul>");
|
||||
|
||||
// decrement all by 1
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let dec_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
dec_button.click();
|
||||
}
|
||||
|
||||
run_scope(create_runtime(), move |cx| {
|
||||
// we can use RSX in test comparisons!
|
||||
// note that if RSX template creation is bugged, this probably won't catch it
|
||||
// (because the same bug will be reproduced in both sides of the assertion)
|
||||
// so I use HTML tests for most internal testing like this
|
||||
// but in user-land testing, RSX comparanda are cool
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Add Counter"</button>
|
||||
<button>"Add 1000 Counters"</button>
|
||||
<button>"Clear Counters"</button>
|
||||
<p>"Total: "<span>"3"</span>" from "<span>"2"</span>" counters."</p>
|
||||
<ul>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"1"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"2"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
.outer_html()
|
||||
);
|
||||
});
|
||||
}
|
||||
120
examples/counters/tests/web.rs
Normal file
120
examples/counters/tests/web.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| view! { cx, <Counters/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let add_counter = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
// add 3 counters
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
|
||||
// check HTML
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->0<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
);
|
||||
|
||||
let counters = div
|
||||
.query_selector("ul")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.children();
|
||||
|
||||
// click first counter once, second counter twice, etc.
|
||||
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let inc_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
for _ in 0..=idx {
|
||||
inc_button.click();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->6<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->1<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->2<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->3<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
);
|
||||
|
||||
// remove the first counter
|
||||
counters
|
||||
.item(0)
|
||||
.unwrap()
|
||||
.last_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.click();
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->5<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->2<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->3<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> })
|
||||
|
||||
@@ -3,8 +3,12 @@ name = "error_boundary"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -24,10 +24,9 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -7,59 +7,50 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
"serde",
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "errors_axum"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -11,10 +11,10 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::landing::{App, AppProps};
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::landing::App;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
error_template::{ErrorTemplate, ErrorTemplateProps},
|
||||
errors::AppError,
|
||||
};
|
||||
use crate::{error_template::ErrorTemplate, errors::AppError};
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
@@ -54,7 +51,8 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
|
||||
let generate_internal_error =
|
||||
create_server_action::<CauseInternalServerError>(cx);
|
||||
|
||||
view! { cx,
|
||||
<p>
|
||||
@@ -70,8 +68,8 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
</p>
|
||||
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
|
||||
<div>
|
||||
// note that the error boundries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribue to the
|
||||
// note that the error boundaries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribute to the
|
||||
// final status code sent by the server when producing ssr pages.
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
@@ -8,12 +7,12 @@ pub mod landing;
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::landing::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use crate::landing::*;
|
||||
use axum::body::Body as AxumBody;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
extract::{State, Path},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
@@ -21,7 +21,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
Extension(options): Extension<Arc<LeptosOptions>>,
|
||||
State(options): State<Arc<LeptosOptions>>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
@@ -37,13 +37,14 @@ async fn custom_handler(
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
crate::landing::register_server_functions();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -51,9 +52,13 @@ async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
|
||||
.leptos_routes(
|
||||
leptos_options.clone(),
|
||||
routes,
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -3,15 +3,18 @@ name = "fetch"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.58"
|
||||
leptos = { path = "../../leptos" }
|
||||
reqwasm = "0.5.0"
|
||||
reqwasm = "0.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,38 +1,50 @@
|
||||
use anyhow::Result;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Cat {
|
||||
url: String,
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum FetchError {
|
||||
#[error("Please request more than zero cats.")]
|
||||
NonZeroCats,
|
||||
#[error("Error loading data from serving.")]
|
||||
Request,
|
||||
#[error("Error deserializaing cat data from request.")]
|
||||
Json,
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
|
||||
if count > 0 {
|
||||
// make the request
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={count}",
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.await
|
||||
.map_err(|_| FetchError::Request)?
|
||||
// convert it to JSON
|
||||
.json::<Vec<Cat>>()
|
||||
.await?
|
||||
.await
|
||||
.map_err(|_| FetchError::Json)?
|
||||
// extract the URL field for each cat
|
||||
.into_iter()
|
||||
.map(|cat| cat.url)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(res)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
Err(FetchError::NonZeroCats)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 0);
|
||||
|
||||
// we use local_resource here because
|
||||
// 1) anyhow::Result isn't serializable/deserializable
|
||||
// 1) our error type isn't serializable/deserializable
|
||||
// 2) we're not doing server-side rendering in this example anyway
|
||||
// (during SSR, create_resource will begin loading on the server and resolve on the client)
|
||||
let cats = create_local_resource(cx, cat_count, fetch_cats);
|
||||
@@ -42,8 +54,8 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
errors.with(|errors| {
|
||||
errors
|
||||
.iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
|
||||
.collect_view(cx)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -60,11 +72,12 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
// and by using the ErrorBoundary fallback to catch Err(_)
|
||||
// so we'll just implement our happy path and let the framework handle the rest
|
||||
let cats_view = move || {
|
||||
cats.with(|data| {
|
||||
data.iter()
|
||||
.flatten()
|
||||
.map(|cat| view! { cx, <img src={cat}/> })
|
||||
.collect::<Vec<_>>()
|
||||
cats.read(cx).map(|data| {
|
||||
data.map(|data| {
|
||||
data.iter()
|
||||
.map(|s| view! { cx, <span>{s}</span> })
|
||||
.collect_view(cx)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
@@ -72,8 +85,9 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
<div>
|
||||
<label>
|
||||
"How many cats would you like?"
|
||||
<input type="number"
|
||||
prop:value={move || cat_count.get().to_string()}
|
||||
<input
|
||||
type="number"
|
||||
prop:value=move || cat_count.get().to_string()
|
||||
on:input=move |ev| {
|
||||
let val = event_target_value(&ev).parse::<u32>().unwrap_or(0);
|
||||
set_cat_count(val);
|
||||
@@ -81,7 +95,9 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
/>
|
||||
</label>
|
||||
<ErrorBoundary fallback>
|
||||
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
|
||||
<Transition fallback=move || {
|
||||
view! { cx, <div>"Loading (Suspense Fallback)..."</div> }
|
||||
}>
|
||||
{cats_view}
|
||||
</Transition>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -2,7 +2,6 @@ use fetch::fetch_example;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(fetch_example)
|
||||
|
||||
@@ -6,13 +6,15 @@ edition = "2021"
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
@@ -21,15 +23,13 @@ leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tracing = "0.1"
|
||||
# openssl = { version = "0.10", features = ["v110"] }
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
tracing = "0.1"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user