mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
285 Commits
server-fn-
...
v0.5.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6ee2a37f4 | ||
|
|
18a92bbfd8 | ||
|
|
4e8c3accf2 | ||
|
|
a8e25af523 | ||
|
|
d531848db5 | ||
|
|
670f415565 | ||
|
|
061213ca78 | ||
|
|
0ce4ee8a7a | ||
|
|
1cd6603da0 | ||
|
|
453911e6fc | ||
|
|
cb6267ad08 | ||
|
|
4518d3c89f | ||
|
|
e47a619556 | ||
|
|
414f5fc393 | ||
|
|
362e3bc603 | ||
|
|
4d549f70c9 | ||
|
|
85dd726d43 | ||
|
|
24febe11f3 | ||
|
|
64b1e9bed3 | ||
|
|
68c91a732d | ||
|
|
8573f22d96 | ||
|
|
61c7ff4256 | ||
|
|
860d887931 | ||
|
|
5e929a75fa | ||
|
|
d82cf0b76a | ||
|
|
cb7e07496a | ||
|
|
17881c5c6e | ||
|
|
2e816b26aa | ||
|
|
68d67c9e92 | ||
|
|
0dea6fdcea | ||
|
|
530dcff86a | ||
|
|
9d9a4932b3 | ||
|
|
bfb67d45e8 | ||
|
|
b1e8105442 | ||
|
|
7aced17976 | ||
|
|
191b40b2ac | ||
|
|
15ca5bec61 | ||
|
|
ba4d226004 | ||
|
|
3adfd334df | ||
|
|
d7ca5f2e96 | ||
|
|
67bdb3498f | ||
|
|
9e9386b223 | ||
|
|
4029de2d42 | ||
|
|
777095670e | ||
|
|
a11c6303e2 | ||
|
|
3394e316b7 | ||
|
|
4b0437394c | ||
|
|
d10a566e48 | ||
|
|
e0cca3e7a3 | ||
|
|
0c8ab7c725 | ||
|
|
a2bef05a4b | ||
|
|
6361985fb1 | ||
|
|
ad290f5ed2 | ||
|
|
5f53a1459e | ||
|
|
379623d548 | ||
|
|
db1113e5b3 | ||
|
|
d943a50df1 | ||
|
|
eb86899e08 | ||
|
|
e2842ede44 | ||
|
|
fdd4b3d919 | ||
|
|
7771052db8 | ||
|
|
d999ff857d | ||
|
|
30370a55e1 | ||
|
|
a7330d61b6 | ||
|
|
e2f6780de4 | ||
|
|
05b4f8e617 | ||
|
|
eb888029d1 | ||
|
|
3e08486385 | ||
|
|
12b0295906 | ||
|
|
2756327f12 | ||
|
|
b8ca8b7849 | ||
|
|
6abdca0597 | ||
|
|
bf14999eb2 | ||
|
|
c87328f5cf | ||
|
|
9a70898b09 | ||
|
|
4a83ffca6f | ||
|
|
319017f03f | ||
|
|
33e166a462 | ||
|
|
8994154b23 | ||
|
|
7b88df32d1 | ||
|
|
0d6ddfb71e | ||
|
|
4a4e16c206 | ||
|
|
11f6a5d341 | ||
|
|
ad208ec473 | ||
|
|
72ad1d7c68 | ||
|
|
d6a9d2efdf | ||
|
|
8eed999611 | ||
|
|
07f2cbfbba | ||
|
|
fc4dea6839 | ||
|
|
17b3300351 | ||
|
|
f3508cef36 | ||
|
|
5c41d20421 | ||
|
|
53e16751a7 | ||
|
|
18f7b56c03 | ||
|
|
c6f51e6a09 | ||
|
|
971fb734de | ||
|
|
200304402f | ||
|
|
4baa75ccf0 | ||
|
|
9af1c7e1a3 | ||
|
|
a302257129 | ||
|
|
4251f6c0f4 | ||
|
|
c080c2cbca | ||
|
|
0676348bd4 | ||
|
|
18ad7cde20 | ||
|
|
b61d0553a0 | ||
|
|
2a3b613230 | ||
|
|
0d4862b238 | ||
|
|
c7607f6fcc | ||
|
|
29216b226f | ||
|
|
32ba0ce4fb | ||
|
|
c781b4e1c7 | ||
|
|
be2d014f08 | ||
|
|
18cdf70864 | ||
|
|
1be25f0f47 | ||
|
|
cc93651bc9 | ||
|
|
a7a1559e01 | ||
|
|
15f08aaa30 | ||
|
|
d0295bae01 | ||
|
|
5220c37edd | ||
|
|
4f649e020c | ||
|
|
e0d15c1a09 | ||
|
|
6f9c40b0a8 | ||
|
|
a946a0181d | ||
|
|
6d44540ab3 | ||
|
|
6a547cb9db | ||
|
|
ac8dd7af67 | ||
|
|
0962f699e4 | ||
|
|
fcd1028fb7 | ||
|
|
dc429e33ff | ||
|
|
6b40ca36a5 | ||
|
|
d869bc6675 | ||
|
|
d8aeb82949 | ||
|
|
32e8213ebf | ||
|
|
fa2be59895 | ||
|
|
321c522fa5 | ||
|
|
7378b8581a | ||
|
|
2d634364a9 | ||
|
|
f7adf6f73d | ||
|
|
fb914e1a50 | ||
|
|
772bb1d60c | ||
|
|
bd4d2202ea | ||
|
|
870808e63f | ||
|
|
d7fff5a8ab | ||
|
|
609afce544 | ||
|
|
181bcadbe2 | ||
|
|
3f2a9facf8 | ||
|
|
c5c79234f1 | ||
|
|
de9fb5e382 | ||
|
|
c9d132f007 | ||
|
|
a1a9d41a7a | ||
|
|
73112c9faa | ||
|
|
50678dafe1 | ||
|
|
b1363a16ab | ||
|
|
ae986e71fa | ||
|
|
0531831fe8 | ||
|
|
18eeee8e1f | ||
|
|
d99269afac | ||
|
|
38d1727e9c | ||
|
|
e0265252d7 | ||
|
|
6cc92cee8d | ||
|
|
1d392483b4 | ||
|
|
3b864ac1a0 | ||
|
|
baa5ea83fa | ||
|
|
d651400fa2 | ||
|
|
b729a658df | ||
|
|
2c8f46466b | ||
|
|
f2117b1186 | ||
|
|
726cf47f17 | ||
|
|
1759a3e149 | ||
|
|
2374439cd8 | ||
|
|
f85bfd31db | ||
|
|
43b58bfba9 | ||
|
|
fafb6c01da | ||
|
|
1bd47f34e5 | ||
|
|
661a038780 | ||
|
|
e706a69139 | ||
|
|
2b59ae18bc | ||
|
|
7d3e2a41b9 | ||
|
|
7ef57345ca | ||
|
|
7e5169e66d | ||
|
|
73a85b4955 | ||
|
|
2c12256260 | ||
|
|
a821abfb11 | ||
|
|
20e5db22b8 | ||
|
|
54e8a536c4 | ||
|
|
afa67726c1 | ||
|
|
1db3e9c686 | ||
|
|
2fd6e0a2a8 | ||
|
|
af454c7643 | ||
|
|
1a589fcf32 | ||
|
|
af215d6ce8 | ||
|
|
e9fef73f53 | ||
|
|
7c9b118b2d | ||
|
|
5db2590bc6 | ||
|
|
dc1ba24470 | ||
|
|
e384d53996 | ||
|
|
946f9ff3e1 | ||
|
|
8d690ac146 | ||
|
|
8245d77738 | ||
|
|
59c7684568 | ||
|
|
a158e7f8bd | ||
|
|
c11c4b0e3e | ||
|
|
fe42ac11a8 | ||
|
|
00f8c9583d | ||
|
|
a317874f93 | ||
|
|
651356a9ec | ||
|
|
1c2327b2d6 | ||
|
|
8c3e0f23b0 | ||
|
|
1719c0d352 | ||
|
|
bb78f64cd5 | ||
|
|
2fe5be2483 | ||
|
|
929fe08525 | ||
|
|
66dfef8729 | ||
|
|
238d61ce1e | ||
|
|
2fa2bf1706 | ||
|
|
a07984be9e | ||
|
|
e8a7086546 | ||
|
|
23d48d4c0e | ||
|
|
3342faa039 | ||
|
|
6c24061c82 | ||
|
|
b9a1fb7743 | ||
|
|
3c3fc969ac | ||
|
|
c87212f2d7 | ||
|
|
b3a4c95dad | ||
|
|
de44b1f91f | ||
|
|
689022661d | ||
|
|
905d46a09d | ||
|
|
5585f20940 | ||
|
|
5c3ed3f018 | ||
|
|
03cabf6ea3 | ||
|
|
2798dc455f | ||
|
|
db20be5576 | ||
|
|
495862e9f9 | ||
|
|
2ca1c51fdc | ||
|
|
70e1ad41e2 | ||
|
|
53ec7ed272 | ||
|
|
d98a577740 | ||
|
|
fd834f48c2 | ||
|
|
7be65a37c6 | ||
|
|
3b5e2d86fb | ||
|
|
716b9fb50b | ||
|
|
006ca13797 | ||
|
|
6e008343c8 | ||
|
|
2ca24883ac | ||
|
|
4a43983f4e | ||
|
|
d9e83121c1 | ||
|
|
f5b4b97c9b | ||
|
|
bcfa430a40 | ||
|
|
7c51815cf5 | ||
|
|
fee2fb953b | ||
|
|
8ecb7f59c4 | ||
|
|
b85cb9fb3b | ||
|
|
a631c5ca1c | ||
|
|
bee9bd8f67 | ||
|
|
8d3874f8a9 | ||
|
|
bade16d227 | ||
|
|
e0a132bde3 | ||
|
|
4d7e1f4d26 | ||
|
|
700eee6604 | ||
|
|
694ed61e4c | ||
|
|
d7330097ba | ||
|
|
c65a3a6ca3 | ||
|
|
793c191619 | ||
|
|
6c3e2fe53e | ||
|
|
08c419e3ee | ||
|
|
736f4185b5 | ||
|
|
9cc0fc8c49 | ||
|
|
8f067dcde7 | ||
|
|
ad6eb58fe1 | ||
|
|
3f3ab1c3c8 | ||
|
|
9adae32847 | ||
|
|
b8098e7992 | ||
|
|
bef4d0dd3b | ||
|
|
a789100e22 | ||
|
|
abeca70625 | ||
|
|
cc293b1170 | ||
|
|
8ab62c17c6 | ||
|
|
cf14e857ca | ||
|
|
c322ef38fd | ||
|
|
c9cc493063 | ||
|
|
fb48f7f117 | ||
|
|
c344e54cf6 | ||
|
|
7306ecccbc | ||
|
|
b98174db7a | ||
|
|
e48f66694d |
51
.github/workflows/check-stable.yml
vendored
51
.github/workflows/check-stable.yml
vendored
@@ -1,51 +0,0 @@
|
||||
name: Check stable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
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
|
||||
1
.github/workflows/ci-changed-examples.yml
vendored
1
.github/workflows/ci-changed-examples.yml
vendored
@@ -29,3 +29,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Check Examples
|
||||
name: CI Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,12 +11,10 @@ on:
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
get-examples-matrix:
|
||||
uses: ./.github/workflows/get-examples-matrix.yml
|
||||
|
||||
test:
|
||||
name: Check
|
||||
name: CI
|
||||
needs: [get-leptos-changed, get-examples-matrix]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
@@ -25,4 +23,5 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
26
.github/workflows/ci-stable-examples.yml
vendored
Normal file
26
.github/workflows/ci-stable-examples.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: CI Stable Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
directory: [examples/counters_stable, examples/counter_without_macros]
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -41,3 +41,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
@@ -20,13 +20,13 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v36
|
||||
uses: tj-actions/changed-files@v39
|
||||
with:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
@@ -34,8 +34,9 @@ jobs:
|
||||
examples
|
||||
!examples/cargo-make
|
||||
!examples/gtk
|
||||
!examples/hackernews_js_fetch
|
||||
!examples/Makefile.toml
|
||||
!examples/README.md
|
||||
!examples/*.md
|
||||
json: true
|
||||
quotepath: false
|
||||
|
||||
|
||||
6
.github/workflows/get-example-changed.yml
vendored
6
.github/workflows/get-example-changed.yml
vendored
@@ -15,20 +15,20 @@ jobs:
|
||||
example_changed: ${{ steps.set-example-changed.outputs.example_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v36
|
||||
uses: tj-actions/changed-files@v39
|
||||
with:
|
||||
files: |
|
||||
examples
|
||||
!examples/cargo-make
|
||||
!examples/gtk
|
||||
!examples/Makefile.toml
|
||||
!examples/README.md
|
||||
!examples/*.md
|
||||
|
||||
- name: List example files that changed
|
||||
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
|
||||
|
||||
14
.github/workflows/get-examples-matrix.yml
vendored
14
.github/workflows/get-examples-matrix.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install JQ Tool
|
||||
uses: mbround18/install-jq@v1
|
||||
@@ -23,12 +23,12 @@ jobs:
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v examples/README.md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v .md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
4
.github/workflows/get-leptos-changed.yml
vendored
4
.github/workflows/get-leptos-changed.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v36
|
||||
uses: tj-actions/changed-files@v39
|
||||
with:
|
||||
files: |
|
||||
integrations
|
||||
|
||||
55
.github/workflows/publish-book.yml
vendored
55
.github/workflows/publish-book.yml
vendored
@@ -1,36 +1,37 @@
|
||||
name: Deploy book
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*-?v[0-9]+*'
|
||||
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
|
||||
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
|
||||
- uses: actions/checkout@v4
|
||||
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
|
||||
|
||||
38
.github/workflows/run-cargo-make-task.yml
vendored
38
.github/workflows/run-cargo-make-task.yml
vendored
@@ -9,6 +9,9 @@ on:
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
toolchain:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -16,23 +19,17 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Setup environment
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
@@ -56,7 +53,7 @@ jobs:
|
||||
run: trunk --version
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
@@ -98,14 +95,17 @@ jobs:
|
||||
|
||||
- name: Maybe install playwright browser dependencies
|
||||
run: |
|
||||
playwright_count=$(find ${{inputs.directory}} -name playwright.config.ts | wc -l)
|
||||
if [ $playwright_count -eq 1 ]; then
|
||||
echo playwright required
|
||||
sudo apt-get update
|
||||
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
|
||||
else
|
||||
echo playwright is not required
|
||||
fi
|
||||
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
if [ ! -v $pw_dir ]; then
|
||||
echo "Playwright required in $pw_dir"
|
||||
cd $pw_dir
|
||||
pnpm dlx playwright install --with-deps
|
||||
else
|
||||
echo Playwright is not required
|
||||
fi
|
||||
done
|
||||
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
|
||||
25
.github/workflows/verify-all-examples.yml
vendored
25
.github/workflows/verify-all-examples.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: CI Examples
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
# Run once a day at 3:00 AM EST
|
||||
- cron: "0 8 * * *"
|
||||
|
||||
jobs:
|
||||
get-examples-matrix:
|
||||
uses: ./.github/workflows/get-examples-matrix.yml
|
||||
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-examples-matrix]
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ Cargo.lock
|
||||
.idea
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.5.0" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.0" }
|
||||
leptos_router = { path = "./router", version = "0.5.0" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0" }
|
||||
leptos = { path = "./leptos", version = "0.5.3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.3" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.3" }
|
||||
leptos_router = { path = "./router", version = "0.5.3" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.3" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
[Website](https://leptos.dev) | [Book](https://leptos-rs.github.io/leptos/) | [Docs.rs](https://docs.rs/leptos/latest/leptos/) | [Playground](https://codesandbox.io/p/sandbox/leptos-rtfggt?file=%2Fsrc%2Fmain.rs%3A1%2C1) | [Discord](https://discord.gg/YdRAhS7eQB)
|
||||
|
||||
You can find a list of useful libraries and example projects at [`awesome-leptos`](https://github.com/leptos-rs/awesome-leptos).
|
||||
|
||||
# Leptos
|
||||
|
||||
```rust
|
||||
@@ -46,10 +48,6 @@ pub fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
### Important Note
|
||||
|
||||
This example, and the entire `main` branch, now reflect the upcoming `0.5.0` release. You can use `0.5.0` with the `0.5.0-beta` release on crates.io or by a git dependency on the `main` branch of this repo. [Click here for the 0.4.9 `README`](https://crates.io/crates/leptos).
|
||||
|
||||
## About the Framework
|
||||
|
||||
Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces.
|
||||
|
||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a suspected security issue, please contact security@leptos.dev rather than opening
|
||||
a public issue.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The most-recently-released version of the library is supported with security updates.
|
||||
For example, if a security issue is discovered that affects 0.3.2 and all later releases,
|
||||
a 0.4.x patch will be released but a new 0.3.x patch release will not be made. You should
|
||||
plan to update to the latest version to receive any new features or bugfixes of any kind.
|
||||
@@ -4,10 +4,19 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", features = ["ssr"] }
|
||||
l0410 = { package = "leptos", version = "0.4.10", features = [
|
||||
"nightly",
|
||||
"ssr",
|
||||
] }
|
||||
leptos = { path = "../leptos", features = ["ssr", "nightly"] }
|
||||
leptos_reactive = { path = "../leptos_reactive", features = ["ssr", "nightly"] }
|
||||
tachydom = { git = "https://github.com/gbj/tachys", features = [
|
||||
"nightly",
|
||||
"leptos",
|
||||
] }
|
||||
tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
yew = { version = "0.20", features = ["ssr"] }
|
||||
tokio-test = "0.4"
|
||||
miniserde = "0.1"
|
||||
gloo = "0.8"
|
||||
@@ -20,7 +29,6 @@ strum_macros = "0.24"
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
tera = "1"
|
||||
reactive-signals = "0.1.0-alpha.4"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
extern crate test;
|
||||
|
||||
//åmod reactive;
|
||||
//mod ssr;
|
||||
mod reactive;
|
||||
mod ssr;
|
||||
mod todomvc;
|
||||
|
||||
@@ -7,19 +7,16 @@ fn leptos_deep_creation(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, || {
|
||||
let signal = create_rw_signal(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(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
let signal = create_rw_signal(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(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
@@ -31,20 +28,17 @@ fn leptos_deep_update(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, || {
|
||||
let signal = create_rw_signal(0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
let signal = create_rw_signal(0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
@@ -56,16 +50,12 @@ fn leptos_narrowing_down(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, || {
|
||||
let sigs = (0..1000).map(|n| create_signal(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(move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
let sigs = (0..1000).map(|n| create_signal(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(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
assert_eq!(memo(), 499500);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
@@ -77,16 +67,13 @@ fn leptos_fanning_out(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, || {
|
||||
let sig = create_rw_signal(0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(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()
|
||||
let sig = create_rw_signal(0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(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);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
@@ -97,145 +84,36 @@ fn leptos_narrowing_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, || {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(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(move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect({
|
||||
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 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(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move || {
|
||||
let (r, w) = create_signal(0);
|
||||
create_isomorphic_effect({
|
||||
move |_| {
|
||||
acc.set(r());
|
||||
}
|
||||
});
|
||||
w.update(|n| *n += 1);
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for disposer in disposers {
|
||||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_deep_update(b: &mut Bencher) {
|
||||
use reactive_signals::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
|
||||
};
|
||||
|
||||
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::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
|
||||
};
|
||||
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::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
|
||||
};
|
||||
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 sigs = (0..1000).map(|n| create_signal(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(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect({
|
||||
let acc = Rc::clone(&acc);
|
||||
move || {
|
||||
acc.set(memo.get());
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
sigs[1].update(|n| *n += 1);
|
||||
sigs[10].update(|n| *n += 1);
|
||||
sigs[100].update(|n| *n += 1);
|
||||
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.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_creation(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_deep_creation(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -257,8 +135,8 @@ fn l021_deep_creation(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_deep_update(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -282,8 +160,8 @@ fn l021_deep_update(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_narrowing_down(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_narrowing_down(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -305,8 +183,8 @@ fn l021_narrowing_down(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
fn l0410_fanning_out(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -325,8 +203,8 @@ fn l021_fanning_out(b: &mut Bencher) {
|
||||
runtime.dispose();
|
||||
}
|
||||
#[bench]
|
||||
fn l021_narrowing_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_narrowing_update(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -339,11 +217,11 @@ fn l021_narrowing_update(b: &mut Bencher) {
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
assert_eq!(memo.get(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
acc.set(memo.get());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
@@ -353,7 +231,7 @@ fn l021_narrowing_update(b: &mut Bencher) {
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
assert_eq!(memo.get(), 499503);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
@@ -362,8 +240,8 @@ fn l021_narrowing_update(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -376,7 +254,7 @@ fn l021_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
move |_| {
|
||||
acc.set(r());
|
||||
acc.set(r.get());
|
||||
}
|
||||
});
|
||||
w.update(|n| *n += 1);
|
||||
|
||||
@@ -2,15 +2,14 @@ use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let r = create_runtime();
|
||||
b.iter(|| {
|
||||
use leptos::*;
|
||||
leptos_dom::HydrationCtx::reset_id();
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
leptos::leptos_dom::HydrationCtx::reset_id();
|
||||
#[component]
|
||||
fn Counter(initial: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial);
|
||||
let (value, set_value) = create_signal(initial);
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
@@ -20,7 +19,6 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
@@ -28,14 +26,53 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
<Counter initial=2/>
|
||||
<Counter initial=3/>
|
||||
</main>
|
||||
}.into_view(cx).render_to_string(cx);
|
||||
}.into_view().render_to_string();
|
||||
|
||||
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 data-hk=\"0-0-0-1\"><h1 data-hk=\"0-0-0-2\">Welcome to our benchmark page.</h1><p data-hk=\"0-0-0-3\">Here's some introductory text.</p><div data-hk=\"0-0-0-5\"><button data-hk=\"0-0-0-6\">-1</button><span data-hk=\"0-0-0-7\">Value: <!>1<!--hk=0-0-0-8-->!</span><button data-hk=\"0-0-0-9\">+1</button></div><!--hk=0-0-0-4--><div data-hk=\"0-0-0-11\"><button data-hk=\"0-0-0-12\">-1</button><span data-hk=\"0-0-0-13\">Value: <!>2<!--hk=0-0-0-14-->!</span><button data-hk=\"0-0-0-15\">+1</button></div><!--hk=0-0-0-10--><div data-hk=\"0-0-0-17\"><button data-hk=\"0-0-0-18\">-1</button><span data-hk=\"0-0-0-19\">Value: <!>3<!--hk=0-0-0-20-->!</span><button data-hk=\"0-0-0-21\">+1</button></div><!--hk=0-0-0-16--></main>" );
|
||||
});
|
||||
r.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tachys_ssr_bench(b: &mut Bencher) {
|
||||
use leptos::{create_runtime, create_signal, SignalGet, SignalUpdate};
|
||||
use tachy_maccy::view;
|
||||
use tachydom::view::{Render, RenderHtml};
|
||||
use tachydom::html::element::ElementChild;
|
||||
use tachydom::html::attribute::global::ClassAttribute;
|
||||
use tachydom::html::attribute::global::GlobalAttributes;
|
||||
use tachydom::html::attribute::global::OnAttribute;
|
||||
use tachydom::renderer::dom::Dom;
|
||||
let rt = create_runtime();
|
||||
b.iter(|| {
|
||||
fn counter(initial: i32) -> impl Render<Dom> + RenderHtml<Dom> {
|
||||
let (value, set_value) = create_signal(initial);
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
{counter(1)}
|
||||
{counter(2)}
|
||||
{counter(3)}
|
||||
</main>
|
||||
}.to_html();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><div><button>-1</button><span>Value: <!>1<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>2<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>3<!>!</span><button>+1</button></div></main>"
|
||||
);
|
||||
});
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
||||
@@ -192,7 +192,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |todo: Todo| {
|
||||
children=move |todo: Todo| {
|
||||
view! { <Todo todo=todo.clone()/> }
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@ use test::Bencher;
|
||||
|
||||
mod leptos;
|
||||
mod sycamore;
|
||||
mod tachys;
|
||||
mod tera;
|
||||
mod yew;
|
||||
|
||||
@@ -17,13 +18,29 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tachys_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::tachys::*;
|
||||
use tachydom::view::{Render, RenderHtml};
|
||||
|
||||
let rendered = TodoMVC(Todos::new()).to_html();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main><section class=\"todoapp\"><header class=\"header\"><h1>todos</h1><input placeholder=\"What needs to be done?\" autofocus class=\"new-todo\"></header><section class=\"main hidden\"><input id=\"toggle-all\" type=\"checkbox\" class=\"toggle-all\"><label for=\"toggle-all\">Mark all as complete</label><ul class=\"todo-list\"></ul></section><footer class=\"footer hidden\"><span class=\"todo-count\"><strong>0</strong><!> items<!> left</span><ul class=\"filters\"><li><a href=\"#/\" class=\"selected selected\">All</a></li><li><a href=\"#/active\" class=\"\">Active</a></li><li><a href=\"#/completed\" class=\"\">Completed</a></li></ul><button class=\"clear-completed hidden hidden\">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>" );
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_todomvc_ssr(b: &mut Bencher) {
|
||||
use self::sycamore::*;
|
||||
use ::sycamore::prelude::*;
|
||||
use ::sycamore::*;
|
||||
use ::sycamore::{prelude::*, *};
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
@@ -42,8 +59,7 @@ fn sycamore_todomvc_ssr(b: &mut Bencher) {
|
||||
#[bench]
|
||||
fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
use self::yew::*;
|
||||
use ::yew::prelude::*;
|
||||
use ::yew::ServerRenderer;
|
||||
use ::yew::{prelude::*, ServerRenderer};
|
||||
|
||||
b.iter(|| {
|
||||
tokio_test::block_on(async {
|
||||
@@ -60,21 +76,33 @@ fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
let html = ::leptos::ssr::render_to_string(|| {
|
||||
view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new_with_1000(cx)/>
|
||||
<TodoMVC todos=Todos::new_with_1000()/>
|
||||
}
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tachys_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::tachys::*;
|
||||
use tachydom::view::{Render, RenderHtml};
|
||||
|
||||
let rendered = TodoMVC(Todos::new_with_1000()).to_html();
|
||||
assert!(rendered.len() > 20_000)
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use self::sycamore::*;
|
||||
use ::sycamore::prelude::*;
|
||||
use ::sycamore::*;
|
||||
use ::sycamore::{prelude::*, *};
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
@@ -93,8 +121,7 @@ fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
#[bench]
|
||||
fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use self::yew::*;
|
||||
use ::yew::prelude::*;
|
||||
use ::yew::ServerRenderer;
|
||||
use ::yew::{prelude::*, ServerRenderer};
|
||||
|
||||
b.iter(|| {
|
||||
tokio_test::block_on(async {
|
||||
@@ -103,4 +130,19 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tera_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
let html = ::leptos::ssr::render_to_string(|| {
|
||||
view! { <TodoMVC todos=Todos::new()/> }
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
333
benchmarks/src/todomvc/tachys.rs
Normal file
333
benchmarks/src/todomvc/tachys.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
pub use leptos_reactive::*;
|
||||
use miniserde::*;
|
||||
use tachy_maccy::view;
|
||||
use tachydom::{
|
||||
html::{
|
||||
attribute::global::{ClassAttribute, GlobalAttributes, OnAttribute},
|
||||
element::ElementChild,
|
||||
},
|
||||
renderer::dom::Dom,
|
||||
view::{keyed::keyed, Render, RenderHtml},
|
||||
};
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Todos(pub Vec<Todo>);
|
||||
|
||||
const STORAGE_KEY: &str = "todos-leptos";
|
||||
|
||||
impl Todos {
|
||||
pub fn new() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub fn new_with_1000() -> Self {
|
||||
let todos = (0..1000)
|
||||
.map(|id| Todo::new(id, format!("Todo #{id}")))
|
||||
.collect();
|
||||
Self(todos)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, todo: Todo) {
|
||||
self.0.push(todo);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: usize) {
|
||||
self.0.retain(|todo| todo.id != id);
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.0.iter().filter(|todo| !(todo.completed)()).count()
|
||||
}
|
||||
|
||||
pub fn completed(&self) -> usize {
|
||||
self.0.iter().filter(|todo| (todo.completed)()).count()
|
||||
}
|
||||
|
||||
pub fn toggle_all(&self) {
|
||||
// if all are complete, mark them all active instead
|
||||
if self.remaining() == 0 {
|
||||
for todo in &self.0 {
|
||||
if todo.completed.get() {
|
||||
(todo.set_completed)(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// otherwise, mark them all complete
|
||||
else {
|
||||
for todo in &self.0 {
|
||||
(todo.set_completed)(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_completed(&mut self) {
|
||||
self.0.retain(|todo| !todo.completed.get());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Todo {
|
||||
pub id: usize,
|
||||
pub title: ReadSignal<String>,
|
||||
pub set_title: WriteSignal<String>,
|
||||
pub completed: ReadSignal<bool>,
|
||||
pub set_completed: WriteSignal<bool>,
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(id, title, false)
|
||||
}
|
||||
|
||||
pub fn new_with_completed(
|
||||
id: usize,
|
||||
title: String,
|
||||
completed: bool,
|
||||
) -> Self {
|
||||
let (title, set_title) = create_signal(title);
|
||||
let (completed, set_completed) = create_signal(completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
set_title,
|
||||
completed,
|
||||
set_completed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
self.set_completed
|
||||
.update(|completed| *completed = !*completed);
|
||||
}
|
||||
}
|
||||
|
||||
const ESCAPE_KEY: u32 = 27;
|
||||
const ENTER_KEY: u32 = 13;
|
||||
|
||||
pub fn TodoMVC(todos: Todos) -> impl Render<Dom> + RenderHtml<Dom> {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
.map(|todo| todo.id)
|
||||
.max()
|
||||
.map(|last| last + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let (todos, set_todos) = create_signal(todos);
|
||||
provide_context(set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(Mode::All);
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
todo!()
|
||||
/* let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
}
|
||||
} */
|
||||
};
|
||||
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| !todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
Mode::Completed => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
})
|
||||
});
|
||||
|
||||
// effect to serialize to JSON
|
||||
// this does reactive reads, so it will automatically serialize on any relevant change
|
||||
create_effect(move |_| {
|
||||
()
|
||||
/* if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
.0
|
||||
.iter()
|
||||
.map(TodoSerialized::from)
|
||||
.collect::<Vec<_>>();
|
||||
let json = json::to_string(&objs);
|
||||
if storage.set_item(STORAGE_KEY, &json).is_err() {
|
||||
log::error!("error while trying to set item in localStorage");
|
||||
}
|
||||
} */
|
||||
});
|
||||
|
||||
view! {
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus
|
||||
/>
|
||||
</header>
|
||||
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
r#type="checkbox"
|
||||
//prop:checked=move || todos.with(|t| t.remaining() > 0)
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label r#for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
{move || {
|
||||
keyed(filtered_todos.get(), |todo| todo.id, Todo)
|
||||
}}
|
||||
</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>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Todo(todo: Todo) -> impl Render<Dom> + RenderHtml<Dom> {
|
||||
let (editing, set_editing) = create_signal(false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
|
||||
//let input = NodeRef::new();
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
set_todos.update(|t| t.remove(todo.id));
|
||||
} else {
|
||||
(todo.set_title)(value.to_string());
|
||||
}
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! {
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
/* <div class="view">
|
||||
<input class="toggle" r#type="checkbox"/>
|
||||
<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! {
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
/>
|
||||
}
|
||||
})
|
||||
}} */
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Active,
|
||||
Completed,
|
||||
All,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Mode::All
|
||||
}
|
||||
}
|
||||
|
||||
pub fn route(hash: &str) -> Mode {
|
||||
match hash {
|
||||
"/active" => Mode::Active,
|
||||
"/completed" => Mode::Completed,
|
||||
_ => Mode::All,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TodoSerialized {
|
||||
pub id: usize,
|
||||
pub title: String,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl TodoSerialized {
|
||||
pub fn into_todo(self) -> Todo {
|
||||
Todo::new_with_completed(self.id, self.title, self.completed)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Todo> for TodoSerialized {
|
||||
fn from(todo: &Todo) -> Self {
|
||||
Self {
|
||||
id: todo.id,
|
||||
title: todo.title.get(),
|
||||
completed: (todo.completed)(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ static TEMPLATE: &str = r#"<main>
|
||||
</main>"#;
|
||||
|
||||
#[bench]
|
||||
fn tera_todomvc(b: &mut Bencher) {
|
||||
fn tera_todomvc_ssr(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
@@ -127,7 +127,7 @@ fn tera_todomvc(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tera_todomvc_1000(b: &mut Bencher) {
|
||||
fn tera_todomvc_ssr_1000(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
@@ -174,4 +174,4 @@ fn tera_todomvc_1000(b: &mut Bencher) {
|
||||
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,4 @@ mdbook serve
|
||||
```
|
||||
|
||||
It should be available at `http://localhost:3000`.
|
||||
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
[output.html]
|
||||
additional-css = ["./mdbook-admonish.css"]
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "3.0.1" # do not edit: managed by `mdbook-admonish install`
|
||||
|
||||
345
docs/book/mdbook-admonish.css
Normal file
345
docs/book/mdbook-admonish.css
Normal file
@@ -0,0 +1,345 @@
|
||||
@charset "UTF-8";
|
||||
:root {
|
||||
--md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
|
||||
--md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
|
||||
--md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
|
||||
--md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
|
||||
--md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
|
||||
--md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
|
||||
--md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
|
||||
--md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
|
||||
--md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
|
||||
--md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
|
||||
--md-admonition-icon--admonish-example: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
|
||||
--md-admonition-icon--admonish-quote: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
|
||||
--md-details-icon: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
|
||||
}
|
||||
|
||||
:is(.admonition) {
|
||||
display: flow-root;
|
||||
margin: 1.5625em 0;
|
||||
padding: 0 1.2rem;
|
||||
color: var(--fg);
|
||||
page-break-inside: avoid;
|
||||
background-color: var(--bg);
|
||||
border: 0 solid black;
|
||||
border-inline-start-width: 0.4rem;
|
||||
border-radius: 0.2rem;
|
||||
box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.05), 0 0 0.1rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@media print {
|
||||
:is(.admonition) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
:is(.admonition) > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:is(.admonition) :is(.admonition) {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
:is(.admonition) > .tabbed-set:only-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
html :is(.admonition) > :last-child {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
a.admonition-anchor-link {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -1.2rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
a.admonition-anchor-link:link, a.admonition-anchor-link:visited {
|
||||
color: var(--fg);
|
||||
}
|
||||
a.admonition-anchor-link:link:hover, a.admonition-anchor-link:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
a.admonition-anchor-link::before {
|
||||
content: "§";
|
||||
}
|
||||
|
||||
:is(.admonition-title, summary.admonition-title) {
|
||||
position: relative;
|
||||
min-height: 4rem;
|
||||
margin-block: 0;
|
||||
margin-inline: -1.6rem -1.2rem;
|
||||
padding-block: 0.8rem;
|
||||
padding-inline: 4.4rem 1.2rem;
|
||||
font-weight: 700;
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
display: flex;
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title) p {
|
||||
margin: 0;
|
||||
}
|
||||
html :is(.admonition-title, summary.admonition-title):last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title)::before {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-start: 1.6rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: #448aff;
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
-webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title):hover a.admonition-anchor-link {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
details.admonition > summary.admonition-title::after {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-end: 1.6rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
background-color: currentcolor;
|
||||
mask-image: var(--md-details-icon);
|
||||
-webkit-mask-image: var(--md-details-icon);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
details[open].admonition > summary.admonition-title::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-note) {
|
||||
border-color: #448aff;
|
||||
}
|
||||
|
||||
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #448aff;
|
||||
mask-image: var(--md-admonition-icon--admonish-note);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-note);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-abstract, .admonish-summary, .admonish-tldr) {
|
||||
border-color: #00b0ff;
|
||||
}
|
||||
|
||||
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 176, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b0ff;
|
||||
mask-image: var(--md-admonition-icon--admonish-abstract);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-abstract);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-info, .admonish-todo) {
|
||||
border-color: #00b8d4;
|
||||
}
|
||||
|
||||
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 184, 212, 0.1);
|
||||
}
|
||||
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b8d4;
|
||||
mask-image: var(--md-admonition-icon--admonish-info);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-info);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-tip, .admonish-hint, .admonish-important) {
|
||||
border-color: #00bfa5;
|
||||
}
|
||||
|
||||
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 191, 165, 0.1);
|
||||
}
|
||||
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00bfa5;
|
||||
mask-image: var(--md-admonition-icon--admonish-tip);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-tip);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-success, .admonish-check, .admonish-done) {
|
||||
border-color: #00c853;
|
||||
}
|
||||
|
||||
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 200, 83, 0.1);
|
||||
}
|
||||
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00c853;
|
||||
mask-image: var(--md-admonition-icon--admonish-success);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-success);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-question, .admonish-help, .admonish-faq) {
|
||||
border-color: #64dd17;
|
||||
}
|
||||
|
||||
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(100, 221, 23, 0.1);
|
||||
}
|
||||
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #64dd17;
|
||||
mask-image: var(--md-admonition-icon--admonish-question);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-question);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-warning, .admonish-caution, .admonish-attention) {
|
||||
border-color: #ff9100;
|
||||
}
|
||||
|
||||
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 145, 0, 0.1);
|
||||
}
|
||||
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff9100;
|
||||
mask-image: var(--md-admonition-icon--admonish-warning);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-warning);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-failure, .admonish-fail, .admonish-missing) {
|
||||
border-color: #ff5252;
|
||||
}
|
||||
|
||||
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 82, 82, 0.1);
|
||||
}
|
||||
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff5252;
|
||||
mask-image: var(--md-admonition-icon--admonish-failure);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-failure);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-danger, .admonish-error) {
|
||||
border-color: #ff1744;
|
||||
}
|
||||
|
||||
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 23, 68, 0.1);
|
||||
}
|
||||
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff1744;
|
||||
mask-image: var(--md-admonition-icon--admonish-danger);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-danger);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-bug) {
|
||||
border-color: #f50057;
|
||||
}
|
||||
|
||||
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(245, 0, 87, 0.1);
|
||||
}
|
||||
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #f50057;
|
||||
mask-image: var(--md-admonition-icon--admonish-bug);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-bug);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-example) {
|
||||
border-color: #7c4dff;
|
||||
}
|
||||
|
||||
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(124, 77, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #7c4dff;
|
||||
mask-image: var(--md-admonition-icon--admonish-example);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-example);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-quote, .admonish-cite) {
|
||||
border-color: #9e9e9e;
|
||||
}
|
||||
|
||||
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(158, 158, 158, 0.1);
|
||||
}
|
||||
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #9e9e9e;
|
||||
mask-image: var(--md-admonition-icon--admonish-quote);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-quote);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.navy :is(.admonition) {
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
.ayu :is(.admonition),
|
||||
.coal :is(.admonition) {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
|
||||
.rust :is(.admonition) {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.rust .admonition-anchor-link:link, .rust .admonition-anchor-link:visited {
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
# Introduction
|
||||
|
||||
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
|
||||
It will walk through the fundamental concepts you need to build applications,
|
||||
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
|
||||
It will walk through the fundamental concepts you need to build applications,
|
||||
beginning with a simple application rendered in the browser, and building toward a
|
||||
full-stack application with server-side rendering and hydration.
|
||||
|
||||
The guide doesn’t assume you know anything about fine-grained reactivity or the
|
||||
details of modern Web frameworks. It does assume you are familiar with the Rust
|
||||
The guide doesn’t assume you know anything about fine-grained reactivity or the
|
||||
details of modern Web frameworks. It does assume you are familiar with the Rust
|
||||
programming language, HTML, CSS, and the DOM and basic Web APIs.
|
||||
|
||||
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
|
||||
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
|
||||
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
|
||||
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
|
||||
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
|
||||
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
|
||||
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
|
||||
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
|
||||
understand Leptos.
|
||||
|
||||
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
|
||||
|
||||
**Important Note**: This current version of the book reflects the `0.5.1` release. The CodeSandbox versions of the examples still reflect `0.4` and earlier APIs and are in the process of being updated.
|
||||
|
||||
> The source code for the book is available [here](https://github.com/leptos-rs/leptos/tree/main/docs/book). PRs for typos or clarification are always welcome.
|
||||
|
||||
@@ -26,6 +26,8 @@ cargo init leptos-tutorial
|
||||
cargo add leptos --features=csr,nightly
|
||||
```
|
||||
|
||||
> **Note**: This version of the book reflects the Leptos 0.5 release. The CodeSandbox examples have not yet been updated from 0.4 and earlier versions.
|
||||
|
||||
Or you can leave off `nightly` if you're using stable Rust
|
||||
|
||||
```bash
|
||||
@@ -34,13 +36,23 @@ cargo add leptos --features=csr
|
||||
|
||||
> Using `nightly` Rust, and the `nightly` feature in Leptos enables the function-call syntax for signal getters and setters that is used in most of this book.
|
||||
>
|
||||
> To use `nightly` Rust, you can run
|
||||
> To use nightly Rust, you can either opt into nightly for all your Rust projects by running
|
||||
>
|
||||
> ```bash
|
||||
> rustup toolchain install nightly
|
||||
> rustup default nightly
|
||||
> ```
|
||||
>
|
||||
> or only for this project
|
||||
>
|
||||
> ```bash
|
||||
> rustup toolchain install nightly
|
||||
> cd <into your project>
|
||||
> rustup override set nightly
|
||||
> ```
|
||||
>
|
||||
> [See here for more details.](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html)
|
||||
>
|
||||
> If you’d rather use stable Rust with Leptos, you can do that too. In the guide and examples, you’ll just use the [`ReadSignal::get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) and [`WriteSignal::set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) methods instead of calling signal getters and setters as functions.
|
||||
|
||||
Make sure you've added the `wasm32-unknown-unknown` target so that Rust can compile your code to WebAssembly to run in the browser.
|
||||
@@ -65,7 +77,7 @@ And add a simple “Hello, world!” to your `main.rs`
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|| view! { <p>"Hello, world!"</p> })
|
||||
mount_to_body(|| view! { <p>"Hello, world!"</p> })
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -180,9 +180,9 @@ data flow and of fine-grained reactive updates.
|
||||
|
||||
> **Note**: There are some significant drawbacks to this approach. Both signals and memos need to own their values, so a memo will need to clone the field’s value on every change. The most natural way to manage state in a framework like Leptos is always to provide signals that are as locally-scoped and fine-grained as they can be, not to hoist everything up into global state. But when you _do_ need some kind of global state, `create_slice` can be a useful tool.
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/15-global-state-0-5-8c2ff6?file=%2Fsrc%2Fmain.rs%3A1%2C2)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/15-global-state-0-5-8c2ff6?file=%2Fsrc%2Fmain.rs%3A1%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -367,7 +367,6 @@ fn GlobalStateInput() -> impl IntoView {
|
||||
// that we created in the other component
|
||||
// neither of them will cause the other to rerun
|
||||
let (name, set_name) = create_slice(
|
||||
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
@@ -397,7 +396,6 @@ fn GlobalStateInput() -> impl IntoView {
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <Option2/><Option3/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
- [Dynamic Attributes](./view/02_dynamic_attributes.md)
|
||||
- [Components and Props](./view/03_components.md)
|
||||
- [Iteration](./view/04_iteration.md)
|
||||
- [Iterating over More Complex Data](./view/04b_iteration.md)
|
||||
- [Forms and Inputs](./view/05_forms.md)
|
||||
- [Control Flow](./view/06_control_flow.md)
|
||||
- [Error Handling](./view/07_errors.md)
|
||||
- [Parent-Child Communication](./view/08_parent_child.md)
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [No Macros: The View Builder Syntax](./view/builder.md)
|
||||
- [Reactivity](./reactivity/README.md)
|
||||
- [Working with Signals](./reactivity/working_with_signals.md)
|
||||
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
|
||||
@@ -43,6 +45,8 @@
|
||||
- [Responses and Redirects](./server/27_response.md)
|
||||
- [Progressive Enhancement and Graceful Degradation](./progressive_enhancement/README.md)
|
||||
- [`<ActionForm/>`s](./progressive_enhancement/action_form.md)
|
||||
- [Deployment](./deployment.md)
|
||||
- [Deployment](./deployment/README.md)
|
||||
- [Optimizing WASM Binary Size](./deployment/binary_size.md)
|
||||
- [Guide: Islands](./islands.md)
|
||||
- [Appendix: How Does the Reactive System Work?](./appendix_reactive_graph.md)
|
||||
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)
|
||||
- [Appendix: Some Small DX Improvements](./appendix_dx.md)
|
||||
|
||||
62
docs/book/src/appendix_dx.md
Normal file
62
docs/book/src/appendix_dx.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# A Running List of Small Developer Experience Improvements
|
||||
|
||||
## Autocompletion inside `#[component]` and `#[server]`
|
||||
|
||||
Because of the nature of macros (they can expand from anything to anything, but only if the input is exactly correct at that instant) it can be hard for rust-analyzer to do proper autocompletion and other support.
|
||||
|
||||
But you can tell rust-analyzer to ignore certain proc macros. For `#[component]` and `#[server]` especially, which annotate function bodies but don't actually transform anything inside the body of your function, this can be really helpful.
|
||||
|
||||
Note that this means that rust-analyzer doesn't know about your component props, which may generate its own set of errors or warnings in the IDE.
|
||||
|
||||
VSCode `settings.json`:
|
||||
|
||||
```json
|
||||
"rust-analyzer.procMacro.ignored": {
|
||||
"leptos_macro": [
|
||||
"component",
|
||||
"server"
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
neovim with lspconfig:
|
||||
|
||||
```lua
|
||||
require('lspconfig').rust_analyzer.setup {
|
||||
-- Other Configs ...
|
||||
settings = {
|
||||
["rust-analyzer"] = {
|
||||
-- Other Settings ...
|
||||
procMacro = {
|
||||
ignored = {
|
||||
leptos_macro = {
|
||||
"component",
|
||||
"server",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Helix, in `.helix/languages.toml`:
|
||||
|
||||
```toml
|
||||
[[language]]
|
||||
name = "rust"
|
||||
|
||||
[language-server.rust-analyzer]
|
||||
config = { procMacro = { ignored = { leptos_macro = ["component", "server"] } } }
|
||||
```
|
||||
|
||||
```admonish info
|
||||
The Jetbrains `intellij-rust` plugin (RustRover as well) currently does not support dynamic config for macro exclusion.
|
||||
However, the project currently maintains a hardcoded list of excluded macros.
|
||||
As soon as [this open PR](https://github.com/intellij-rust/intellij-rust/pull/10873) is merged, the `component` and
|
||||
`server` macro will be excluded automatically without additional configuration needed.
|
||||
|
||||
Update (2023/10/02):
|
||||
The `intellij-rust` plugin got deprecated in favor of RustRover at the same time the PR was opened, but an official
|
||||
support request was made to integrate the contents of this PR.
|
||||
```
|
||||
@@ -235,9 +235,9 @@ At the very most, you might consider memoizing the final node before running som
|
||||
|
||||
```rust
|
||||
let text = create_memo(move |_| {
|
||||
d()
|
||||
d()
|
||||
});
|
||||
create_effect(move |_| {
|
||||
engrave_text_into_bar_of_gold(&text());
|
||||
engrave_text_into_bar_of_gold(&text());
|
||||
});
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ let async_data = create_resource(
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
logging::log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
);
|
||||
@@ -30,7 +30,7 @@ To create a resource that simply runs once, you can pass a non-reactive, empty s
|
||||
let once = create_resource(|| (), |_| async move { load_data().await });
|
||||
```
|
||||
|
||||
To access the value you can use `.read()` or `.with(|data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but for any `Resource<_, T>`, they always return `Option<T>`, not `T`: because it’s always possible that your resource is still loading.
|
||||
To access the value you can use `.get()` or `.with(|data| /* */)`. These work just like `.get()` and `.with()` on a signal—`get` clones the value and returns it, `with` applies a closure to it—but for any `Resource<_, T>`, they always return `Option<T>`, not `T`: because it’s always possible that your resource is still loading.
|
||||
|
||||
So, you can show the current state of a resource in your view:
|
||||
|
||||
@@ -38,7 +38,7 @@ So, you can show the current state of a resource in your view:
|
||||
let once = create_resource(|| (), |_| async move { load_data().await });
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read() {
|
||||
{move || match once.get() {
|
||||
None => view! { <p>"Loading..."</p> }.into_view(),
|
||||
Some(data) => view! { <ShowData data/> }.into_view()
|
||||
}}
|
||||
@@ -47,9 +47,9 @@ view! {
|
||||
|
||||
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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-resources-0-5-x6h5j6?file=%2Fsrc%2Fmain.rs%3A2%2C3)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-resources-0-5-9jq86q?file=%2Fsrc%2Fmain.rs%3A2%2C3" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -74,7 +74,6 @@ fn App() -> impl IntoView {
|
||||
|
||||
// create_resource takes two arguments after its scope
|
||||
let async_data = create_resource(
|
||||
|
||||
// the first is the "source signal"
|
||||
count,
|
||||
// the second is the loader
|
||||
@@ -89,12 +88,12 @@ fn App() -> impl IntoView {
|
||||
// that doesn't depend on anything: we just load it once
|
||||
let stable = create_resource(|| (), |_| async move { load_data(1).await });
|
||||
|
||||
// we can access the resource values with .read()
|
||||
// we can access the resource values with .get()
|
||||
// this will reactively return None before the Future has resolved
|
||||
// and update to Some(T) when it has resolved
|
||||
let async_result = move || {
|
||||
async_data
|
||||
.read()
|
||||
.get()
|
||||
.map(|value| format!("Server returned {value:?}"))
|
||||
// This loading state will only show before the first load
|
||||
.unwrap_or_else(|| "Loading...".into())
|
||||
@@ -114,7 +113,7 @@ fn App() -> impl IntoView {
|
||||
"Click me"
|
||||
</button>
|
||||
<p>
|
||||
<code>"stable"</code>": " {move || stable.read()}
|
||||
<code>"stable"</code>": " {move || stable.get()}
|
||||
</p>
|
||||
<p>
|
||||
<code>"count"</code>": " {count}
|
||||
@@ -129,9 +128,8 @@ fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -4,11 +4,11 @@ In the previous chapter, we showed how you can create a simple loading screen to
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0);
|
||||
let a = create_resource(count, |count| async move { load_a(count).await });
|
||||
let once = create_resource(count, |count| async move { load_a(count).await });
|
||||
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read() {
|
||||
{move || match once.get() {
|
||||
None => view! { <p>"Loading..."</p> }.into_view(),
|
||||
Some(data) => view! { <ShowData data/> }.into_view()
|
||||
}}
|
||||
@@ -25,7 +25,7 @@ let b = create_resource(count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(), b.read()) {
|
||||
{move || match (a.get(), b.get()) {
|
||||
(Some(a), Some(b)) => view! {
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
@@ -53,12 +53,12 @@ view! {
|
||||
<h2>"My Data"</h2>
|
||||
<h3>"A"</h3>
|
||||
{move || {
|
||||
a.read()
|
||||
a.get()
|
||||
.map(|a| view! { <ShowA a/> })
|
||||
}}
|
||||
<h3>"B"</h3>
|
||||
{move || {
|
||||
b.read()
|
||||
b.get()
|
||||
.map(|b| view! { <ShowB b/> })
|
||||
}}
|
||||
</Suspense>
|
||||
@@ -89,7 +89,7 @@ view! {
|
||||
// `future` provides the `Future` to be resolved
|
||||
future=|| fetch_monkeys(3)
|
||||
// the data is bound to whatever variable name you provide
|
||||
bind:data
|
||||
let:data
|
||||
>
|
||||
// you receive the data by reference and can use it in your view here
|
||||
<p>{*data} " little monkeys, jumping on the bed."</p>
|
||||
@@ -97,9 +97,9 @@ view! {
|
||||
}
|
||||
```
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-0-5-qzpgqs?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-0-5-qzpgqs?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -141,16 +141,15 @@ fn App() -> impl IntoView {
|
||||
// and then whenever any resources has been resolved
|
||||
<p>
|
||||
"Your shouting name is "
|
||||
{move || async_data.read()}
|
||||
{move || async_data.get()}
|
||||
</p>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# `<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).
|
||||
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.Transition.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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-0-5-2jg5lz?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-0-5-2jg5lz?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -75,9 +75,8 @@ fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -91,9 +91,9 @@ view! {
|
||||
|
||||
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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/13-actions-0-5-8xk35v?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/13-actions-0-5-8xk35v?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -168,7 +168,7 @@ fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ There are as many ways to deploy a web application as there are developers, let
|
||||
|
||||
1. Remember: Always deploy Rust apps built in `--release` mode, not debug mode. This has a huge effect on both performance and binary size.
|
||||
2. Test locally in release mode as well. The framework applies certain optimizations in release mode that it does not apply in debug mode, so it’s possible for bugs to surface at this point. (If your app behaves differently or you do encounter a bug, it’s likely a framework-level bug and you should open a GitHub issue with a reproduction.)
|
||||
3. See the chapter on "Optimizing WASM Binary Size" for additional tips and tricks to further improve the time-to-interactive metric for your WASM app on first load.
|
||||
|
||||
> We asked users to submit their deployment setups to help with this chapter. I’ll quote from them below, but you can read the full thread [here](https://github.com/leptos-rs/leptos/issues/1152).
|
||||
|
||||
@@ -54,7 +55,7 @@ RUN cargo leptos build --release -vv
|
||||
|
||||
FROM rustlang/rust:nightly-bullseye as runner
|
||||
# Copy the server binary to the /app directory
|
||||
COPY --from=builder /app/target/server/release/leptos_website /app/
|
||||
COPY --from=builder /app/target/release/leptos-start /app/
|
||||
# /target/site contains our JS/WASM/CSS, etc.
|
||||
COPY --from=builder /app/target/site /app/site
|
||||
# Copy Cargo.toml if it’s needed at runtime
|
||||
@@ -63,12 +64,11 @@ WORKDIR /app
|
||||
|
||||
# Set any required env variables and
|
||||
ENV RUST_LOG="info"
|
||||
ENV APP_ENVIRONMENT="production"
|
||||
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
|
||||
ENV LEPTOS_SITE_ROOT="site"
|
||||
EXPOSE 8080
|
||||
# Run the server
|
||||
CMD ["/app/leptos_website"]
|
||||
CMD ["/app/leptos_start"]
|
||||
```
|
||||
|
||||
> Read more: [`gnu` and `musl` build files for Leptos apps](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1634916088).
|
||||
@@ -1,4 +1,4 @@
|
||||
# Appendix: Optimizing WASM Binary Size
|
||||
# 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.)
|
||||
|
||||
@@ -59,13 +59,13 @@ And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`.
|
||||
|
||||
## 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.)
|
||||
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.
|
||||
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's 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!
|
||||
|
||||
@@ -50,7 +50,6 @@ If you want to really understand the issue here, it may help to look at the expa
|
||||
|
||||
```rust
|
||||
Suspense(
|
||||
|
||||
::leptos::component_props_builder(&Suspense)
|
||||
.fallback(|| ())
|
||||
.children({
|
||||
@@ -61,7 +60,6 @@ Suspense(
|
||||
leptos::Fragment::lazy(|| {
|
||||
vec -> impl IntoView
|
||||
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> IV + 'static,
|
||||
IV: IntoView,
|
||||
|
||||
@@ -36,7 +36,7 @@ fn Home() -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
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) or with a [server-rendered `cargo-leptos` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_actix). `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
|
||||
|
||||
@@ -50,7 +50,7 @@ use stylers::style;
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let styler_class = style! { "App",
|
||||
#two{
|
||||
##two{
|
||||
color: blue;
|
||||
}
|
||||
div.one{
|
||||
|
||||
489
docs/book/src/islands.md
Normal file
489
docs/book/src/islands.md
Normal file
@@ -0,0 +1,489 @@
|
||||
# Guide: Islands
|
||||
|
||||
Leptos 0.5 introduces the new `experimental-islands` feature. This guide will walk through the islands feature and core concepts, while implementing a demo app using the islands architecture.
|
||||
|
||||
## The Islands Architecture
|
||||
|
||||
The dominant JavaScript frontend frameworks (React, Vue, Svelte, Solid, Angular) all originated as frameworks for building client-rendered single-page apps (SPAs). The initial page load is rendered to HTML, then hydrated, and subsequent navigations are handled directly in the client. (Hence “single page”: everything happens from a single page load from the server, even if there is client-side routing later.) Each of these frameworks later added server-side rendering to improve initial load times, SEO, and user experience.
|
||||
|
||||
This means that by default, the entire app is interactive. It also means that the entire app has to be shipped to the client as JavaScript in order to be hydrated. Leptos has followed this same pattern.
|
||||
|
||||
> You can read more in the chapters on [server-side rendering](./ssr/22_life_cycle.md).
|
||||
|
||||
But it’s also possible to work in the opposite direction. Rather than taking an entirely-interactive app, rendering it to HTML on the server, and then hydrating it in the browser, you can begin with a plain HTML page and add small areas of interactivity. This is the traditional format for any website or app before the 2010s: your browser makes a series of requests to the server and returns the HTML for each new page in response. After the rise of “single-page apps” (SPA), this approach has sometimes become known as a “multi-page app” (MPA) by comparison.
|
||||
|
||||
The phrase “islands architecture” has emerged recently to describe the approach of beginning with a “sea” of server-rendered HTML pages, and adding “islands” of interactivity throughout the page.
|
||||
|
||||
> ### Additional Reading
|
||||
>
|
||||
> The rest of this guide will look at how to use islands with Leptos. For more background on the approach in general, check out some of the articles below:
|
||||
>
|
||||
> - Jason Miller, [“Islands Architecture”](https://jasonformat.com/islands-architecture/), Jason Miller
|
||||
> - Ryan Carniato, [“Islands & Server Components & Resumability, Oh My!”](https://dev.to/this-is-learning/islands-server-components-resumability-oh-my-319d)
|
||||
> - [“Islands Architectures”](https://www.patterns.dev/posts/islands-architecture) on patterns.dev
|
||||
> - [Astro Islands](https://docs.astro.build/en/concepts/islands/)
|
||||
|
||||
## Activating Islands Mode
|
||||
|
||||
Let’s start with a fresh `cargo-leptos` app:
|
||||
|
||||
```bash
|
||||
cargo leptos new --git leptos-rs/start
|
||||
```
|
||||
|
||||
> I’m using Actix because I like it. Feel free to use Axum; there should be approximately no server-specific differences in this guide.
|
||||
|
||||
I’m just going to run
|
||||
|
||||
```bash
|
||||
cargo leptos build
|
||||
```
|
||||
|
||||
in the background while I fire up my editor and keep writing.
|
||||
|
||||
The first thing I’ll do is to add the `experimental-islands` feature in my `Cargo.toml`. I need to add this to both `leptos` and `leptos_actix`:
|
||||
|
||||
```toml
|
||||
leptos = { version = "0.5", features = ["nightly", "experimental-islands"] }
|
||||
leptos_actix = { version = "0.5", optional = true, features = [
|
||||
"experimental-islands",
|
||||
] }
|
||||
```
|
||||
|
||||
Next I’m going to modify the `hydrate` function exported from `src/lib.rs`. I’m going to remove the line that calls `leptos::mount_to_body(App)` and replace it with
|
||||
|
||||
```rust
|
||||
leptos::leptos_dom::HydrationCtx::stop_hydrating();
|
||||
```
|
||||
|
||||
Each “island” we create will actually act as its own entrypoint, so our `hydrate()` function just says “okay, hydration’s done now.”
|
||||
|
||||
Okay, now fire up your `cargo leptos watch` and go to [`http://localhost:3000`](http://localhost:3000) (or wherever).
|
||||
|
||||
Click the button, and...
|
||||
|
||||
Nothing happens!
|
||||
|
||||
Perfect.
|
||||
|
||||
## Using Islands
|
||||
|
||||
Nothing happens because we’ve just totally inverted the mental model of our app. Rather than being interactive by default and hydrating everything, the app is now plain HTML by default, and we need to opt into interactivity.
|
||||
|
||||
This has a big effect on WASM binary sizes: if I compile in release mode, this app is a measly 24kb of WASM (uncompressed), compared to 355kb in non-islands mode. (355kb is quite large for a “Hello, world!” It’s really just all the code related to client-side routing, which isn’t being used in the demo.)
|
||||
|
||||
When we click the button, nothing happens, because our whole page is static.
|
||||
|
||||
So how do we make something happen?
|
||||
|
||||
Let’s turn the `HomePage` component into an island!
|
||||
|
||||
Here was the non-interactive version:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
let (count, set_count) = create_signal(0);
|
||||
let on_click = move |_| set_count.update(|count| *count += 1);
|
||||
|
||||
view! {
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<button on:click=on_click>"Click Me: " {count}</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here’s the interactive version:
|
||||
|
||||
```rust
|
||||
#[island]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
let (count, set_count) = create_signal(0);
|
||||
let on_click = move |_| set_count.update(|count| *count += 1);
|
||||
|
||||
view! {
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<button on:click=on_click>"Click Me: " {count}</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now when I click the button, it works!
|
||||
|
||||
The `#[island]` macro works exactly like the `#[component]` macro, except that in islands mode, it designates this as an interactive island. If we check the binary size again, this is 166kb uncompressed in release mode; much larger than the 24kb totally static version, but much smaller than the 355kb fully-hydrated version.
|
||||
|
||||
If you open up the source for the page now, you’ll see that your `HomePage` island has been rendered as a special `<leptos-island>` HTML element which specifies which component should be used to hydrate it:
|
||||
|
||||
```html
|
||||
<leptos-island data-component="HomePage" data-hkc="0-0-0">
|
||||
<h1 data-hk="0-0-2">Welcome to Leptos!</h1>
|
||||
<button data-hk="0-0-3">
|
||||
Click Me:
|
||||
<!-- <DynChild> -->11<!-- </DynChild> -->
|
||||
</button>
|
||||
</leptos-island>
|
||||
```
|
||||
|
||||
The typical Leptos hydration keys and markers are only present inside the island, only the island is hydrated.
|
||||
|
||||
## Using Islands Effectively
|
||||
|
||||
Remember that _only_ code within an `#[island]` needs to be compiled to WASM and shipped to the browser. This means that islands should be as small and specific as possible. My `HomePage`, for example, would be better broken apart into a regular component and an island:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<Counter/>
|
||||
}
|
||||
}
|
||||
|
||||
#[island]
|
||||
fn Counter() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
let (count, set_count) = create_signal(0);
|
||||
let on_click = move |_| set_count.update(|count| *count += 1);
|
||||
|
||||
view! {
|
||||
<button on:click=on_click>"Click Me: " {count}</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now the `<h1>` doesn’t need to be included in the client bundle, or hydrated. This seems like a silly distinction now; but note that you can now add as much inert HTML content as you want to the `HomePage` itself, and the WASM binary size will remain exactly the same.
|
||||
|
||||
In regular hydration mode, your WASM binary size grows as a function of the size/complexity of your app. In islands mode, your WASM binary grows as a function of the amount of interactivity in your app. You can add as much non-interactive content as you want, outside islands, and it will not increase that binary size.
|
||||
|
||||
## Unlocking Superpowers
|
||||
|
||||
So, this 50% reduction in WASM binary size is nice. But really, what’s the point?
|
||||
|
||||
The point comes when you combine two key facts:
|
||||
|
||||
1. Code inside `#[component]` functions now _only_ runs on the server.
|
||||
2. Children and props can be passed from the server to islands, without being included in the WASM binary.
|
||||
|
||||
This means you can run server-only code directly in the body of a component, and pass it directly into the children. Certain tasks that take a complex blend of server functions and Suspense in fully-hydrated apps can be done inline in islands.
|
||||
|
||||
We’re going to rely on a third fact in the rest of this demo:
|
||||
|
||||
3. Context can be passed between otherwise-independent islands.
|
||||
|
||||
So, instead of our counter demo, let’s make something a little more fun: a tabbed interface that reads data from files on the server.
|
||||
|
||||
## Passing Server Children to Islands
|
||||
|
||||
One of the most powerful things about islands is that you can pass server-rendered children into an island, without the island needing to know anything about them. Islands hydrate their own content, but not children that are passed to them.
|
||||
|
||||
As Dan Abramov of React put it (in the very similar context of RSCs), islands aren’t really islands: they’re donuts. You can pass server-only content directly into the “donut hole,” as it were, allowing you to create tiny atolls of interactivity, surrounded on _both_ sides by the sea of inert server HTML.
|
||||
|
||||
> In the demo code included below, I added some styles to show all server content as a light-blue “sea,” and all islands as light-green “land.” Hopefully that will help picture what I’m talking about!
|
||||
|
||||
To continue with the demo: I’m going to create a `Tabs` component. Switching between tabs will require some interactivity, so of course this will be an island. Let’s start simple for now:
|
||||
|
||||
```rust
|
||||
#[island]
|
||||
fn Tabs(labels: Vec<String>) -> impl IntoView {
|
||||
let buttons = labels
|
||||
.into_iter()
|
||||
.map(|label| view! { <button>{label}</button> })
|
||||
.collect_view();
|
||||
view! {
|
||||
<div style="display: flex; width: 100%; justify-content: space-between;">
|
||||
{buttons}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Oops. This gives me an error
|
||||
|
||||
```
|
||||
error[E0463]: can't find crate for `serde`
|
||||
--> src/app.rs:43:1
|
||||
|
|
||||
43 | #[island]
|
||||
| ^^^^^^^^^ can't find crate
|
||||
```
|
||||
|
||||
Easy fix: let’s `cargo add serde --features=derive`. The `#[island]` macro wants to pull in `serde` here because it needs to serialize and deserialize the `labels` prop.
|
||||
|
||||
Now let’s update the `HomePage` to use `Tabs`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// these are the files we’re going to read
|
||||
let files = ["a.txt", "b.txt", "c.txt"];
|
||||
// the tab labels will just be the file names
|
||||
let labels = files.iter().copied().map(Into::into).collect();
|
||||
view! {
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<p>"Click any of the tabs below to read a recipe."</p>
|
||||
<Tabs labels/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you take a look in the DOM inspector, you’ll see the island is now something like
|
||||
|
||||
```html
|
||||
<leptos-island
|
||||
data-component="Tabs"
|
||||
data-hkc="0-0-0"
|
||||
data-props='{"labels":["a.txt","b.txt","c.txt"]}'
|
||||
></leptos-island>
|
||||
```
|
||||
|
||||
Our `labels` prop is getting serialized to JSON and stored in an HTML attribute so it can be used to hydrate the island.
|
||||
|
||||
Now let’s add some tabs. For the moment, a `Tab` island will be really simple:
|
||||
|
||||
```rust
|
||||
#[island]
|
||||
fn Tab(index: usize, children: Children) -> impl IntoView {
|
||||
view! {
|
||||
<div>{children()}</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each tab, for now will just be a `<div>` wrapping its children.
|
||||
|
||||
Our `Tabs` component will also get some children: for now, let’s just show them all.
|
||||
|
||||
```rust
|
||||
#[island]
|
||||
fn Tabs(labels: Vec<String>, children: Children) -> impl IntoView {
|
||||
let buttons = labels
|
||||
.into_iter()
|
||||
.map(|label| view! { <button>{label}</button> })
|
||||
.collect_view();
|
||||
view! {
|
||||
<div style="display: flex; width: 100%; justify-content: space-around;">
|
||||
{buttons}
|
||||
</div>
|
||||
{children()}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Okay, now let’s go back into the `HomePage`. We’re going to create the list of tabs to put into our tab box.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
let files = ["a.txt", "b.txt", "c.txt"];
|
||||
let labels = files.iter().copied().map(Into::into).collect();
|
||||
let tabs = move || {
|
||||
files
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, filename)| {
|
||||
let content = std::fs::read_to_string(filename).unwrap();
|
||||
view! {
|
||||
<Tab index>
|
||||
<h2>{filename.to_string()}</h2>
|
||||
<p>{content}</p>
|
||||
</Tab>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
};
|
||||
|
||||
view! {
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<p>"Click any of the tabs below to read a recipe."</p>
|
||||
<Tabs labels>
|
||||
<div>{tabs()}</div>
|
||||
</Tabs>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Uh... What?
|
||||
|
||||
If you’re used to using Leptos, you know that you just can’t do this. All code in the body of components has to run on the server (to be rendered to HTML) and in the browser (to hydrate), so you can’t just call `std::fs`; it will panic, because there’s no access to the local filesystem (and certainly not to the server filesystem!) in the browser. This would be a security nightmare!
|
||||
|
||||
Except... wait. We’re in islands mode. This `HomePage` component _really does_ only run on the server. So we can, in fact, just use ordinary server code like this.
|
||||
|
||||
> **Is this a dumb example?** Yes! Synchronously reading from three different local files in a `.map()` is not a good choice in real life. The point here is just to demonstrate that this is, definitely, server-only content.
|
||||
|
||||
Go ahead and create three files in the root of the project called `a.txt`, `b.txt`, and `c.txt`, and fill them in with whatever content you’d like.
|
||||
|
||||
Refresh the page and you should see the content in the browser. Edit the files and refresh again; it will be updated.
|
||||
|
||||
You can pass server-only content from a `#[component]` into the children of an `#[island]`, without the island needing to know anything about how to access that data or render that content.
|
||||
|
||||
**This is really important.** Passing server `children` to islands means that you can keep islands small. Ideally, you don’t want to slap and `#[island]` around a whole chunk of your page. You want to break that chunk out into an interactive piece, which can be an `#[island]`, and a bunch of additional server content that can be passed to that island as `children`, so that the non-interactive subsections of an interactive part of the page can be kept out of the WASM binary.
|
||||
|
||||
## Passing Context Between Islands
|
||||
|
||||
These aren’t really “tabs” yet: they just show every tab, all the time. So let’s add some simple logic to our `Tabs` and `Tab` components.
|
||||
|
||||
We’ll modify `Tabs` to create a simple `selected` signal. We provide the read half via context, and set the value of the signal whenever someone clicks one of our buttons.
|
||||
|
||||
```rust
|
||||
#[island]
|
||||
fn Tabs(labels: Vec<String>, children: Children) -> impl IntoView {
|
||||
let (selected, set_selected) = create_signal(0);
|
||||
provide_context(selected);
|
||||
|
||||
let buttons = labels
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, label)| view! {
|
||||
<button on:click=move |_| set_selected(index)>
|
||||
{label}
|
||||
</button>
|
||||
})
|
||||
.collect_view();
|
||||
// ...
|
||||
```
|
||||
|
||||
And let’s modify the `Tab` island to use that context to show or hide itself:
|
||||
|
||||
```rust
|
||||
#[island]
|
||||
fn Tab(children: Children) -> impl IntoView {
|
||||
let selected = expect_context::<ReadSignal<usize>>();
|
||||
view! {
|
||||
<div style:display=move || if selected() {
|
||||
"block"
|
||||
} else {
|
||||
"none"
|
||||
}>
|
||||
// ...
|
||||
```
|
||||
|
||||
Now the tabs behave exactly as I’d expect. `Tabs` passes the signal via context to each `Tab`, which uses it to determine whether it should be open or not.
|
||||
|
||||
> That’s why in `HomePage`, I made `let tabs = move ||` a function, and called it like `{tabs()}`: creating the tabs lazily this way meant that the `Tabs` island would already have provided the `selected` context by the time each `Tab` went looking for it.
|
||||
|
||||
Our complete tabs demo is about 220kb uncompressed: not the smallest demo in the world, but still about a third smaller than the counter button! Just for kicks, I built the same demo without islands mode, using `#[server]` functions and `Suspense`. and it was 429kb. So again, this was about a 50% savings in binary size. And this app includes quite minimal server-only content: remember that as we add additional server-only components and pages, this 220 will not grow.
|
||||
|
||||
## Overview
|
||||
|
||||
This demo may seem pretty basic. It is. But there are a number of immediate takeaways:
|
||||
|
||||
- **50% WASM binary size reduction**, which means measurable improvements in time to interactivity and initial load times for clients.
|
||||
- **Reduced HTML page size.** This one is less obvious, but it’s true and important: HTML generated from `#[component]`s doesn’t need all the hydration IDs and other boilerplate added.
|
||||
- **Reduced data serialization costs.** Creating a resource and reading it on the client means you need to serialize the data, so it can be used for hydration. If you’ve also read that data to create HTML in a `Suspense`, you end up with “double data,” i.e., the same exact data is both rendered to HTML and serialized as JSON, increasing the size of responses, and therefore slowing them down.
|
||||
- **Easily use server-only APIs** inside a `#[component]` as if it were a normal, native Rust function running on the server—which, in islands mode, it is!
|
||||
- **Reduced `#[server]`/`create_resource`/`Suspense` boilerplate** for loading server data.
|
||||
|
||||
## Future Exploration
|
||||
|
||||
The `experimental-islands` feature included in 0.5 reflects work at the cutting edge of what frontend web frameworks are exploring right now. As it stands, our islands approach is very similar to Astro (before its recent View Transitions support): it allows you to build a traditional server-rendered, multi-page app and pretty seamlessly integrate islands of interactivity.
|
||||
|
||||
There are some small improvements that will be easy to add. For example, we can do something very much like Astro's View Transitions approach:
|
||||
|
||||
- add client-side routing for islands apps by fetching subsequent navigations from the server and replacing the HTML document with the new one
|
||||
- add animated transitions between the old and new document using the View Transitions API
|
||||
- support explicit persistent islands, i.e., islands that you can mark with unique IDs (something like `persist:searchbar` on the component in the view), which can be copied over from the old to the new document without losing their current state
|
||||
|
||||
There are other, larger architectural changes that I’m [not sold on yet](https://github.com/leptos-rs/leptos/issues/1830).
|
||||
|
||||
## Additional Information
|
||||
|
||||
Check out the [islands PR](https://github.com/leptos-rs/leptos/pull/1660), [roadmap](https://github.com/leptos-rs/leptos/issues/1830), and [Hackernews demo](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_islands_axum) for additional discussion.
|
||||
|
||||
## Demo Code
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<main style="background-color: lightblue; padding: 10px">
|
||||
<Routes>
|
||||
<Route path="" view=HomePage/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the home page of your application.
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
let files = ["a.txt", "b.txt", "c.txt"];
|
||||
let labels = files.iter().copied().map(Into::into).collect();
|
||||
let tabs = move || {
|
||||
files
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, filename)| {
|
||||
let content = std::fs::read_to_string(filename).unwrap();
|
||||
view! {
|
||||
<Tab index>
|
||||
<div style="background-color: lightblue; padding: 10px">
|
||||
<h2>{filename.to_string()}</h2>
|
||||
<p>{content}</p>
|
||||
</div>
|
||||
</Tab>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
};
|
||||
|
||||
view! {
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<p>"Click any of the tabs below to read a recipe."</p>
|
||||
<Tabs labels>
|
||||
<div>{tabs()}</div>
|
||||
</Tabs>
|
||||
}
|
||||
}
|
||||
|
||||
#[island]
|
||||
fn Tabs(labels: Vec<String>, children: Children) -> impl IntoView {
|
||||
let (selected, set_selected) = create_signal(0);
|
||||
provide_context(selected);
|
||||
|
||||
let buttons = labels
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, label)| {
|
||||
view! {
|
||||
<button on:click=move |_| set_selected(index)>
|
||||
{label}
|
||||
</button>
|
||||
}
|
||||
})
|
||||
.collect_view();
|
||||
view! {
|
||||
<div
|
||||
style="display: flex; width: 100%; justify-content: space-around;\
|
||||
background-color: lightgreen; padding: 10px;"
|
||||
>
|
||||
{buttons}
|
||||
</div>
|
||||
{children()}
|
||||
}
|
||||
}
|
||||
|
||||
#[island]
|
||||
fn Tab(index: usize, children: Children) -> impl IntoView {
|
||||
let selected = expect_context::<ReadSignal<usize>>();
|
||||
view! {
|
||||
<div
|
||||
style:background-color="lightgreen"
|
||||
style:padding="10px"
|
||||
style:display=move || if selected() == index {
|
||||
"block"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
>
|
||||
{children()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -28,15 +28,15 @@ There’s a very simple way to determine whether you should use a capital-S `<Sc
|
||||
|
||||
## `<Body/>` and `<Html/>`
|
||||
|
||||
There are even a couple elements designed to make semantic HTML and styling easier. [`<Html/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) lets you set the `lang` and `dir` on your `<html>` tag from your application code. `<Html/>` and [`<Body/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) both have `class` props that let you set their respective `class` attributes, which is sometimes needed by CSS frameworks for styling.
|
||||
There are even a couple elements designed to make semantic HTML and styling easier. [`<Html/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) lets you set the `lang` and `dir` on your `<html>` tag from your application code. `<Html/>` and [`<Body/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Body.html) both have `class` props that let you set their respective `class` attributes, which is sometimes needed by CSS frameworks for styling.
|
||||
|
||||
`<Body/>` and `<Html/>` both also have `attributes` props which can be used to set any number of additional attributes on them via the [`AdditionalAttributes`](https://docs.rs/leptos/latest/leptos/struct.AdditionalAttributes.html) type:
|
||||
`<Body/>` and `<Html/>` both also have `attributes` props which can be used to set any number of additional attributes on them via the `attr:` syntax:
|
||||
|
||||
```rust
|
||||
<Html
|
||||
lang="he"
|
||||
dir="rtl"
|
||||
attributes=AdditionalAttributes::from(vec![("data-theme", "dark")])
|
||||
attr:data-theme="dark"
|
||||
/>
|
||||
```
|
||||
|
||||
|
||||
@@ -56,3 +56,45 @@ let on_submit = move |ev| {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Inputs
|
||||
|
||||
Server function arguments that are structs with nested serializable fields should make use of indexing notation of `serde_qs`.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
struct HeftyData {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ComplexInput() -> impl IntoView {
|
||||
let submit = Action::<VeryImportantFn, _>::server();
|
||||
|
||||
view! {
|
||||
<ActionForm action=submit>
|
||||
<input type="text" name="hefty_arg[first_name]" value="leptos"/>
|
||||
<input
|
||||
type="text"
|
||||
name="hefty_arg[last_name]"
|
||||
value="closures-everywhere"
|
||||
/>
|
||||
<input type="submit"/>
|
||||
</ActionForm>
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn very_important_fn(
|
||||
hefty_arg: HeftyData,
|
||||
) -> Result<(), ServerFnError> {
|
||||
assert_eq!(hefty_arg.first_name.as_str(), "leptos");
|
||||
assert_eq!(hefty_arg.last_name.as_str(), "closures-everywhere");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -50,7 +50,6 @@ let (use_last, set_use_last) = create_signal(true);
|
||||
// any time one of the source signals changes
|
||||
create_effect(move |_| {
|
||||
log(
|
||||
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
@@ -122,7 +121,6 @@ Like `create_resource`, `watch` takes a first argument, which is reactively trac
|
||||
let (num, set_num) = create_signal(0);
|
||||
|
||||
let stop = watch(
|
||||
|
||||
move || num.get(),
|
||||
move |num, prev_num, _| {
|
||||
log::debug!("Number: {}; Prev: {:?}", num, prev_num);
|
||||
@@ -137,9 +135,9 @@ stop(); // stop watching
|
||||
set_num.set(2); // (nothing happens)
|
||||
```
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/14-effect-0-5-d6hkch?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/14-effect-0-5-d6hkch?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -295,10 +293,8 @@ fn EffectVsDerivedSignal() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
/*#[component]
|
||||
#[component]
|
||||
pub fn Show<F, W, IV>(
|
||||
/// The scope the component is running in
|
||||
|
||||
/// The components Show wraps
|
||||
children: Box<dyn Fn() -> Fragment>,
|
||||
/// A closure that returns a bool that determines whether this thing runs
|
||||
@@ -317,17 +313,16 @@ where
|
||||
true => children().into_view(),
|
||||
false => fallback().into_view(),
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
fn log(std::fmt::Display) {
|
||||
fn log(msg: impl std::fmt::Display) {
|
||||
let log = use_context::<RwSignal<Vec<String>>>().unwrap();
|
||||
log.update(|log| log.push(msg.to_string()));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -20,7 +20,7 @@ let text = move || if count_is_odd() {
|
||||
// an effect automatically tracks the signals it depends on
|
||||
// and reruns when they change
|
||||
create_effect(move |_| {
|
||||
log!("text = {}", text());
|
||||
logging::log!("text = {}", text());
|
||||
});
|
||||
|
||||
view! {
|
||||
|
||||
@@ -16,7 +16,7 @@ Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `Wr
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count(1);
|
||||
log!(count());
|
||||
logging::log!(count());
|
||||
```
|
||||
|
||||
is the same as
|
||||
@@ -24,7 +24,7 @@ is the same as
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count.set(1);
|
||||
log!(count.get());
|
||||
logging::log!(count.get());
|
||||
```
|
||||
|
||||
You might notice that `.get()` and `.set()` can be implemented in terms of `.with()` and `.update()`. In other words, `count.get()` is identical with `count.with(|n| n.clone())`, and `count.set(1)` is implemented by doing `count.update(|n| *n = 1)`.
|
||||
@@ -63,7 +63,35 @@ if names.with(Vec::is_empty) {
|
||||
}
|
||||
```
|
||||
|
||||
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unncessary closure.
|
||||
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unnecessary closure.
|
||||
|
||||
There are some helper macros to make using `.with()` and `.update()` easier to use, especially when using multiple signals.
|
||||
|
||||
```rust
|
||||
let (first, _) = create_signal("Bob".to_string());
|
||||
let (middle, _) = create_signal("J.".to_string());
|
||||
let (last, _) = create_signal("Smith".to_string());
|
||||
```
|
||||
|
||||
If you wanted to concatenate these 3 signals together without unnecessary cloning, you would have to write something like:
|
||||
|
||||
```rust
|
||||
let name = move || {
|
||||
first.with(|first| {
|
||||
middle.with(|middle| last.with(|last| format!("{first} {middle} {last}")))
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
Which is very long and annoying to write.
|
||||
|
||||
Instead, you can use the `with!` macro to get references to all the signals at the same time.
|
||||
|
||||
```rust
|
||||
let name = move || with!(|first, middle, last| format!("{first} {middle} {last}"));
|
||||
```
|
||||
|
||||
This expands to the same thing as above. Take a look at the `with!` docs for more info, and the corresponding macros `update!`, `with_value!` and `update_value!`.
|
||||
|
||||
## Making signals depend on each other
|
||||
|
||||
@@ -80,8 +108,8 @@ let memoized_double_count = create_memo(move |_| count() * 2);
|
||||
```
|
||||
|
||||
> For guidance on whether to use a derived signal or a memo, see the docs for [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html)
|
||||
>
|
||||
> **2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C.
|
||||
|
||||
**2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C.
|
||||
|
||||
```rust
|
||||
let (first_name, set_first_name) = create_signal("Bridget".to_string());
|
||||
|
||||
@@ -101,3 +101,44 @@ Now if you navigate to `/` or to `/users` you’ll get the home page or the `<Us
|
||||
Note that you can define your routes in any order. The router scores each route to see how good a match it is, rather than simply trying to match them top to bottom.
|
||||
|
||||
Simple enough?
|
||||
|
||||
## Conditional Routes
|
||||
|
||||
`leptos_router` is based on the assumption that you have one and only one `<Routes/>` component in your app. It uses this to generate routes on the server side, optimize route matching by caching calculated branches, and render your application.
|
||||
|
||||
You should not conditionally render `<Routes/>` using another component like `<Show/>` or `<Suspense/>`.
|
||||
|
||||
```rust
|
||||
// ❌ don't do this!
|
||||
view! {
|
||||
<Show when=|| is_loaded() fallback=|| view! { <p>"Loading"</p> }>
|
||||
<Routes>
|
||||
<Route path="/" view=Home/>
|
||||
</Routes>
|
||||
</Show>
|
||||
}
|
||||
```
|
||||
|
||||
Instead, you can use nested routing to render your `<Routes/>` once, and conditionally render the router outlet:
|
||||
|
||||
```rust
|
||||
// ✅ do this instead!
|
||||
view! {
|
||||
<Routes>
|
||||
// parent route
|
||||
<Route path="/" view=move || {
|
||||
view! {
|
||||
// only show the outlet if data have loaded
|
||||
<Show when=|| is_loaded() fallback=|| view! { <p>"Loading"</p> }>
|
||||
<Outlet/>
|
||||
</Show>
|
||||
}
|
||||
}>
|
||||
// nested child route
|
||||
<Route path="/" view=Home/>
|
||||
</Route>
|
||||
</Routes>
|
||||
}
|
||||
```
|
||||
|
||||
If this looks bizarre, don’t worry! The next section of the book is about this kind of nested routing.
|
||||
|
||||
@@ -143,8 +143,8 @@ pub fn ContactList() -> impl IntoView {
|
||||
// the contact list
|
||||
<For each=contacts
|
||||
key=|contact| contact.id
|
||||
view=|contact| todo!()
|
||||
>
|
||||
children=|contact| todo!()
|
||||
/>
|
||||
// the nested child, if any
|
||||
// don’t forget this!
|
||||
<Outlet/>
|
||||
@@ -153,6 +153,43 @@ pub fn ContactList() -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
## Refactoring Route Definitions
|
||||
|
||||
You don’t need to define all your routes in one place if you don’t want to. You can refactor any `<Route/>` and its children out into a separate component.
|
||||
|
||||
For example, you can refactor the example above to use two separate components:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/contacts" view=ContactList>
|
||||
<ContactInfoRoutes/>
|
||||
<Route path="" view=|| view! {
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component(transparent)]
|
||||
fn ContactInfoRoutes() -> impl IntoView {
|
||||
view! {
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=EmailAndPhone/>
|
||||
<Route path="address" view=Address/>
|
||||
<Route path="messages" view=Messages/>
|
||||
</Route>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This second component is a `#[component(transparent)]`, meaning it just returns its data, not a view: in this case, it's a [`RouteDefinition`](https://docs.rs/leptos_router/latest/leptos_router/struct.RouteDefinition.html) struct, which is what the `<Route/>` returns. As long as it is marked `#[component(transparent)]`, this sub-route can be defined wherever you want, and inserted as a component into your tree of route definitions.
|
||||
|
||||
## Nested Routing and Performance
|
||||
|
||||
All of this is nice, conceptually, but again—what’s the big deal?
|
||||
@@ -167,9 +204,9 @@ In fact, in this case, we don’t even need to rerender the `<Contact/>` compone
|
||||
|
||||
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple we’ll cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so don’t be surprised if there’s anything you don’t understand.
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -279,9 +316,8 @@ fn ContactInfo() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -82,9 +82,9 @@ This can get a little messy: deriving a signal that wraps an `Option<_>` or `Res
|
||||
|
||||
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explained them all yet.
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -117,7 +117,7 @@ fn App() -> impl IntoView {
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=ContactList
|
||||
>
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|| view! {
|
||||
@@ -194,9 +194,8 @@ fn ContactInfo() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -35,9 +35,9 @@ The second argument here is a set of [`NavigateOptions`](https://docs.rs/leptos_
|
||||
|
||||
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -70,7 +70,7 @@ fn App() -> impl IntoView {
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=ContactList
|
||||
>
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|| view! {
|
||||
@@ -147,9 +147,8 @@ fn ContactInfo() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -34,7 +34,7 @@ pub fn FormExample() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search/>
|
||||
<input type="search" name="q" value=search/>
|
||||
<input type="submit"/>
|
||||
</Form>
|
||||
<Transition fallback=move || ()>
|
||||
@@ -53,7 +53,7 @@ We can actually take it a step further and do something kind of clever:
|
||||
```rust
|
||||
view! {
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search
|
||||
<input type="search" name="q" value=search
|
||||
oninput="this.form.requestSubmit()"
|
||||
/>
|
||||
</Form>
|
||||
@@ -62,9 +62,9 @@ view! {
|
||||
|
||||
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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/20-form-0-5-9g7v9p?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/20-form-0-5-9g7v9p?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -172,9 +172,8 @@ pub fn FormExample() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -33,7 +33,6 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
#[component]
|
||||
pub fn BusyButton() -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<button on:click=move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
@@ -70,6 +69,18 @@ There are a few things to note about the way you define a server function, too.
|
||||
- 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 URL Prefixes
|
||||
|
||||
You can optionally define a specific URL prefix to be used in the definition of the server function.
|
||||
This is done by providing an optional 2nd argument to the `#[server]` macro.
|
||||
By default the URL prefix will be `/api`, if not specified.
|
||||
Here are some examples:
|
||||
|
||||
```rust
|
||||
#[server(AddTodo)] // will use the default URL prefix of `/api`
|
||||
#[server(AddTodo, "/foo")] // will use the URL prefix of `/foo`
|
||||
```
|
||||
|
||||
## 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:
|
||||
@@ -105,6 +116,21 @@ In other words, you have two choices:
|
||||
>
|
||||
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
|
||||
|
||||
|
||||
## Server Functions Endpoint Paths
|
||||
|
||||
By default, a unique path will be generated. You can optionally define a specific endpoint path to be used in the URL. This is done by providing an optional 4th argument to the `#[server]` macro. Leptos will generate the complete path by concatenating the URL prefix (2nd argument) and the endpoint path (4th argument).
|
||||
For example,
|
||||
|
||||
```rust
|
||||
#[server(MyServerFnType, "/api", "Url", "hello")]
|
||||
```
|
||||
will generate a server function endpoint at `/api/hello` that accepts a POST request.
|
||||
|
||||
> **Can I use the same server function endpoint path with multiple encodings?**
|
||||
>
|
||||
> No. Different server functions must have unique paths. The `#[server]` macro automatically generates unique paths, but you need to be careful if you choose to specify the complete path manually, as the server looks up server functions by their path.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -35,7 +35,6 @@ Here’s a simplified example from our [`session_auth_axum` example](https://git
|
||||
```rust
|
||||
#[server(Login, "/api")]
|
||||
pub async fn login(
|
||||
|
||||
username: String,
|
||||
password: String,
|
||||
remember: Option<String>,
|
||||
|
||||
@@ -8,12 +8,12 @@ If you’ve ever listened to streaming music or watched a video online, I’m su
|
||||
|
||||
Let me say a little more about what I mean.
|
||||
|
||||
Leptos supports all four different modes of rendering HTML that includes asynchronous data:
|
||||
Leptos supports all the major ways of rendering HTML that includes asynchronous data:
|
||||
|
||||
1. [Synchronous Rendering](#synchronous-rendering)
|
||||
1. [Async Rendering](#async-rendering)
|
||||
1. [In-Order streaming](#in-order-streaming)
|
||||
1. [Out-of-Order Streaming](#out-of-order-streaming)
|
||||
1. [Out-of-Order Streaming](#out-of-order-streaming) (and a partially-blocked variant)
|
||||
|
||||
## Synchronous Rendering
|
||||
|
||||
@@ -67,7 +67,7 @@ If you’re using server-side rendering, the synchronous mode is almost never wh
|
||||
- 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.)
|
||||
|
||||
5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. If one of them reads from one or more “blocking resources” (see below), the fallback will not be sent; rather, the server will wait until that `<Suspense/>` has resolved and then replace the fallback with the resolved fragment on the server, which means that it is included in the initial HTML response and appears even if JavaScript is disabled or not supported. Other `<Suspense/>` stream in out of order as usual.
|
||||
5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. It is triggered by setting `ssr=SsrMode::PartiallyBlocked` on a route, and depending on blocking resources within the view. If one of the `<Suspense/>` components reads from one or more “blocking resources” (see below), the fallback will not be sent; rather, the server will wait until that `<Suspense/>` has resolved and then replace the fallback with the resolved fragment on the server, which means that it is included in the initial HTML response and appears even if JavaScript is disabled or not supported. Other `<Suspense/>` stream in out of order, similar to the `SsrMode::OutOfOrder` default.
|
||||
|
||||
This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is _not_ useful if there’s only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering.
|
||||
|
||||
@@ -134,4 +134,23 @@ pub fn BlogPost() -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. Meta tags and other head elements awaiting the blocking resource will be rendered before the stream is sent.
|
||||
|
||||
Combined with the following route definition, which uses `SsrMode::PartiallyBlocked`, the blocking resource will be fully rendered on the server side, making it accessible to users who disable WebAssembly or JavaScript.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=HomePage/>
|
||||
|
||||
// We'll load the posts with async rendering, so they can set
|
||||
// the title and metadata *after* loading the data
|
||||
<Route
|
||||
path="/post/:id"
|
||||
view=Post
|
||||
ssr=SsrMode::PartiallyBlocked
|
||||
/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -9,7 +9,7 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
leptos::log!("where do I run?");
|
||||
logging::log!("where do I run?");
|
||||
// ... whatever
|
||||
}
|
||||
```
|
||||
@@ -87,43 +87,6 @@ The WASM version of your app, running in the browser, expects to find three item
|
||||
|
||||
It’s pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If you’re seeing warnings like this and you don’t think it’s your fault, it’s much more likely that it’s a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
|
||||
|
||||
### Mutating the DOM during rendering
|
||||
|
||||
This is a slightly more common way to create a client/server mismatch: updating a signal _during rendering_ in a way that mutates the view.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (loaded, set_loaded) = create_signal(false);
|
||||
|
||||
// create_effect only runs on the client
|
||||
create_effect(move |_| {
|
||||
// do something like reading from localStorage
|
||||
set_loaded(true);
|
||||
});
|
||||
|
||||
move || {
|
||||
if loaded() {
|
||||
view! { <p>"Hello, world!"</p> }.into_any()
|
||||
} else {
|
||||
view! { <div class="loading">"Loading..."</div> }.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This one gives us the scary panic
|
||||
|
||||
```
|
||||
panicked at 'assertion failed: `(left == right)`
|
||||
left: `"DIV"`,
|
||||
right: `"P"`: SSR and CSR elements have the same hydration key but different node kinds.
|
||||
```
|
||||
|
||||
And a handy link to this page!
|
||||
|
||||
The problem here is that `create_effect` runs **immediately** and **synchronously**, but only in the browser. As a result, on the server, `loaded` is false, and a `<div>` is rendered. But on the browser, by the time the view is being rendered, `loaded` has already been set to `true`, and the browser is expecting to find a `<p>`.
|
||||
|
||||
#### Solution
|
||||
|
||||
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.
|
||||
@@ -166,7 +129,7 @@ For example, say that I want to store something in the browser’s `localStorage
|
||||
pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
logging::log!("{storage:?}");
|
||||
}
|
||||
```
|
||||
|
||||
@@ -180,7 +143,7 @@ pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
create_effect(move |_| {
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
logging::log!("{storage:?}");
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -195,4 +158,4 @@ In particular, you’ll sometimes see errors about the crate `mio` or missing th
|
||||
|
||||
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).)
|
||||
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/latest/leptos_server/index.html).)
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Todos {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
fn test_remaining() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,6 @@ fn clear() {
|
||||
|
||||
clear.click();
|
||||
|
||||
```rust
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
@@ -108,6 +107,7 @@ assert_eq!(
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
}
|
||||
````
|
||||
|
||||
### [`wasm-bindgen-test` with `counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable/tests/web)
|
||||
|
||||
@@ -156,9 +156,9 @@ You can see here that while `set_count` just sets the value, `set_count.update()
|
||||
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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
|
||||
@@ -145,9 +145,10 @@ Derived signals let you create reactive computed values that can be used in mult
|
||||
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.
|
||||
signal change (when `count()` changes) and once 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 re designed to solve this problem
|
||||
for expensive calculations.
|
||||
|
||||
> #### Advanced Topic: Injecting Raw HTML
|
||||
>
|
||||
@@ -166,9 +167,9 @@ are designed to solve this problem for expensive calculations.
|
||||
>
|
||||
> [Click here for the full `view` macros docs](https://docs.rs/leptos/latest/leptos/macro.view.html).
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attributes-0-5-lwdrpm?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attributes-0-5-lwdrpm?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>Code Sandbox Source</summary>
|
||||
@@ -227,7 +228,33 @@ fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
// passing a function to an attribute
|
||||
// reactively sets that attribute
|
||||
// signals are functions, so this <=> `move || count.get()`
|
||||
value=count
|
||||
>
|
||||
</progress>
|
||||
<br/>
|
||||
|
||||
// This progress bar will use `double_count`
|
||||
// so it should move twice as fast!
|
||||
<progress
|
||||
max="50"
|
||||
// derived signals are functions, so they can also
|
||||
// reactive update the DOM
|
||||
value=double_count
|
||||
>
|
||||
</progress>
|
||||
<p>"Count: " {count}</p>
|
||||
<p>"Double Count: " {double_count}</p>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -35,9 +35,7 @@ Instead, let’s create a `<ProgressBar/>` component.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
) -> impl IntoView {
|
||||
fn ProgressBar() -> impl IntoView {
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
@@ -64,7 +62,6 @@ In Leptos, you define props by giving additional arguments to the component func
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
@@ -118,7 +115,6 @@ argument to the component function with `#[prop(optional)]`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
@@ -149,7 +145,6 @@ with `#[prop(default = ...)`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: ReadSignal<i32>
|
||||
@@ -199,7 +194,6 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar<F>(
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
@@ -254,7 +248,6 @@ reactive value.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
@@ -373,7 +366,6 @@ component function, and each one of the props:
|
||||
/// Shows progress toward a goal.
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
|
||||
/// The maximum value of the progress bar.
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
@@ -412,9 +404,9 @@ and see the power of the `#[component]` macro combined with rust-analyzer here.
|
||||
> In general, you should not need to use transparent components unless you are
|
||||
> creating custom wrapping components that fall into one of these two categories.
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-0-5-5vvl69?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/3-components-0-5-5vvl69?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -481,7 +473,7 @@ fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ 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:
|
||||
Leptos supports two different patterns for iterating over items:
|
||||
|
||||
1. For static views: `Vec<_>`
|
||||
2. For dynamic lists: `<For/>`
|
||||
@@ -51,7 +51,8 @@ The fact that the _list_ is static doesn’t mean the interface needs to be stat
|
||||
You can render dynamic items as part of a static list.
|
||||
|
||||
```rust
|
||||
// create a list of N signals
|
||||
// create a list of 5 signals
|
||||
let length = 5;
|
||||
let counters = (1..=length).map(|idx| create_signal(idx));
|
||||
|
||||
// each item manages a reactive view
|
||||
@@ -86,7 +87,7 @@ 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
|
||||
- `children`: 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
|
||||
@@ -103,9 +104,9 @@ it is generated, and using that as an ID for the key function.
|
||||
|
||||
Check out the `<DynamicList/>` component below for an example.
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-0-5-pwdn2y?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-0-5-pwdn2y?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -136,7 +137,6 @@ fn App() -> impl IntoView {
|
||||
/// to add or remove any.
|
||||
#[component]
|
||||
fn StaticList(
|
||||
|
||||
/// How many counters to include in this list.
|
||||
length: usize,
|
||||
) -> impl IntoView {
|
||||
@@ -172,7 +172,6 @@ fn StaticList(
|
||||
/// remove counters.
|
||||
#[component]
|
||||
fn DynamicList(
|
||||
|
||||
/// The number of counters to begin with.
|
||||
initial_length: usize,
|
||||
) -> impl IntoView {
|
||||
@@ -229,9 +228,9 @@ fn DynamicList(
|
||||
// can only grow, because moving items around inside the list
|
||||
// means their indices will change and they will all rerender
|
||||
key=|counter| counter.0
|
||||
// the view function receives each item from your `each` iterator
|
||||
// `children` receives each item from your `each` iterator
|
||||
// and returns a view
|
||||
view=move |(id, (count, set_count))| {
|
||||
children=move |(id, (count, set_count))| {
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
@@ -258,9 +257,8 @@ fn DynamicList(
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
278
docs/book/src/view/04b_iteration.md
Normal file
278
docs/book/src/view/04b_iteration.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# Iterating over More Complex Data with `<For/>`
|
||||
|
||||
This chapter goes into iteration over nested data structures in a bit
|
||||
more depth. It belongs here with the other chapter on iteration, but feel
|
||||
free to skip it and come back if you’d like to stick with simpler subjects
|
||||
for now.
|
||||
|
||||
## The Problem
|
||||
|
||||
I just said that the framework does not rerender any of the items in one of the
|
||||
rows, unless the key has changed. This probably makes sense at first, but it can
|
||||
easily trip you up.
|
||||
|
||||
Let’s consider an example in which each of the items in our row is some data structure.
|
||||
Imagine, for example, that the items come from some JSON array of keys and values:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
struct DatabaseEntry {
|
||||
key: String,
|
||||
value: i32,
|
||||
}
|
||||
```
|
||||
|
||||
Let’s define a simple component that will iterate over the rows and display each one:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// start with a set of three rows
|
||||
let (data, set_data) = create_signal(vec![
|
||||
DatabaseEntry {
|
||||
key: "foo".to_string(),
|
||||
value: 10,
|
||||
},
|
||||
DatabaseEntry {
|
||||
key: "bar".to_string(),
|
||||
value: 20,
|
||||
},
|
||||
DatabaseEntry {
|
||||
key: "baz".to_string(),
|
||||
value: 15,
|
||||
},
|
||||
]);
|
||||
view! {
|
||||
// when we click, update each row,
|
||||
// doubling its value
|
||||
<button on:click=move |_| {
|
||||
set_data.update(|data| {
|
||||
for row in data {
|
||||
row.value *= 2;
|
||||
}
|
||||
});
|
||||
// log the new value of the signal
|
||||
logging::log!("{:?}", data.get());
|
||||
}>
|
||||
"Update Values"
|
||||
</button>
|
||||
// iterate over the rows and display each value
|
||||
<For
|
||||
each=data
|
||||
key=|state| state.key.clone()
|
||||
let:child
|
||||
>
|
||||
<p>{child.value}</p>
|
||||
</For>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Note the `let:child` syntax here. In the previous chapter we introduced `<For/>`
|
||||
> with a `children` prop. We can actually create this value directly in the children
|
||||
> of the `<For/>` component, without breaking out of the `view` macro: the `let:child`
|
||||
> combined with `<p>{child.value}</p>` above is the equivalent of
|
||||
>
|
||||
> ```rust
|
||||
> children=|child| view! { <p>{child.value}</p> }
|
||||
> ```
|
||||
|
||||
When you click the `Update Values` button... nothing happens. Or rather:
|
||||
the signal is updated, the new value is logged, but the `{child.value}`
|
||||
for each row doesn’t update.
|
||||
|
||||
Let’s see: is that because we forgot to add a closure to make it reactive?
|
||||
Let’s try `{move || child.value}`.
|
||||
|
||||
...Nope. Still nothing.
|
||||
|
||||
Here’s the problem: as I said, each row is only rerendered when the key changes.
|
||||
We’ve updated the value for each row, but not the key for any of the rows, so
|
||||
nothing has rerendered. And if you look at the type of `child.value`, it’s a plain
|
||||
`i32`, not a reactive `ReadSignal<i32>` or something. This means that even if we
|
||||
wrap a closure around it, the value in this row will never update.
|
||||
|
||||
We have three possible solutions:
|
||||
|
||||
1. change the `key` so that it always updates when the data structure changes
|
||||
2. change the `value` so that it’s reactive
|
||||
3. take a reactive slice of the data structure instead of using each row directly
|
||||
|
||||
## Option 1: Change the Key
|
||||
|
||||
Each row is only rerendered when the key changes. Our rows above didn’t rerender,
|
||||
because the key didn’t change. So: why not just force the key to change?
|
||||
|
||||
```rust
|
||||
<For
|
||||
each=data
|
||||
key=|state| (state.key.clone(), state.value)
|
||||
let:child
|
||||
>
|
||||
<p>{child.value}</p>
|
||||
</For>
|
||||
```
|
||||
|
||||
Now we include both the key and the value in the `key`. This means that whenever the
|
||||
value of a row changes, `<For/>` will treat it as if it’s an entirely new row, and
|
||||
replace the previous one.
|
||||
|
||||
### Pros
|
||||
|
||||
This is very easy. We can make it even easier by deriving `PartialEq`, `Eq`, and `Hash`
|
||||
on `DatabaseEntry`, in which case we could just `key=|state| state.clone()`.
|
||||
|
||||
### Cons
|
||||
|
||||
**This is the least efficient of the three options.** Every time the value of a row
|
||||
changes, it throws out the previous `<p>` element and replaces it with an entirely new
|
||||
one. Rather than making a fine-grained update to the text node, in other words, it really
|
||||
does rerender the entire row on every change, and this is expensive in proportion to how
|
||||
complex the UI of the row is.
|
||||
|
||||
You’ll notice we also end up cloning the whole data structure so that `<For/>` can hold
|
||||
onto a copy of the key. For more complex structures, this can become a bad idea fast!
|
||||
|
||||
## Option 2: Nested Signals
|
||||
|
||||
If we do want that fine-grained reactivity for the value, one option is to wrap the `value`
|
||||
of each row in a signal.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
struct DatabaseEntry {
|
||||
key: String,
|
||||
value: RwSignal<i32>,
|
||||
}
|
||||
```
|
||||
|
||||
`RwSignal<_>` is a “read-write signal,” which combines the getter and setter in one object.
|
||||
I’m using it here because it’s a little easier to store in a struct than separate getters
|
||||
and setters.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// start with a set of three rows
|
||||
let (data, set_data) = create_signal(vec![
|
||||
DatabaseEntry {
|
||||
key: "foo".to_string(),
|
||||
value: create_rw_signal(10),
|
||||
},
|
||||
DatabaseEntry {
|
||||
key: "bar".to_string(),
|
||||
value: create_rw_signal(20),
|
||||
},
|
||||
DatabaseEntry {
|
||||
key: "baz".to_string(),
|
||||
value: create_rw_signal(15),
|
||||
},
|
||||
]);
|
||||
view! {
|
||||
// when we click, update each row,
|
||||
// doubling its value
|
||||
<button on:click=move |_| {
|
||||
data.with(|data| {
|
||||
for row in data {
|
||||
row.value.update(|value| *value *= 2);
|
||||
}
|
||||
});
|
||||
// log the new value of the signal
|
||||
logging::log!("{:?}", data.get());
|
||||
}>
|
||||
"Update Values"
|
||||
</button>
|
||||
// iterate over the rows and display each value
|
||||
<For
|
||||
each=data
|
||||
key=|state| state.key.clone()
|
||||
let:child
|
||||
>
|
||||
<p>{child.value}</p>
|
||||
</For>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This version works! And if you look in the DOM inspector in your browser, you’ll
|
||||
see that unlike in the previous version, in this version only the individual text
|
||||
nodes are updated. Passing the signal directly into `{child.value}` works, as
|
||||
signals do keep their reactivity if you pass them into the view.
|
||||
|
||||
Note that I changed the `set_data.update()` to a `data.with()`. `.with()` is the
|
||||
non-cloning way of accessing a signal’s value. In this case, we are only updating
|
||||
the internal values, not updating the list of values: because signals maintain their
|
||||
own state, we don’t actual need to update the `data` signal at all, so the immutable
|
||||
`.with()` is fine here.
|
||||
|
||||
> In fact, this version doesn’t update `data`, so the `<For/>` is essentially a static
|
||||
> list as in the last chapter, and this could just be a plain iterator. But the `<For/>`
|
||||
> is useful if we want to add or remove rows in the future.
|
||||
|
||||
### Pros
|
||||
|
||||
This is the most efficient option, and fits directly with the rest of the mental model
|
||||
of the framework: values that change over time are wrapped in signals so the interface
|
||||
can respond to them.
|
||||
|
||||
### Cons
|
||||
|
||||
Nested reactivity can be cumbersome if you’re receiving data from an API or another
|
||||
data source you don’t control, and you don’t want to create a different struct wrapping
|
||||
each field in a signal.
|
||||
|
||||
## Option 3: Memoized Slices
|
||||
|
||||
Leptos provides a primitive called [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html),
|
||||
which creates a derived computation that only triggers a reactive update when its value
|
||||
has changed.
|
||||
|
||||
This allows you to create reactive values for subfields of a larger data structure,
|
||||
without needing to wrap the fields of that structure in signals.
|
||||
|
||||
Most of the application can remain the same as the initial (broken) version, but the `<For/>`
|
||||
will be updated to this:
|
||||
|
||||
```rust
|
||||
<For
|
||||
each=move || data().into_iter().enumerate()
|
||||
key=|(_, state)| state.key.clone()
|
||||
children=move |(index, _)| {
|
||||
let value = create_memo(move |_| {
|
||||
data.with(|data| data.get(index).map(|d| d.value).unwrap_or(0))
|
||||
});
|
||||
view! {
|
||||
<p>{value}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
You’ll notice a few differences here:
|
||||
|
||||
- we convert the `data` signal into an enumerated iterator
|
||||
- we use the `children` prop explicitly, to make it easier to run some non-`view` code
|
||||
- we define a `value` memo and use that in the view. This `value` field doesn’t actually
|
||||
use the `child` being passed into each row. Instead, it uses the index and reaches back
|
||||
into the original `data` to get the value.
|
||||
|
||||
Every time `data` changes, now, each memo will be recalculated. If its value has changed,
|
||||
it will update its text node, without rerendering the whole row.
|
||||
|
||||
## Pros
|
||||
|
||||
We get the same fine-grained reactivity of the signal-wrapped version, without needing to
|
||||
wrap the data in signals.
|
||||
|
||||
## Cons
|
||||
|
||||
It’s a bit more complex to set up this memo-per-row inside the `<For/>` loop rather than
|
||||
using nested signals. For example, you’ll notice that we have to guard against the possibility
|
||||
that the `data[index]` would panic by using `data.get(index)`, because this memo may be
|
||||
triggered to re-run once just after the row is removed. (This is because the memo for each row
|
||||
and the whole `<For/>` both depend on the same `data` signal, and the order of execution for
|
||||
multiple reactive values that depend on the same signal isn’t guaranteed.)
|
||||
|
||||
Note also that while memos memoize their reactive changes, the same
|
||||
calculation does need to re-run to check the value every time, so nested reactive signals
|
||||
will still be more efficient for pinpoint updates here.
|
||||
@@ -19,7 +19,7 @@ There are two important things to remember:
|
||||
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. (The same is true for `checked` and `prop:checked`
|
||||
`prop:value` for this reason. (The same is true for `checked` and `prop:checked`
|
||||
on an `<input type="checkbox">`.)
|
||||
|
||||
```rust
|
||||
@@ -44,28 +44,28 @@ view! {
|
||||
```
|
||||
|
||||
> #### Why do you need `prop:value`?
|
||||
>
|
||||
>
|
||||
> Web browsers are the most ubiquitous and stable platform for rendering graphical user interfaces in existence. They have also maintained an incredible backwards compatibility over their three decades of existence. Inevitably, this means there are some quirks.
|
||||
>
|
||||
>
|
||||
> One odd quirk is that there is a distinction between HTML attributes and DOM element properties, i.e., between something called an “attribute” which is parsed from HTML and can be set on a DOM element with `.setAttribute()`, and something called a “property” which is a field of the JavaScript class representation of that parsed HTML element.
|
||||
>
|
||||
> In the case of an `<input value=...>`, setting the `value` *attribute* is defined as setting the initial value for the input, and setting `value` *property* sets its current value. It maybe easiest to understand this by opening `about:blank` and running the following JavaScript in the browser console, line by line:
|
||||
>
|
||||
> In the case of an `<input value=...>`, setting the `value` _attribute_ is defined as setting the initial value for the input, and setting `value` _property_ sets its current value. It maybe easiest to understand this by opening `about:blank` and running the following JavaScript in the browser console, line by line:
|
||||
>
|
||||
> ```js
|
||||
> // create an input and append it to the DOM
|
||||
> const el = document.createElement("input")
|
||||
> document.body.appendChild(el)
|
||||
>
|
||||
> el.setAttribute("value", "test") // updates the input
|
||||
> el.setAttribute("value", "another test") // updates the input again
|
||||
>
|
||||
> const el = document.createElement("input");
|
||||
> document.body.appendChild(el);
|
||||
>
|
||||
> el.setAttribute("value", "test"); // updates the input
|
||||
> el.setAttribute("value", "another test"); // updates the input again
|
||||
>
|
||||
> // now go and type into the input: delete some characters, etc.
|
||||
>
|
||||
> el.setAttribute("value", "one more time?")
|
||||
>
|
||||
> el.setAttribute("value", "one more time?");
|
||||
> // nothing should have changed. setting the "initial value" does nothing now
|
||||
>
|
||||
>
|
||||
> // however...
|
||||
> el.value = "But this works"
|
||||
> el.value = "But this works";
|
||||
> ```
|
||||
>
|
||||
> Many other frontend frameworks conflate attributes and properties, or create a special case for inputs that sets the value correctly. Maybe Leptos should do this too; but for now, I prefer giving users the maximum amount of control over whether they’re setting an attribute or a property, and doing my best to educate people about the actual underlying browser behavior rather than obscuring it.
|
||||
@@ -137,9 +137,9 @@ The view should be pretty self-explanatory by now. Note two things:
|
||||
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.)
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-forms-0-5-rf2t7c?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/5-forms-0-5-rf2t7c?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -242,9 +242,8 @@ fn UncontrolledComponent() -> impl IntoView {
|
||||
// Because we defined it as `fn App`, we can now use it in a
|
||||
// template as <App/>
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -148,10 +148,10 @@ 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());
|
||||
logging::log!("{}: rendering Big", value());
|
||||
"Big"
|
||||
} else {
|
||||
log!("{}: rendering Small", value());
|
||||
logging::log!("{}: rendering Small", value());
|
||||
"Small"
|
||||
};
|
||||
```
|
||||
@@ -283,9 +283,9 @@ view! {
|
||||
}
|
||||
```
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-0-5-4yn7qz?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-0-5-4yn7qz?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -376,9 +376,8 @@ fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -110,9 +110,9 @@ 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.
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-errors-0-5-5mptv9?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/7-errors-0-5-5mptv9?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -167,9 +167,8 @@ fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -72,12 +72,7 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonB<F>(
|
||||
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
pub fn ButtonB(#[prop(into)] on_click: Callback<MouseEvent>) -> impl IntoView
|
||||
{
|
||||
view! {
|
||||
<button on:click=on_click>
|
||||
@@ -93,10 +88,49 @@ of keeping local state local, preventing the problem of spaghetti mutation. But
|
||||
the logic to mutate that signal needs to exist up in `<App/>`, not down in `<ButtonB/>`. These
|
||||
are real trade-offs, not a simple right-or-wrong choice.
|
||||
|
||||
> Note the way we use the `Callback<In, Out>` type. This is basically a
|
||||
> wrapper around a closure `Fn(In) -> Out` that is also `Copy` and makes it
|
||||
> easy to pass around.
|
||||
>
|
||||
> We also used the `#[prop(into)]` attribute so we can pass a normal closure into
|
||||
> `on_click`. Please see the [chapter "`into` Props"](./03_components.md#into-props) for more details.
|
||||
|
||||
### 2.1 Use Closure instead of `Callback`
|
||||
|
||||
You can use a Rust closure `Fn(MouseEvent)` directly instead of `Callback`:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonB<F>(on_click: F) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static
|
||||
{
|
||||
view! {
|
||||
<button on:click=on_click>
|
||||
"Toggle"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The code is very similar in this case. On more advanced use-cases using a
|
||||
closure might require some cloning compared to using a `Callback`.
|
||||
|
||||
> Note the way we declare the generic type `F` here for the callback. If you’re
|
||||
> confused, look back at the [generic props](./03_components.html#generic-props) section
|
||||
> of the chapter on components.
|
||||
|
||||
|
||||
## 3. Use an Event Listener
|
||||
|
||||
You can actually write Option 2 in a slightly different way. If the callback maps directly onto
|
||||
@@ -191,7 +225,7 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(d: WriteSignal<bool>) -> impl IntoView {
|
||||
pub fn Layout(set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! {
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
@@ -203,7 +237,7 @@ pub fn Layout(d: WriteSignal<bool>) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(d: WriteSignal<bool>) -> impl IntoView {
|
||||
pub fn Content(set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! {
|
||||
<div class="content">
|
||||
<ButtonD set_toggled/>
|
||||
@@ -212,7 +246,7 @@ pub fn Content(d: WriteSignal<bool>) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD<F>(d: WriteSignal<bool>) -> impl IntoView {
|
||||
pub fn ButtonD<F>(set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
@@ -285,9 +319,9 @@ 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.
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-0-5-7rz7qd?file=%2Fsrc%2Fmain.rs%3A1%2C2)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-0-5-7rz7qd?file=%2Fsrc%2Fmain.rs%3A1%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -321,7 +355,6 @@ pub fn App() -> impl IntoView {
|
||||
provide_context(SmallcapsContext(set_smallcaps));
|
||||
|
||||
view! {
|
||||
|
||||
<main>
|
||||
<p
|
||||
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
|
||||
@@ -353,12 +386,10 @@ pub fn App() -> impl IntoView {
|
||||
/// Button A receives a signal setter and updates the signal itself
|
||||
#[component]
|
||||
pub fn ButtonA(
|
||||
|
||||
/// Signal that will be toggled when the button is clicked.
|
||||
setter: WriteSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
@@ -370,7 +401,6 @@ pub fn ButtonA(
|
||||
/// Button B receives a closure
|
||||
#[component]
|
||||
pub fn ButtonB<F>(
|
||||
|
||||
/// Callback that will be invoked when the button is clicked.
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
@@ -378,7 +408,6 @@ where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
{
|
||||
view! {
|
||||
|
||||
<button
|
||||
on:click=on_click
|
||||
>
|
||||
@@ -404,7 +433,6 @@ where
|
||||
#[component]
|
||||
pub fn ButtonC() -> impl IntoView {
|
||||
view! {
|
||||
|
||||
<button>
|
||||
"Toggle Italics"
|
||||
</button>
|
||||
@@ -418,7 +446,6 @@ pub fn ButtonD() -> impl IntoView {
|
||||
let setter = use_context::<SmallcapsContext>().unwrap().0;
|
||||
|
||||
view! {
|
||||
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
@@ -428,9 +455,8 @@ pub fn ButtonD() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -47,7 +47,6 @@ Let’s define a component that takes some children and a render prop.
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
@@ -97,7 +96,7 @@ a component that takes its children and turns them into an unordered list.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn WrapsChildren(Children) -> impl IntoView {
|
||||
pub fn WrapsChildren(children: Children) -> impl IntoView {
|
||||
// Fragment has `nodes` field that contains a Vec<View>
|
||||
let children = children()
|
||||
.nodes
|
||||
@@ -123,9 +122,9 @@ view! {
|
||||
}
|
||||
```
|
||||
|
||||
[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)
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-0-5-m4jwhp?file=%2Fsrc%2Fmain.rs%3A1%2C1)
|
||||
|
||||
<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>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-0-5-m4jwhp?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
<details>
|
||||
<summary>CodeSandbox Source</summary>
|
||||
@@ -179,7 +178,6 @@ pub fn App() -> impl IntoView {
|
||||
/// Displays a `render_prop` and some children within markup.
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
@@ -204,7 +202,7 @@ where
|
||||
|
||||
/// Wraps each child in an `<li>` and embeds them in a `<ul>`.
|
||||
#[component]
|
||||
pub fn WrapsChildren(Children) -> impl IntoView {
|
||||
pub fn WrapsChildren(children: Children) -> impl IntoView {
|
||||
// children() returns a `Fragment`, which has a
|
||||
// `nodes` field that contains a Vec<View>
|
||||
// this means we can iterate over the children
|
||||
@@ -223,9 +221,8 @@ pub fn WrapsChildren(Children) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
leptos::mount_to_body(App)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
98
docs/book/src/view/builder.md
Normal file
98
docs/book/src/view/builder.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# No Macros: The View Builder Syntax
|
||||
|
||||
> If you’re perfectly happy with the `view!` macro syntax described so far, you’re welcome to skip this chapter. The builder syntax described in this section is always available, but never required.
|
||||
|
||||
For one reason or another, many developers would prefer to avoid macros. Perhaps you don’t like the limited `rustfmt` support. (Although, you should check out [`leptosfmt`](https://github.com/bram209/leptosfmt), which is an excellent tool!) Perhaps you worry about the effect of macros on compile time. Perhaps you prefer the aesthetics of pure Rust syntax, or you have trouble context-switching between an HTML-like syntax and your Rust code. Or perhaps you want more flexibility in how you create and manipulate HTML elements than the `view` macro provides.
|
||||
|
||||
If you fall into any of those camps, the builder syntax may be for you.
|
||||
|
||||
The `view` macro expands an HTML-like syntax to a series of Rust functions and method calls. If you’d rather not use the `view` macro, you can simply use that expanded syntax yourself. And it’s actually pretty nice!
|
||||
|
||||
First off, if you want you can even drop the `#[component]` macro: a component is just a setup function that creates your view, so you can define a component as a simple function call:
|
||||
|
||||
```rust
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView { }
|
||||
```
|
||||
|
||||
Elements are created by calling a function with the same name as the HTML element:
|
||||
|
||||
```rust
|
||||
p()
|
||||
```
|
||||
|
||||
You can add children to the element with [`.child()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child), which takes a single child or a tuple or array of types that implement [`IntoView`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html).
|
||||
|
||||
```rust
|
||||
p().child((em().child("Big, "), strong().child("bold "), "text"))
|
||||
```
|
||||
|
||||
Attributes are added with [`.attr()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.attr). This can take any of the same types that you could pass as an attribute into the view macro (types that implement [`IntoAttribute`](https://docs.rs/leptos/latest/leptos/trait.IntoAttribute.html)).
|
||||
|
||||
```rust
|
||||
p().attr("id", "foo").attr("data-count", move || count().to_string())
|
||||
```
|
||||
|
||||
Similarly, the `class:`, `prop:`, and `style:` syntaxes map directly onto [`.class()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.class), [`.prop()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.prop), and [`.style()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.style) methods.
|
||||
|
||||
Event listeners can be added with [`.on()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.on). Typed events found in [`leptos::ev`](https://docs.rs/leptos/latest/leptos/ev/index.html) prevent typos in event names and allow for correct type inference in the callback function.
|
||||
|
||||
```rust
|
||||
button()
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear")
|
||||
```
|
||||
|
||||
> Many additional methods can be found in the [`HtmlElement`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child) docs, including some methods that are not directly available in the `view` macro.
|
||||
|
||||
All of this adds up to a very Rusty syntax to build full-featured views, if you prefer this style.
|
||||
|
||||
```rust
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
div()
|
||||
.child((
|
||||
button()
|
||||
// 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"),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.child("-1"),
|
||||
span().child(("Value: ", move || count.get().value(), "!")),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.child("+1"),
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
This also has the benefit of being more flexible: because these are all plain Rust functions and methods, it’s easier to use them in things like iterator adapters without any additional “magic”:
|
||||
|
||||
```rust
|
||||
// take some set of attribute names and values
|
||||
let attrs: Vec<(&str, AttributeValue)> = todo!();
|
||||
// you can use the builder syntax to “spread” these onto the
|
||||
// element in a way that’s not possible with the view macro
|
||||
let p = attrs
|
||||
.into_iter()
|
||||
.fold(p(), |el, (name, value)| el.attr(name, value));
|
||||
|
||||
```
|
||||
|
||||
> ## Performance Note
|
||||
>
|
||||
> One caveat: the `view` macro applies significant optimizations in server-side-rendering (SSR) mode to improve HTML rendering performance significantly (think 2-4x faster, depending on the characteristics of any given app). It does this by analyzing your `view` at compile time and converting the static parts into simple HTML strings, rather than expanding them into the builder syntax.
|
||||
>
|
||||
> This means two things:
|
||||
>
|
||||
> 1. The builder syntax and `view` macro should not be mixed, or should only be mixed very carefully: at least in SSR mode, the output of the `view` should be treated as a “black box” that can’t have additional builder methods applied to it without causing inconsistencies.
|
||||
> 2. Using the builder syntax will result in less-than-optimal SSR performance. It won’t be slow, by any means (and it’s worth running your own benchmarks in any case), just slower than the `view`-optimized version.
|
||||
@@ -18,7 +18,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"hackernews",
|
||||
"hackernews_axum",
|
||||
"js-framework-benchmark",
|
||||
"leptos-tailwind-axum",
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
@@ -27,8 +26,9 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"ssr_modes",
|
||||
"ssr_modes_axum",
|
||||
"suspense_tests",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"tailwind_actix",
|
||||
"tailwind_csr",
|
||||
"tailwind_axum",
|
||||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
@@ -41,7 +41,7 @@ workspace = false
|
||||
description = "Generate the list of workspace members"
|
||||
script = '''
|
||||
examples=$(ls |
|
||||
grep -v README.md |
|
||||
grep -v .md |
|
||||
grep -v Makefile.toml |
|
||||
grep -v cargo-make |
|
||||
grep -v gtk |
|
||||
@@ -49,10 +49,12 @@ jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
|
||||
'''
|
||||
|
||||
[tasks.test-runner-report]
|
||||
[tasks.test-report]
|
||||
workspace = false
|
||||
description = "report ci test runners for each example - OPTION: [all]"
|
||||
description = "report web testing technology used by examples - OPTION: [all]"
|
||||
script = '''
|
||||
set -emu
|
||||
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
ITALIC="\e[3m"
|
||||
@@ -60,11 +62,10 @@ YELLOW="\e[0;33m"
|
||||
RESET="\e[0m"
|
||||
|
||||
echo
|
||||
echo "${YELLOW}Test Runner Report${RESET}"
|
||||
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
|
||||
echo "${YELLOW}Web Test Technology${RESET}"
|
||||
echo
|
||||
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
|
||||
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
|
||||
sed 's%./%%' |
|
||||
sed 's%/Makefile.toml%%' |
|
||||
grep -v Makefile.toml |
|
||||
@@ -75,38 +76,78 @@ start_path=$(pwd)
|
||||
for path in $makefile_paths; do
|
||||
cd $path
|
||||
|
||||
test_runner=
|
||||
crate_symbols=
|
||||
|
||||
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
|
||||
if [ $test_count -gt 0 ]; then
|
||||
test_runner="-C"
|
||||
fi
|
||||
pw_count=$(find . -name playwright.config.ts | wc -l)
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"cucumber"*)
|
||||
crate_symbols=$crate_symbols"C"
|
||||
;;
|
||||
*"fantoccini"*)
|
||||
crate_symbols=$crate_symbols"D"
|
||||
;;
|
||||
esac
|
||||
done <"./Cargo.toml"
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"wasm-test.toml"*)
|
||||
test_runner=$test_runner"-W"
|
||||
*"cargo-make/wasm-test.toml"*)
|
||||
crate_symbols=$crate_symbols"W"
|
||||
;;
|
||||
*"playwright-test.toml"*)
|
||||
test_runner=$test_runner"-P"
|
||||
*"cargo-make/playwright-test.toml"*)
|
||||
crate_symbols=$crate_symbols"P"
|
||||
crate_symbols=$crate_symbols"N"
|
||||
;;
|
||||
*"cargo-leptos-test.toml"*)
|
||||
test_runner=$test_runner"-L"
|
||||
*"cargo-make/playwright-trunk-test.toml"*)
|
||||
crate_symbols=$crate_symbols"P"
|
||||
crate_symbols=$crate_symbols"T"
|
||||
;;
|
||||
*"cargo-make/trunk_server.toml"*)
|
||||
crate_symbols=$crate_symbols"T"
|
||||
;;
|
||||
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
|
||||
crate_symbols=$crate_symbols"L"
|
||||
;;
|
||||
*"cargo-make/cargo-leptos-test.toml"*)
|
||||
crate_symbols=$crate_symbols"L"
|
||||
if [ $pw_count -gt 0 ]; then
|
||||
crate_symbols=$crate_symbols"P"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done <"./Makefile.toml"
|
||||
|
||||
if [ ! -z "$1" ]; then
|
||||
# Sort list of tools
|
||||
sorted_crate_symbols=$(echo ${crate_symbols} | grep -o . | sort | tr -d "\n")
|
||||
|
||||
formatted_crate_symbols=" ➤ ${BOLD}${YELLOW}${sorted_crate_symbols}${RESET}"
|
||||
crate_line=$path
|
||||
if [ ! -z ${1+x} ]; then
|
||||
# Show all examples
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
elif [ ! -z $test_runner ]; then
|
||||
if [ ! -z $crate_symbols ]; then
|
||||
crate_line=$crate_line$formatted_crate_symbols
|
||||
fi
|
||||
echo $crate_line
|
||||
elif [ ! -z $crate_symbols ]; then
|
||||
# Filter out examples that do not run tests in `ci`
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
crate_line=$crate_line$formatted_crate_symbols
|
||||
echo $crate_line
|
||||
fi
|
||||
|
||||
cd ${start_path}
|
||||
done
|
||||
|
||||
c="${BOLD}${YELLOW}C${RESET} = Cucumber"
|
||||
d="${BOLD}${YELLOW}D${RESET} = WebDriver"
|
||||
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
|
||||
n="${BOLD}${YELLOW}N${RESET} = Node"
|
||||
p="${BOLD}${YELLOW}P${RESET} = Playwright"
|
||||
t="${BOLD}${YELLOW}T${RESET} = Trunk"
|
||||
w="${BOLD}${YELLOW}W${RESET} = WASM"
|
||||
|
||||
echo
|
||||
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
|
||||
echo "${ITALIC}Keys:${RESET} $c, $d, $l, $n, $p, $t, $w"
|
||||
echo
|
||||
'''
|
||||
|
||||
@@ -1,7 +1,47 @@
|
||||
# Examples
|
||||
# Examples README
|
||||
|
||||
## Main Branch
|
||||
|
||||
The examples in this directory are all built and tested against the current `main` branch.
|
||||
|
||||
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch and not the current release.
|
||||
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch but not the current release.
|
||||
|
||||
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).
|
||||
To see the examples as they were at the time of the `0.5.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.5.0/examples).
|
||||
|
||||
## Cargo Make
|
||||
|
||||
[Cargo Make](https://sagiegurari.github.io/cargo-make/) is used to build, test, and run examples.
|
||||
|
||||
Here are the highlights.
|
||||
|
||||
- Extendable custom task files are located in the [cargo-make](./cargo-make/) directory
|
||||
- Running a task will automatically install `cargo` dependencies
|
||||
- Each `Makefile.toml` file must extend the [cargo-make/main.toml](./cargo-make/main.toml) file
|
||||
- [cargo-make](./cargo-make/) files that end in `*-test.toml` configure web testing strategies
|
||||
- Run `cargo make test-report` to learn which examples have web tests
|
||||
|
||||
## Getting Started
|
||||
|
||||
Follow these steps to get any example up and running.
|
||||
|
||||
1. `cd` to the example root directory
|
||||
2. Run `cargo make ci` to setup and test the example
|
||||
3. Run `cargo make start` to run the example
|
||||
4. Open the client URL in the console output (<http://127.0.0.1:8080> or <http://127.0.0.1:3000> by default)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Example projects depend on the following tools. Please install them as needed.
|
||||
|
||||
- [Rust](https://www.rust-lang.org/)
|
||||
- Nightly Rust
|
||||
- Run `rustup toolchain install nightly`
|
||||
- Run `rustup target add wasm32-unknown-unknown`
|
||||
- [Cargo Make](https://sagiegurari.github.io/cargo-make/)
|
||||
- Run `cargo install --force cargo-make`
|
||||
- Setup a command alias like `alias cm='cargo make'` to reduce typing (**_Optional_**)
|
||||
- [Trunk](https://github.com/thedodd/trunk)
|
||||
- Run `cargo install trunk`
|
||||
- [Node Version Manager](https://github.com/nvm-sh/nvm/) (**_Optional_**)
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [pnpm](https://pnpm.io/) (**_Optional_**)
|
||||
|
||||
68
examples/SSR_NOTES.md
Normal file
68
examples/SSR_NOTES.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Server Side Rendering
|
||||
|
||||
## Cargo Leptos
|
||||
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
|
||||
1. Install cargo-leptos
|
||||
|
||||
```bash
|
||||
cargo install --locked cargo-leptos
|
||||
```
|
||||
|
||||
2. Build the site in watch mode, recompiling on file changes
|
||||
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
```
|
||||
|
||||
## WASM Pack
|
||||
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. For examples with CSS you also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
|
||||
1. Install wasm-pack
|
||||
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
2. Build the Webassembly used to hydrate the HTML from the server
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
### Server Side Rendering With Hydration
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
|
||||
to generate the WebAssembly to hydrate the HTML delivered from the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above before running
|
||||
> `cargo run`
|
||||
@@ -1 +1,4 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# `<AnimatedShow>` combined with CSS animations
|
||||
# Animated Show Example
|
||||
|
||||
This is a very simple example of the `<AnimatedShow>` component.
|
||||
|
||||
This component is an extension for the `<Show>` component and it will not take in a fallback, but it will unmount the
|
||||
component from the DOM after a given duration. This makes it possible to have really easy unmount animations with just
|
||||
The `<AnimatedShow>` component is an extension for the `<Show>` component and it will not take in a fallback, but it will unmount the component from the DOM after a given duration. This makes it possible to have really easy unmount animations with just
|
||||
CSS.
|
||||
|
||||
Just execute `trunk serve` to start the demo.
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
@@ -2,7 +2,3 @@ extend = { path = "./cargo-leptos.toml" }
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = ["install-cargo-leptos", "cargo-leptos-e2e"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
7
examples/cargo-make/cargo-leptos-webdriver-test.toml
Normal file
7
examples/cargo-make/cargo-leptos-webdriver-test.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
extend = [
|
||||
{ path = "./cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/webdriver.toml" },
|
||||
]
|
||||
|
||||
[tasks.integration-test]
|
||||
dependencies = ["install-cargo-leptos", "start-webdriver", "cargo-leptos-e2e"]
|
||||
@@ -1,6 +1,10 @@
|
||||
[tasks.install-cargo-leptos]
|
||||
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
args = ["leptos", "end-to-end"]
|
||||
|
||||
[tasks.build]
|
||||
clear = true
|
||||
command = "cargo"
|
||||
@@ -23,33 +27,6 @@ args = ["check-all-features", "--release"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.start-client]
|
||||
dependencies = ["install-cargo-leptos"]
|
||||
command = "cargo"
|
||||
args = ["leptos", "watch"]
|
||||
|
||||
[tasks.stop-client]
|
||||
condition = { env_set = ["APP_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
|
||||
pkill -f todo_app_sqlite
|
||||
fi
|
||||
|
||||
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
|
||||
pkill -f cargo-leptos
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.client-status]
|
||||
condition = { env_set = ["APP_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${APP_PROCESS_NAME}) ]; then
|
||||
echo " ${APP_PROCESS_NAME} is not running"
|
||||
else
|
||||
echo " ${APP_PROCESS_NAME} is up"
|
||||
fi
|
||||
|
||||
if [ -z $(pidof cargo-leptos) ]; then
|
||||
echo " cargo-leptos is not running"
|
||||
else
|
||||
echo " cargo-leptos is up"
|
||||
fi
|
||||
'''
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
[tasks.clean]
|
||||
dependencies = [
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
command = "rm"
|
||||
args = ["-rf", "target"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
script = '''
|
||||
find . -type d -name dist | xargs rm -rf
|
||||
'''
|
||||
|
||||
[tasks.clean-node_modules]
|
||||
script = '''
|
||||
|
||||
34
examples/cargo-make/client-process.toml
Normal file
34
examples/cargo-make/client-process.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[tasks.start-client]
|
||||
|
||||
[tasks.stop-client]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ ! -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
pkill -ef ${CLIENT_PROCESS_NAME}
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.client-status]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
echo " ${CLIENT_PROCESS_NAME} is not running"
|
||||
else
|
||||
echo " ${CLIENT_PROCESS_NAME} is up"
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.maybe-start-client]
|
||||
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
|
||||
echo " Starting ${CLIENT_PROCESS_NAME}"
|
||||
if [ -z ${SPAWN_CLIENT_PROCESS} ];then
|
||||
cargo make start-client ${@} &
|
||||
else
|
||||
cargo make start-client ${@}
|
||||
fi
|
||||
else
|
||||
echo " ${CLIENT_PROCESS_NAME} is already started"
|
||||
fi
|
||||
'''
|
||||
@@ -1,8 +1,9 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/compile.toml" },
|
||||
{ path = "../cargo-make/clean.toml" },
|
||||
{ path = "../cargo-make/lint.toml" },
|
||||
{ path = "../cargo-make/node.toml" },
|
||||
{ path = "./compile.toml" },
|
||||
{ path = "./clean.toml" },
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./node.toml" },
|
||||
{ path = "./process.toml" },
|
||||
]
|
||||
|
||||
# CI Stages
|
||||
|
||||
14
examples/cargo-make/playwright-trunk-test.toml
Normal file
14
examples/cargo-make/playwright-trunk-test.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/playwright.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
[tasks.integration-test]
|
||||
description = "Run integration test with automated start and stop of processes"
|
||||
env = { SPAWN_CLIENT_PROCESS = "1" }
|
||||
dependencies = ["start", "wait-one", "test-playwright", "stop"]
|
||||
|
||||
[tasks.wait-one]
|
||||
script = '''
|
||||
sleep 1
|
||||
'''
|
||||
13
examples/cargo-make/process.toml
Normal file
13
examples/cargo-make/process.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
extend = [
|
||||
{ path = "./client-process.toml" },
|
||||
{ path = "./server-process.toml" },
|
||||
]
|
||||
|
||||
[tasks.start]
|
||||
dependencies = ["maybe-start-server", "maybe-start-client"]
|
||||
|
||||
[tasks.status]
|
||||
dependencies = ["server-status", "client-status"]
|
||||
|
||||
[tasks.stop]
|
||||
dependencies = ["stop-client", "stop-server"]
|
||||
34
examples/cargo-make/server-process.toml
Normal file
34
examples/cargo-make/server-process.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[tasks.start-server]
|
||||
|
||||
[tasks.stop-server]
|
||||
condition = { env_set = ["SERVER_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ ! -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
|
||||
pkill -ef ${SERVER_PROCESS_NAME}
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.server-status]
|
||||
condition = { env_set = ["SERVER_PROCESS_NAME"] }
|
||||
script = '''
|
||||
if [ -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
|
||||
echo " ${SERVER_PROCESS_NAME} is not running"
|
||||
else
|
||||
echo " ${SERVER_PROCESS_NAME} is up"
|
||||
fi
|
||||
'''
|
||||
|
||||
[tasks.maybe-start-server]
|
||||
condition = { env_set = ["SERVER_PROCESS_NAME"] }
|
||||
script = '''
|
||||
YELLOW="\e[0;33m"
|
||||
RESET="\e[0m"
|
||||
|
||||
if [ -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
|
||||
echo " Starting ${SERVER_PROCESS_NAME}"
|
||||
echo " ${YELLOW}>> Run cargo make stop to end process${RESET}"
|
||||
cargo make start-server ${@} &
|
||||
else
|
||||
echo " ${SERVER_PROCESS_NAME} is already started"
|
||||
fi
|
||||
'''
|
||||
@@ -1,18 +1,10 @@
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "trunk"
|
||||
|
||||
[tasks.build]
|
||||
command = "trunk"
|
||||
args = ["build"]
|
||||
|
||||
[tasks.start-trunk]
|
||||
[tasks.start-client]
|
||||
command = "trunk"
|
||||
args = ["serve", "${@}"]
|
||||
|
||||
[tasks.stop-trunk]
|
||||
script = '''
|
||||
pkill -f "cargo-make"
|
||||
pkill -f "trunk"
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.dev]
|
||||
dependencies = ["start-trunk"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
This example creates a simple counter in a client side rendered app with Rust and WASM!
|
||||
|
||||
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.
|
||||
## Getting Started
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
@@ -25,7 +25,7 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "=0.2.86"
|
||||
wasm-bindgen = "0.2.87"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "counter_isomorphic"
|
||||
|
||||
@@ -2,42 +2,6 @@
|
||||
|
||||
This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
|
||||
|
||||
## Client Side Rendering
|
||||
For this example the server must store the counter state since it can be modified by many users.
|
||||
This means it is not possible to produce a working CSR-only version as a non-static server is required.
|
||||
## Getting Started
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
|
||||
1. Install cargo-leptos
|
||||
```bash
|
||||
cargo install --locked cargo-leptos
|
||||
```
|
||||
2. Build the site in watch mode, recompiling on file changes
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
```
|
||||
|
||||
## Server Side Rendering without cargo-leptos
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. For examples with CSS you also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
1. Install wasm-pack
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
2. Build the Webassembly used to hydrate the HTML from the server
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
@@ -15,13 +15,12 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
|
||||
#[server(GetServerCount, "/api")]
|
||||
#[server]
|
||||
pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
Ok(COUNT.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
#[server(AdjustServerCount, "/api")]
|
||||
#[server]
|
||||
pub async fn adjust_server_count(
|
||||
delta: i32,
|
||||
msg: String,
|
||||
@@ -33,7 +32,7 @@ pub async fn adjust_server_count(
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
#[server(ClearServerCount, "/api")]
|
||||
#[server]
|
||||
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
COUNT.store(0, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&0).await;
|
||||
@@ -147,6 +146,8 @@ pub fn Counter() -> impl IntoView {
|
||||
// but uses HTML forms to submit the actions
|
||||
#[component]
|
||||
pub fn FormCounter() -> impl IntoView {
|
||||
// these struct names are auto-generated by #[server]
|
||||
// they are just the PascalCased versions of the function names
|
||||
let adjust = create_server_action::<AdjustServerCount>();
|
||||
let clear = create_server_action::<ClearServerCount>();
|
||||
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
This example creates a simple counter whose state is persisted and synced in the url with query params.
|
||||
|
||||
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
|
||||
## Getting Started
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
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.
|
||||
## Getting Started
|
||||
|
||||
Issue the `cargo make test-flow` command to run unit and wasm tests.
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
@@ -3,9 +3,8 @@ use leptos::{ev, html::*, *};
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(Count::new(initial_value, step));
|
||||
let count = RwSignal::new(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()
|
||||
// children can be added with .child()
|
||||
@@ -18,27 +17,16 @@ pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
// 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()))
|
||||
.on(ev::click, move |_| count.update(Count::clear))
|
||||
.child("Clear"),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.on(ev::click, move |_| count.update(Count::decrease))
|
||||
.child("-1"),
|
||||
span()
|
||||
.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(
|
||||
span().child(("Value: ", move || count.get().value(), "!")),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.on(ev::click, move |_| count.update(Count::increase))
|
||||
.child("+1"),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
]
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
This example showcases a basic leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events.
|
||||
|
||||
## Client Side Rendering
|
||||
## Getting Started
|
||||
|
||||
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
@@ -65,7 +65,7 @@ pub fn Counters() -> impl IntoView {
|
||||
<For
|
||||
each=counters
|
||||
key=|counter| counter.0
|
||||
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
children=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! {
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
@@ -88,9 +88,17 @@ fn Counter(
|
||||
set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
// just an example of how a cleanup function works
|
||||
// this will run when the scope is disposed, i.e., when this row is deleted
|
||||
on_cleanup(|| log::debug!("deleted a row"));
|
||||
// because the signal was created in the parent scope, it won't be disposed
|
||||
// of until the parent scope is. but we no longer need it, so we'll dispose of
|
||||
// it when this row is deleted, instead. if we don't dispose of it here,
|
||||
// this memory will "leak," i.e., the signal will continue to exist until the
|
||||
// parent component is removed. in the case of this component, where it's the
|
||||
// root, that's the lifetime of the program.
|
||||
on_cleanup(move || {
|
||||
log::debug!("deleted a row");
|
||||
value.dispose();
|
||||
});
|
||||
|
||||
view! {
|
||||
<li>
|
||||
|
||||
@@ -5,12 +5,15 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2.87"
|
||||
wasm-bindgen-test = "0.3.37"
|
||||
pretty_assertions = "1.4.0"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
features = [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-test.toml" },
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user